diff --git a/CHANGELOG.md b/CHANGELOG.md index fe6a399..b940559 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [5.5.0-beta.1] 2024-10-30 +### Added +- Support servers running DLC content (fixes Frostline servers) +- Text autocompletion in maps search field +- Add disk space warning to popup dialog +### Fixed +- Abort fallback query method if DLC is required + ## [5.4.2-beta.1] 2024-10-05 ### Fixed - Sanitize third-party API IDs to remove UGC collisions diff --git a/dzgui.sh b/dzgui.sh index 638435f..3fee9a7 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -525,14 +525,14 @@ fetch_a2s(){ logger INFO "Updated A2S helper to sha '$sha'" } fetch_dzq(){ - local sum="232f42b98a3b50a0dd6e73fee55521b2" + local sum="9caed1445c45832f4af87736ba3f9637" local file="$helpers_path/a2s/dayzquery.py" if [[ -f $file ]] && [[ $(get_hash "$file") == $sum ]]; then logger INFO "DZQ is current" return 0 fi - local sha=ccc4f71b48610a1885706c9d92638dbd8ca012a5 - local author="yepoleb" + local sha=788e85b82189cb3485d4a08ee350c67994b29ea3 + local author="aclist" local repo="dayzquery" local url="https://raw.githubusercontent.com/$author/$repo/$sha/dayzquery.py" curl -Ls "$url" > "$file" @@ -566,10 +566,10 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="9cac4d3b87ef292e7d30b25ca86cc438" + ["ui.py"]="91ea4d842d35a7d5e1441174f1f463c5" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="71d3a941209792a41f381f011e78def8" + ["funcs"]="105e7be170eea48ce61fcfe7b50b8f59" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" diff --git a/helpers/funcs b/helpers/funcs index 8bf10c5..6aa8790 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -1,6 +1,6 @@ #!/usr/bin/env bash set -o pipefail -version=5.4.2 +version=5.5.0 #CONSTANTS aid=221100 @@ -280,6 +280,14 @@ initialize_remote_servers(){ res=$(get_remote_servers) parse_server_json "$res" >> "$file" } +is_dlc(){ + local dlc + local ip="$1" + local gport="$2" + local res="$(curl -Ls "https://api.steampowered.com/IGameServersService/GetServerList/v1/?filter=\gameaddr\\${ip}:${gport}\appid\221100&key=$steam_api")" + dlc=$(<<< "$res" jq '.response.servers[].gametype|contains("isDLC")') + printf "%s\n" "$dlc" +} a2s(){ local ip="$1" local qport="$2" @@ -1081,25 +1089,39 @@ try_fallback(){ local qport="$2" local mode="$3" if [[ $mode != "rules" ]] && [[ $mode != "names" ]]; then + logger WARN "Fallback query API called with an invalid mode: '$mode'" return 1 fi [[ -z $api_key ]] && return 1 + logger INFO "Using fallback query API on '$ip:$qport' with mode: '$mode'" local res=$(curl -s "$bm_api" -H "Authorization: Bearer "$api_key"" \ -G -d "filter[game]=$game" \ -d "filter[search]=%22${ip}:${qport}%22") [[ -z $res ]] && return 1 local len=$(<<< "$res" jq '.data|length') [[ $len -eq 0 ]] && return 1 + + # cull defunct entries local record=$(<<< "$res" jq -r ' .data[].attributes - |select(.status != "removed").details') + |select(.status != "removed")') + + # reverse lookup gport + local dlc + local gport=$(<<< "$record" jq -r '.port') + dlc=$(is_dlc "$ip" "$gport") + if [[ $dlc == "true" ]]; then + logger FAIL "Fallback query API called on DLC-enabled server" + return 1 + fi + case "$mode" in "rules") - <<< "$record" jq '.modIds[]' + <<< "$record" jq '.details.modIds[]' ;; "names") <<< "$record" jq ' - [.modNames, .modIds] as [$n, $i] + .details|[.modNames, .modIds] as [$n, $i] | {names: $n, ids: $i}' ;; esac @@ -1403,7 +1425,7 @@ manual_mod_install(){ if [[ $mode == "auto" ]] || [[ $mode == "force" ]]; then $steam_cmd "steam://url/CommunityFilePage/${stage_mods[$i]}+workshop_download_item $aid ${stage_mods[$i]}" - echo "# Opening workshop page for ${stage_mods[$i]}" + echo "# Opening workshop page for ${stage_mods[$i]}. If you see no progress, you may be out of disk space." else $steam_cmd "steam://url/CommunityFilePage/${stage_mods[$i]}" echo "# Opening workshop page for ${stage_mods[$i]}. If you see no progress after subscribing, try unsubscribing and resubscribing again until the download commences." diff --git a/helpers/ui.py b/helpers/ui.py index 5e77f68..7e7e4f6 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -19,7 +19,7 @@ gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib, Gdk, GObject, Pango from enum import Enum -# 5.4.1 +# 5.5.0 app_name = "DZGUI" start_time = 0 @@ -895,8 +895,7 @@ class TreeView(Gtk.TreeView): case Gdk.KEY_m: if self.get_first_col() == "Mod": return - grid.right_panel.filters_vbox.maps_combo.grab_focus() - grid.right_panel.filters_vbox.maps_combo.popup() + grid.right_panel.filters_vbox.maps_entry.grab_focus() case _: return False elif keyname.isnumeric() and int(keyname) > 0: @@ -1566,7 +1565,7 @@ def KeysDialog(parent, text, mode): Enter/Space/Double click: connect to server Right-click on row/Ctrl-l: displays additional context menus Ctrl-f: jump to keyword field - Ctrl-m: jump to maps dropdown + Ctrl-m: jump to maps field Ctrl-d: toggle dry run (debug) mode Ctrl-r: refresh player count for active row 1-9: toggle filter ON/OFF @@ -1639,7 +1638,7 @@ class ModDialog(GenericDialog): def _load(): dialog.destroy() if data.returncode == 1: - spawn_dialog(parent, "Server has no mods installed", "NOTIFY") + spawn_dialog(parent, "Server has no mods installed or is unsupported in this mode", "NOTIFY") return self.show_all() self.set_markup("Modlist (%s mods)" %(mod_count)) @@ -1858,15 +1857,29 @@ class FilterPanel(Gtk.Box): self.keyword_entry.set_placeholder_text("Filter by keyword") self.keyword_entry.connect("activate", self._on_keyword_enter) self.keyword_entry.connect("key-press-event", self._on_esc_pressed) + + completion = Gtk.EntryCompletion(inline_completion=True) + completion.set_text_column(0) + completion.set_minimum_key_length(1) + completion.connect("match_selected", self._on_completer_match) renderer_text = Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END) - self.maps_combo = Gtk.ComboBox.new_with_model(map_store) + self.maps_combo = Gtk.ComboBox.new_with_entry() + self.maps_combo.set_entry_text_column(0) + self.maps_combo.set_model(map_store) + + # instantiate maps completer entry + self.maps_entry = self.maps_combo.get_child() + self.maps_entry.set_completion(completion) + self.maps_entry.set_placeholder_text("Filter by map") + self.maps_entry.connect("changed", self._on_map_completion, True) + self.maps_entry.connect("key-press-event", self._on_map_entry_keypress) + self.maps_combo.pack_start(renderer_text, True) self.maps_combo.add_attribute(renderer_text, "text", 0) self.maps_combo.connect("changed", self._on_map_changed) self.maps_combo.connect("key-press-event", self._on_esc_pressed) - self.pack_start(self.filters_label, False, False, True) self.pack_start(self.keyword_entry, False, False, True) self.pack_start(self.maps_combo, False, False, True) @@ -1874,6 +1887,37 @@ class FilterPanel(Gtk.Box): for i, check in enumerate(checks[0:]): self.pack_start(checks[i], False, False, True) + def _on_map_entry_keypress(self, entry, event): + match event.keyval: + case Gdk.KEY_Return: + text = entry.get_text() + if text is None: + return + # if entry is exact match for value in liststore, + # trigger map change function + for i in enumerate(map_store): + if text == i[1][0]: + self.maps_combo.set_active(i[0]) + self._on_map_changed(self.maps_combo) + case Gdk.KEY_Escape: + GLib.idle_add(self.restore_focus_to_treeview) + # TODO: this is a workaround for widget.grab_remove() + # set cursor position to SOL when unfocusing + text = self.maps_entry.get_text() + self.maps_entry.set_position(len(text)) + case _: + return + + def _on_completer_match(self, completion, model, iter): + self.maps_combo.set_active_iter(iter) + + def _on_map_completion(self, entry, editable): + text = entry.get_text() + completion = entry.get_completion() + + if len(text) >= completion.get_minimum_key_length(): + completion.set_model(map_store) + self._on_map_changed(self.maps_combo) def grab_keyword_focus(self): self.keyword_entry.grab_focus() @@ -1884,9 +1928,11 @@ class FilterPanel(Gtk.Box): return False def _on_esc_pressed(self, entry, event): - keyname = Gdk.keyval_name(event.keyval) - if keyname == "Escape": - GLib.idle_add(self.restore_focus_to_treeview) + match event.keyval: + case Gdk.KEY_Escape: + GLib.idle_add(self.restore_focus_to_treeview) + case _: + return True def get_outer_grid(self): panel = self.get_parent() @@ -1952,13 +1998,19 @@ class FilterPanel(Gtk.Box): tree_iter = combo.get_active_iter() if tree_iter is not None: - selected_map.clear() + # take no action if completer query is same as current map sel + old_sel = selected_map[0].split("Map=")[1] model = combo.get_model() selection = model[tree_iter][0] - selected_map.append("Map=" + selection) - logger.info("User selected map '%s'" %(selection)) - filter_servers(transient_parent, self, treeview, context) + if selection == old_sel: + return + selected_map.clear() + if selection is not None: + selected_map.append("Map=" + selection) + logger.info("User selected map '%s'" %(selection)) + filter_servers(transient_parent, self, treeview, context) + self.maps_entry.set_text(selection) def main():