From e6244370ddb2e77de16d0f0d9d089c295c9dd548 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 5 Aug 2024 06:03:15 +0900 Subject: [PATCH] feat: scan LAN servers --- CHANGELOG.md | 4 ++ dzgui.sh | 9 +-- helpers/funcs | 25 ++++++- helpers/lan | 46 ++++++++++++ helpers/query_v2.py | 10 +++ helpers/ui.py | 168 ++++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 249 insertions(+), 13 deletions(-) create mode 100755 helpers/lan diff --git a/CHANGELOG.md b/CHANGELOG.md index 36f36a2..9e9297f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [5.3.3-beta.3] 2024-08-04 +### Added +- Scan local area network for DayZ servers + ## [5.3.3-beta.2] 2024-08-03 ### Fixed - Clerical hotfix for previous player names fix diff --git a/dzgui.sh b/dzgui.sh index c13d3ed..090724e 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.3.3-beta.2 +version=5.3.3-beta.3 #CONSTANTS aid=221100 @@ -535,10 +535,11 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="f14772424461ec438579dec567db0634" - ["query_v2.py"]="1822bd1769ce7d7cb0d686a60f9fa197" + ["ui.py"]="9755d63904fb63dc48eff3397a31de17" + ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="28728839c031ae99002860a546352455" + ["funcs"]="2ebe0f7072f7a9459007b1d9d09e2f4f" + ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" local repo="dztui" diff --git a/helpers/funcs b/helpers/funcs index 6fa07eb..4351ea9 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -44,6 +44,7 @@ _cache_launch="$cache_dir/$prefix.launch_mods" _cache_address="$cache_dir/$prefix.launch_address" _cache_coords="$cache_path/$prefix.coords" _cache_cooldown="$cache_path/$prefix.cooldown" +_cache_lan="$cache_path/$prefix.lan" #XDG freedesktop_path="$HOME/.local/share/applications" @@ -55,6 +56,7 @@ km_helper="$helpers_path/latlon" sums_path="$helpers_path/sums.md5" query_helper="$helpers_path/query_v2.py" func_helper="$helpers_path/funcs" +lan_helper="$helpers_path/lan" #STEAM PATHS workshop_path="$steam_path/steamapps/workshop" @@ -125,8 +127,19 @@ declare -A funcs=( ["force_update"]="force_update" ["Handshake"]="final_handshake" ["get_player_count"]="get_player_count" +["lan_scan"]="lan_scan" ) +lan_scan(){ + local port="$1" + local res + res=$("$lan_helper" "$port") + if [[ $? -ne 0 ]]; then + printf "\n" + else + printf "%s\n" "$res" + fi +} get_player_count(){ shift local res @@ -589,6 +602,16 @@ dump_servers(){ _iterate "$file" "${iters[@]}" fi ;; + *Scan[[:space:]]LAN[[:space:]]servers*) + local port=$(<<< "$subcontext" awk -F: '{print $2}') + local file="$_cache_lan" + if [[ ! $subcontext =~ Name ]]; then + [[ -f $file ]] && rm $file + local lan=$(lan_scan $port) + readarray -t iters <<< "$lan" + _iterate "$file" "${iters[@]}" + fi + ;; esac shift logger INFO "Server context is '$subcontext', reading from file '$file'" @@ -988,8 +1011,6 @@ query_defunct(){ -d "$(payload)" 'https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/?format=json' } local result=$(post | jq -r '.[].publishedfiledetails[] | select(.result==1) | "\(.file_size) \(.publishedfileid)"') - local result2=$(post | jq -r '') - echo "$result2" > $HOME/json <<< "$result" awk '{print $2}' } encode(){ diff --git a/helpers/lan b/helpers/lan new file mode 100755 index 0000000..cf16934 --- /dev/null +++ b/helpers/lan @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +query_name(){ + local ip="$1" + local port="$2" + local api="$HOME/.local/share/dzgui/helpers/query_v2.py" + python3 "$api" "$ip" "$port" test +} + +scan(){ + local ip="$1" + local port="$2" + local res=$(query_name "$ip" "$port") + [[ -z $res ]] && return 1 + printf "%s\n" "${ip}:XXX:${port}" +} + +get_netmask(){ + ip r \ + | awk '/default/ {print $3}' \ + | uniq \ + | awk -F. 'OFS="."{print $1,$2,$3}' +} + +iter(){ + _testping(){ + ping -c1 -i 0.1 -w 1 "$1" 2>/dev/null 1>&2 + [[ $? -eq 0 ]] && echo "$1" + } + export -f _testping + local mask=$(get_netmask) + # GNU parallel is not available OOTB on Steam Deck + for i in $(seq 1 255); do + echo "${mask}.${i}" + done | xargs -I {} -P 200 bash -c '_testping "{}"' +} + +export -f scan +export -f query_name + +DZG_LAN_PORT="$1" + +readarray -t ips < <(iter) +for i in "${ips[@]}"; do + scan "$i" $DZG_LAN_PORT +done diff --git a/helpers/query_v2.py b/helpers/query_v2.py index 4ed62e9..1033d5b 100644 --- a/helpers/query_v2.py +++ b/helpers/query_v2.py @@ -5,6 +5,14 @@ import json from a2s import dayzquery sys.path.append('a2s') +def test_local(ip, qport): + try: + info = a2s.info((ip, int(qport)), 0.5) + name = info.server_name + print(name) + except: + sys.exit(1) + def get_info(ip, qport): try: info = a2s.info((ip, int(qport))) @@ -73,3 +81,5 @@ match mode: get_rules(ip, qport) case "names": get_names(ip, qport) + case "test": + test_local(ip, qport) diff --git a/helpers/ui.py b/helpers/ui.py index b830ce8..4612b40 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -17,6 +17,7 @@ import time locale.setlocale(locale.LC_ALL, '') gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib, Gdk, GObject, Pango +from enum import Enum # 5.2.3 app_name = "DZGUI" @@ -91,7 +92,8 @@ connect = [ ("Quick-connect to favorite server",), ("Recent servers",), ("Connect by IP",), - ("Connect by ID",) + ("Connect by ID",), + ("Scan LAN servers",) ] manage = [ ("Add server by IP",), @@ -143,6 +145,7 @@ status_tooltip = { "Recent servers": "Shows the last 10 servers you connected to (includes attempts)", "Connect by IP": "Connect to a server by IP", "Connect by ID": "Connect to a server by Battlemetrics ID", + "Scan LAN servers": "Search for servers on your local network", "Add server by IP": "Add a server by IP", "Add server by ID": "Add a server by Battlemetrics ID", "Change favorite server": "Update your quick-connect server", @@ -570,10 +573,16 @@ class ButtonBox(Gtk.Box): renderer = Gtk.CellRendererText() column = Gtk.TreeViewColumn(column_title, renderer, text=i) treeview.append_column(column) + self._populate(context) treeview.set_model(row_store) treeview.grab_focus() - def _populate(self, array_context): + def _populate(self, context): + match context: + case 'Manage': array_context = manage + case 'Main menu': array_context = connect + case 'Options': array_context = options + case 'Help': array_context = help row_store.clear() status = array_context[0][0] treeview = self.get_treeview() @@ -606,11 +615,7 @@ class ButtonBox(Gtk.Box): for col in cols: col.set_title(context) - match context: - case 'Manage': self._populate(manage) - case 'Main menu': self._populate(connect) - case 'Options': self._populate(options) - case 'Help': self._populate(help) + self._populate(context) toggle_signal(treeview, treeview.selected_row, '_on_tree_selection_changed', True) @@ -1069,6 +1074,17 @@ class TreeView(Gtk.TreeView): toggle_signal(self.get_outer_grid().right_panel.filters_vbox, check, '_on_check_toggle', True) toggle_signal(self, self, '_on_keypress', True) + if mode == "Scan LAN servers": + lan_dialog = LanButtonDialog(self.get_outer_window()) + port = lan_dialog.get_selected_port() + if port is None: + grid = self.get_outer_grid() + right_panel = grid.right_panel + vbox = right_panel.button_vbox + vbox._update_single_column("Main menu") + return + mode = mode + ":" + port + wait_dialog = GenericDialog(transient_parent, "Fetching server metadata", "WAIT") wait_dialog.show_all() thread = threading.Thread(target=self._background, args=(wait_dialog, mode)) @@ -1304,6 +1320,10 @@ class AppHeaderBar(Gtk.HeaderBar): self.set_decoration_layout("menu:minimize,maximize,close") self.set_show_close_button(True) +class Port(Enum): + # Contains enums for LanButtonDialog ports + DEFAULT = 1 + CUSTOM = 2 class GenericDialog(Gtk.MessageDialog): def __init__(self, parent, text, mode): @@ -1361,6 +1381,140 @@ class GenericDialog(Gtk.MessageDialog): self.format_secondary_text(text) +class LanButtonDialog(Gtk.Window): + def __init__(self, parent): + super().__init__() + + self.buttonBox = Gtk.Box() + + header_label = "Scan LAN servers" + buttons = [ + ( "Use default query port (27016)", Port.DEFAULT ), + ( "Enter custom query port", Port.CUSTOM ), + ] + + self.buttonBox.set_orientation(Gtk.Orientation.VERTICAL) + self.buttonBox.active_button = None + + for i in enumerate(buttons): + + string = i[1][0] + enum = i[1][1] + + button = Gtk.RadioButton(label=string) + button.port = enum + button.connect("toggled", self._on_button_toggled) + + if i[0] == 0: + self.buttonBox.active_button = button + else: + button.join_group(self.buttonBox.active_button) + + self.buttonBox.add(button) + + self.entry = Gtk.Entry() + self.buttonBox.add(self.entry) + self.entry.set_no_show_all(True) + + self.label = Gtk.Label() + self.label.set_text("Invalid port") + self.label.set_no_show_all(True) + self.buttonBox.add(self.label) + + self.dialog = LanDialog(parent, header_label, self.buttonBox, self.entry, self.label) + self.dialog.run() + self.dialog.destroy() + + def get_selected_port(self): + return self.dialog.p + + def _on_button_toggled(self, button): + if button.get_active(): + self.buttonBox.active_button = button + + match button.port: + case Port.DEFAULT: + self.entry.set_visible(False) + case Port.CUSTOM: + self.entry.set_visible(True) + self.entry.grab_focus() + + def get_active_button(): + return self.buttonBox.active_button + +class LanDialog(Gtk.MessageDialog): + # Custom dialog class that performs integer validation and blocks input if invalid port + # Returns None if user cancels the dialog + def __init__(self, parent, text, child, entry, label): + super().__init__(transient_for=parent, + flags=0, + message_type=Gtk.MessageType.INFO, + buttons=Gtk.ButtonsType.OK_CANCEL, + text=text, + secondary_text="Select the query port", + title=app_name, + modal=True, + ) + + self.outer = self.get_content_area() + self.outer.pack_start(child, False, False, 0) + self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) + self.set_size_request(500, 0) + self.outer.set_margin_start(30) + self.outer.set_margin_end(30) + self.outer.show_all() + + self.connect("response", self._on_dialog_response, child, entry) + self.connect("key-press-event", self._on_keypress, entry) + self.connect("key-release-event", self._on_key_release, entry, label) + + self.child = child + + self.p = None + + def _on_key_release(self, dialog, event, entry, label): + label.set_visible(False) + if entry.is_visible() == False or entry.get_text() == "": + return + if self._is_invalid(entry.get_text()): + label.set_visible(True) + else: + label.set_visible(False) + + def _on_keypress(self, a, event, entry): + if event.keyval == Gdk.KEY_Return: + self.response(Gtk.ResponseType.OK) + if event.keyval == Gdk.KEY_Up: + entry.set_text("") + self.child.get_children()[0].grab_focus() + + def _on_dialog_response(self, dialog, resp, child, entry): + match resp: + case Gtk.ResponseType.CANCEL: + return + case Gtk.ResponseType.DELETE_EVENT: + return + + string = entry.get_text() + port = child.active_button.port + + match port: + case Port.DEFAULT: + self.p = "27016" + case Port.CUSTOM: + if self._is_invalid(string): + self.stop_emission_by_name("response") + else: + self.p = string + + def _is_invalid(self, string): + if string.isdigit() == False \ + or int(string) == 0 \ + or int(string[0]) == 0 \ + or int(string) > 65535: + return True + return False + def ChangelogDialog(parent, text, mode): dialog = GenericDialog(parent, text, mode)