diff --git a/CHANGELOG.md b/CHANGELOG.md index 8caec7d..0aaa350 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [5.6.0-beta.18] 2024-12-14 +### Added +- Open Steam workshop subscriptions dialog +- Additional logging +### Fixed +- Empty dialog popups if user manually deletes local mods while application is running +- Abort DayZ path discovery if VDF if Steam files are not synched +### Changed +- Admonish user to restart Steam in error dialog if DayZ path could not be found + ## [5.6.0-beta.17] 2024-12-14 ### Added - Additional logging diff --git a/dzgui.sh b/dzgui.sh index 86c478e..ce0e203 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -578,10 +578,10 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="be3da1e542d14105f4358dd38901e25a" + ["ui.py"]="99544ccef6060125509c4b689a808a15" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="37897aa36bc2fb6286cee02c8bb07258" + ["funcs"]="98261fdba4323f77c6dd610c1efc4d11" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" @@ -747,7 +747,7 @@ find_library_folder(){ local search_path="$1" steam_path="$(python3 "$helpers_path/vdf2json.py" -i "$1/steamapps/libraryfolders.vdf" \ | jq -r '.libraryfolders[]|select(.apps|has("221100")).path')" - if [[ ! $? -eq 0 ]]; then + if [[ ! $? -eq 0 ]] || [[ -z $steam_path ]]; then logger WARN "Failed to parse Steam path using '$search_path'" return 1 fi @@ -800,7 +800,7 @@ create_config(){ find_library_folder "$default_steam_path" if [[ -z $steam_path ]]; then logger raise_error "Steam path was empty" - zenity --question --text="DayZ not found or not installed at the Steam library given." --ok-label="Choose path manually" --cancel-label="Exit" + zenity --question --text="DayZ not found or not installed at the Steam library given. NOTE: if you recently installed DayZ or moved its location, you MUST restart Steam first for these changes to synch." --ok-label="Choose path manually" --cancel-label="Exit" if [[ $? -eq 0 ]]; then logger INFO "User selected file picker" file_picker diff --git a/helpers/funcs b/helpers/funcs index 22c6d44..576b5c3 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -104,6 +104,7 @@ declare -A funcs=( ["Connect by IP"]="validate_and_connect" ["Connect by ID"]="validate_and_connect" ["Connect from table"]="connect_from_table" +["find_id"]="find_id" ["toggle"]="toggle" ["Open link"]="open_link" ["filter"]="dump_servers" @@ -121,6 +122,7 @@ declare -A funcs=( ["is_in_favs"]="is_in_favs" ["show_log"]="show_log" ["Output system info to log file"]="generate_log" +["open_user_workshop"]="open_user_workshop" ["open_workshop_page"]="open_workshop_page" ["Add to my servers"]="update_favs_from_table" ["Remove from my servers"]="update_favs_from_table" @@ -332,18 +334,32 @@ is_in_favs(){ done return 1 } +find_id(){ + local file="$default_steam_path/config/loginusers.vdf" + [[ ! -f $file ]] && return 1 + local res=$(python3 $HOME/.local/share/dzgui/helpers/vdf2json.py \ + -i "$file" | jq -r '.users + |to_entries[] + |select(.value.MostRecent=="1") + |.key' + ) + [[ -z $res ]] && return 1 + printf "%s" "$res" + return 0 +} list_mods(){ local symlink local sep local name local base_dir local size + local mods if [[ -z $(installed_mods) ]] || [[ -z $(find $workshop_dir -maxdepth 2 -name "*.cpp" | grep .cpp) ]]; then printf "No mods currently installed or incorrect path set." logger WARN "Found no locally installed mods" return 1 else - for dir in $(find $game_dir/* -maxdepth 1 -type l); do + mods=$(for dir in $(find $game_dir/* -maxdepth 1 -type l); do symlink=$(basename $dir) sep="␞" name=$(awk -F\" '/name/ {print $2}' "${dir}/meta.cpp") @@ -351,7 +367,14 @@ list_mods(){ size=$(du -s "$(readlink -f "$game_dir/$symlink")" | awk '{print $1}') size=$(python3 -c "n=($size/1024) +.005; print(round(n,4))") LC_NUMERIC=C printf "%s$sep%s$sep%s$sep%3.3f\n" "$name" "$symlink" "$base_dir" "$size" - done | sort -k1 + done | sort -k1) + # user may have manually pruned mods out-of-band + # handle directory detritus but no actual mods + if [[ -z $mods ]]; then + printf "No mods currently installed or incorrect path set." + return 1 + fi + echo "$mods" fi } installed_mods(){ @@ -1051,6 +1074,12 @@ update_config_val(){ show_log(){ < "$debug_log" sed 's/Keyword␞/Keyword/' } +open_user_workshop(){ + shift + local id="$1" + url="https://steamcommunity.com/profiles/$id/myworkshopfiles/?appid=$aid&browsefilter=mysubscriptions" + $steam_cmd steam://openurl/$url & +} open_workshop_page(){ shift local id="$1" @@ -1173,7 +1202,7 @@ legacy_symlinks(){ logger INFO "Removing legacy symlinks" for d in "$game_dir"/*; do if [[ $d =~ @[0-9]+-.+ ]]; then - logger INFO "Unlinking $d" + logger INFO "Unlinking '$d'" unlink "$d" fi done @@ -1183,11 +1212,14 @@ legacy_symlinks(){ logger INFO "Removing legacy encoding format" for d in "${mod_dirs[@]}"; do # suppress errors if mods are downloading at boot + logger INFO "Testing directory '$d'" [[ ! -f "$d/meta.cpp" ]] && continue local id=$(awk -F"= " '/publishedid/ {print $2}' "$d"/meta.cpp | awk -F\; '{print $1}') + logger INFO "Given id is '$id'" local encoded_id=$(echo "$id" | awk '{printf("%c",$1)}' | base64 | sed 's/\//_/g; s/=//g; s/+/]/g') + logger INFO "Resolved id is '$encoded_id'" if [[ -h "$game_dir/@$encoded_id" ]]; then - logger INFO "Unlinking $game_dir/@$encoded_id" + logger INFO "Unlinking '$game_dir/@$encoded_id'" unlink "$game_dir/@$encoded_id" fi done diff --git a/helpers/ui.py b/helpers/ui.py index b5f01c0..38abd1b 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -215,6 +215,7 @@ class RowType(EnumWithAttrs): "label": "Toggle mod install mode", "tooltip": "Switch between manual and auto mod installation", "default": "manual", + "link_label": "Open Steam Workshop", "alt": "auto", "val": "auto_install" } @@ -756,6 +757,29 @@ def process_tree_option(input, treeview): case RowType.TGL_BRANCH: wait_msg = "Updating DZGUI branch" call_on_thread(False, "toggle", wait_msg, cmd_string) + case RowType.TGL_INSTALL: + if query_config(None, "auto_install")[0] == "1": + proc = call_out(transient_parent, "toggle", cmd_string) + grid.update_right_statusbar() + tooltip = format_metadata(command.dict["label"]) + transient_parent.grid.update_statusbar(tooltip) + return + # manual -> auto mode + proc = call_out(transient_parent, "find_id", "") + if proc.returncode == 1: + link=None + uid=None + else: + link=command.dict["link_label"] + uid=proc.stdout + manual_sub_msg = """\ + When switching from MANUAL to AUTO mod install mode, + DZGUI will manage mod installation and deletion for you. + To prevent conflicts with Steam Workshop subscriptions and old mods from being downloaded + when Steam updates, you should unsubscribe from any existing Workshop mods you manually subscribed to. + Open your Profile > Workshop Items and select 'Unsubscribe from all' + on the right-hand side, then click OK below to enable AUTO mod install mode.""" + LinkDialog(transient_parent, textwrap.dedent(manual_sub_msg), Popup.NOTIFY, link, command, uid) case _: proc = call_out(transient_parent, "toggle", cmd_string) grid.update_right_statusbar() @@ -2205,6 +2229,40 @@ class ModDialog(GenericDialog): subprocess.Popen(['/usr/bin/env', 'bash', funcs, "open_workshop_page", mod_id]) +class LinkDialog(GenericDialog): + def __init__(self, parent, text, mode, link, command, uid=None): + super().__init__(parent, text, mode) + + self.dialog = GenericDialog(parent, text, mode) + self.dialogBox = self.dialog.get_content_area() + self.dialog.set_default_response(Gtk.ResponseType.OK) + self.dialog.set_size_request(500, 0) + + if link is not None: + button = Gtk.Button(label=link) + button.set_margin_start(60) + button.set_margin_end(60) + button.connect("clicked", self._on_button_clicked, uid) + self.dialogBox.pack_end(button, False, False, 0) + + self.dialog.show_all() + self.dialog.connect("response", self._on_dialog_response, parent, command) + + def _on_button_clicked(self, button, uid): + subprocess.Popen(['/usr/bin/env', 'bash', funcs, "open_user_workshop", uid]) + + def _on_dialog_response(self, dialog, resp, parent, command): + match resp: + case Gtk.ResponseType.DELETE_EVENT: + return + case Gtk.ResponseType.OK: + self.dialog.destroy() + proc = call_out(parent, "toggle", command.dict["label"]) + parent.grid.update_right_statusbar() + tooltip = format_metadata(command.dict["label"]) + parent.grid.update_statusbar(tooltip) + + class EntryDialog(GenericDialog): def __init__(self, parent, text, mode, link): super().__init__(parent, text, mode)