feat: ping column

This commit is contained in:
aclist committed 2025-07-29 03:01:37 +09:00
1 parent 99e13be3e8
commit b4ae6a9e8b
4 files changed
+157 -24

No files matched your search

+9 -8
View File
@@ -1,19 +1,20 @@
# Changelog # Changelog
## [6.0.0-beta.1] 2025-07-25 ## [6.0.0-beta.1] 2025-07-28
## Added ## Added
- Additional validation on entry dialogs to prevent submitting empty text - Ping column to server browsing contexts
- Added "Return to main menu" button to dialog windows when failing to load server table
- Speed up load time of server tables
- More robust threading and cache system when filtering servers
- Filter by modded servers - Filter by modded servers
- Refresh server ping context menu and hotkey
- Disable "show mods" context menu if server has no mods
- Breadcrumbs showing current menu context to top of window - Breadcrumbs showing current menu context to top of window
- 0, minus, and backslash keybindings for filter toggles - Speed up load time and navigation of server tables
- Dedicated changelog page - Dedicated changelog page
- Dedicated keybindings page - Dedicated keybindings page
- Vim-style navigation keybindings - Vim-style navigation keybindings
- Additional validation on entry dialogs to prevent submitting empty text
- Added "Return to main menu" button to dialog windows when failing to load server table
- More robust threading and cache system when filtering servers
- Refresh server ping: context menu and hotkey
- Disable "show mods" context menu if server has no mods
- 0, minus, and backslash keybindings for filter toggles
## Fixed ## Fixed
- Entry dialog sensitivity when validating API keys - Entry dialog sensitivity when validating API keys
+2 -2
View File
@@ -585,11 +585,11 @@ fetch_helpers_by_sum(){
[[ -f "$config_file" ]] && source "$config_file" [[ -f "$config_file" ]] && source "$config_file"
declare -A sums declare -A sums
sums=( sums=(
["ui.py"]="c05cb7eaf40347b49cdd71030ca9cc39" ["ui.py"]="168865d5a0e7936e0f72da59dde6b6f4"
["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067"
["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397"
["funcs"]="5d244114c501754a630988354306e470" ["funcs"]="5d244114c501754a630988354306e470"
["servers.py"]="e5eae96d11cb1e551aaae74a469c4f5f" ["servers.py"]="dc23a9f15e5014be171054792cf3fc16"
) )
local author="aclist" local author="aclist"
local repo="dztui" local repo="dztui"
+50 -2
View File
@@ -1,4 +1,5 @@
import json import json
import math
import os import os
import re import re
import socket import socket
@@ -123,6 +124,11 @@ def parse_json(json: list) -> list:
except IndexError: except IndexError:
continue continue
try:
ping = row["ping"]
except KeyError:
ping = 9999
mapname = row["map"].lower() mapname = row["map"].lower()
players = row["players"] players = row["players"]
max_players = row["max_players"] max_players = row["max_players"]
@@ -136,6 +142,7 @@ def parse_json(json: list) -> list:
int(queue), int(queue),
ip, ip,
int(qport), int(qport),
ping,
provider, provider,
modded, modded,
] ]
@@ -143,9 +150,9 @@ def parse_json(json: list) -> list:
return rows return rows
def query_direct(ip: str, qport: int) -> dict | None: def query_direct(ip: str, qport: int, TIMEOUT=3.0) -> dict | None:
try: try:
info = a2s.info((ip, qport)) info = a2s.info((ip, qport), TIMEOUT)
name = info.server_name name = info.server_name
mapname = info.map_name mapname = info.map_name
@@ -155,6 +162,12 @@ def query_direct(ip: str, qport: int) -> dict | None:
max_players = info.max_players max_players = info.max_players
keywords = info.keywords keywords = info.keywords
try:
ping = info.ping
ping = math.floor(info.ping * 1000)
except AttributeError:
ping = 9999
res = {} res = {}
res["name"] = name res["name"] = name
res["map"] = mapname res["map"] = mapname
@@ -163,6 +176,7 @@ def query_direct(ip: str, qport: int) -> dict | None:
res["max_players"] = max_players res["max_players"] = max_players
res["addr"] = address res["addr"] = address
res["gameport"] = gameport res["gameport"] = gameport
res["ping"] = ping
return res return res
except TimeoutError: except TimeoutError:
return None return None
@@ -177,6 +191,40 @@ class Res:
json: Union[str, None] json: Union[str, None]
@dataclass
class Ping:
addr: str
iteration: int
ping: int
def ping(iteration: int, row: list) -> Ping:
addr = row[7]
qport = row[8]
res = None
if row[9] != 9999:
return Ping(addr, iteration, row[9])
try:
ip = addr.split(":")[0]
except IndexError:
ping = 9999
try:
res = query_direct(ip, qport, 0.5)
except Exception as e:
pass
if res is None:
ping = 9999
else:
ping = res["ping"]
return Ping(addr, iteration, ping)
def query_api(key: str, param: str) -> Res: def query_api(key: str, param: str) -> Res:
LIMIT = 10000 LIMIT = 10000
url = "https://api.steampowered.com/IGameServersService/GetServerList/v1/?" url = "https://api.steampowered.com/IGameServersService/GetServerList/v1/?"
+96 -12
View File
@@ -16,11 +16,12 @@ import warnings
from enum import Enum from enum import Enum
from concurrent.futures import wait from concurrent.futures import wait
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from collections.abc import Callable from collections.abc import Callable
from typing import Literal, Self, Any from typing import Literal, Self, Any
sys.path.append("servers") sys.path.append("servers")
import servers as Servers import servers as Servers # noqa E402
locale.setlocale(locale.LC_ALL, "") locale.setlocale(locale.LC_ALL, "")
@@ -1013,6 +1014,7 @@ class OuterWindow(Gtk.Window):
self.show_all() self.show_all()
self.grid.right_panel.filters_vbox.set_visible(False) self.grid.right_panel.filters_vbox.set_visible(False)
self.grid.right_panel.enable_ping_button(False)
self.grid.sel_panel.set_visible(False) self.grid.sel_panel.set_visible(False)
# convenience to avoid deep calls # convenience to avoid deep calls
@@ -1085,16 +1087,30 @@ class RightPanel(Gtk.Box):
self.pack_start(self.button_vbox, False, False, 0) self.pack_start(self.button_vbox, False, False, 0)
self.pack_start(self.filters_vbox, False, False, 0) self.pack_start(self.filters_vbox, False, False, 0)
tooltip = ( debug_tooltip = (
"Used to perform a dry run without\n" "Used to perform a dry run without\n"
"actually connecting to a server" "actually connecting to a server"
) )
ping_tooltip = (
"Refresh the ping for visible servers.\n"
"Available once per unique filter context"
)
self.ping = Gtk.Button(
label="Ping servers",
margin_top=10,
margin_start=80,
margin_end=80,
tooltip_text=ping_tooltip,
)
self.ping.connect("clicked", self._on_ping_clicked)
self.debug_toggle = Gtk.ToggleButton( self.debug_toggle = Gtk.ToggleButton(
label="Debug mode", label="Debug mode",
margin_top=10, margin_top=10,
margin_start=80, margin_start=80,
margin_end=80, margin_end=80,
tooltip_text=tooltip, tooltip_text=debug_tooltip,
) )
if query_config("debug")[0] == "1": if query_config("debug")[0] == "1":
@@ -1110,9 +1126,13 @@ class RightPanel(Gtk.Box):
) )
self.question.connect("clicked", self._on_question_clicked) self.question.connect("clicked", self._on_question_clicked)
self.pack_start(self.ping, False, True, 0)
self.pack_start(self.debug_toggle, False, True, 0) self.pack_start(self.debug_toggle, False, True, 0)
self.pack_start(self.question, False, True, 0) self.pack_start(self.question, False, True, 0)
def enable_ping_button(self, state: bool) -> None:
self.ping.set_visible(state)
def reinit_maps(self, rows: list) -> None: def reinit_maps(self, rows: list) -> None:
map_store.clear() map_store.clear()
map_store.append(["All maps"]) map_store.append(["All maps"])
@@ -1129,6 +1149,37 @@ class RightPanel(Gtk.Box):
grid.statusbar.refresh() grid.statusbar.refresh()
App.grid.notebook.focus_current() App.grid.notebook.focus_current()
def _on_ping_clicked(self, button: Gtk.Button) -> None:
block_signals()
def _update_pings():
rows = ModelManager.get_filtered()
with ThreadPoolExecutor(100) as executor:
futures = [
executor.submit(Servers.ping, i, row)
for i, row in enumerate(rows)
]
wait(futures)
for future in futures:
res = future.result()
path = Gtk.TreePath.new_from_indices([res.iteration])
temp_model[path][9] = res.ping
ModelManager.ping_cache[res.addr] = res.ping
App.treeview.set_model(temp_model)
App.treeview.wait_dialog.destroy()
App.treeview.enable_ping_column(True)
App.treeview.grab_focus()
App.right_panel.ping.set_sensitive(False)
unblock_signals()
temp_model = App.treeview.get_model()
App.treeview.set_model(None)
App.treeview.wait_dialog = GenericDialog("Pinging servers", Popup.WAIT)
App.treeview.wait_dialog.show_all()
thread = threading.Thread(target=_update_pings, args=())
thread.start()
def _on_question_clicked(self, button: Gtk.Button) -> None: def _on_question_clicked(self, button: Gtk.Button) -> None:
App.grid.notebook.toggle_keybindings() App.grid.notebook.toggle_keybindings()
@@ -1250,6 +1301,7 @@ class ModelManagerSingleton:
def __init__(self): def __init__(self):
# packed ListStores # packed ListStores
self.filter_cache = {} self.filter_cache = {}
self.ping_cache = {}
# stringwise (list) representation of the model # stringwise (list) representation of the model
self.control_model = None self.control_model = None
self.filtered = None self.filtered = None
@@ -1285,9 +1337,11 @@ class ModelManagerSingleton:
if prior_map == "All maps": if prior_map == "All maps":
rows = self.filter_map(filters) rows = self.filter_map(filters)
else: else:
App.right_panel.ping.set_sensitive(True)
rows = self.filter_toggle_on(filters, *args) rows = self.filter_toggle_on(filters, *args)
case FilterMode.KEYWORD: case FilterMode.KEYWORD:
App.right_panel.ping.set_sensitive(True)
rows = self.filter_toggle_on(filters, *args) rows = self.filter_toggle_on(filters, *args)
case FilterMode.TOGGLE_OFF: case FilterMode.TOGGLE_OFF:
@@ -1296,8 +1350,14 @@ class ModelManagerSingleton:
rows = self.filtered rows = self.filtered
case FilterMode.TOGGLE_ON: case FilterMode.TOGGLE_ON:
App.right_panel.ping.set_sensitive(True)
rows = self.filter_toggle_on(filters, *args) rows = self.filter_toggle_on(filters, *args)
if mode is not FilterMode.INITIAL:
for row in rows:
if row[7] in self.ping_cache:
row[9] = self.ping_cache[row[7]]
if len(rows) > 0: if len(rows) > 0:
clone = ModelManager.new_model() clone = ModelManager.new_model()
rows = self.sort_rows(rows) rows = self.sort_rows(rows)
@@ -1370,9 +1430,9 @@ class ModelManagerSingleton:
case "1PP": case "1PP":
rows = [row for row in rows if row[2] != "1PP"] rows = [row for row in rows if row[2] != "1PP"]
case "Official": case "Official":
rows = [row for row in rows if row[9] != "Official"] rows = [row for row in rows if row[10] != "Official"]
case "Unoffic.": case "Unoffic.":
rows = [row for row in rows if row[9] != "Unoffic."] rows = [row for row in rows if row[10] != "Unoffic."]
case "Empty": case "Empty":
rows = [row for row in rows if row[4] != 0] rows = [row for row in rows if row[4] != 0]
case "Full": case "Full":
@@ -1397,7 +1457,7 @@ class ModelManagerSingleton:
case "Low pop": case "Low pop":
rows = [row for row in rows if (row[4] / row[5] * 100) > 30] rows = [row for row in rows if (row[4] / row[5] * 100) > 30]
case "Modded": case "Modded":
rows = [row for row in rows if not row[10]] rows = [row for row in rows if not row[11]]
return rows return rows
def filter_toggle_on(self, filters: tuple, *args: str) -> list: def filter_toggle_on(self, filters: tuple, *args: str) -> list:
@@ -1417,7 +1477,7 @@ class ModelManagerSingleton:
def new_model(self) -> Gtk.ListStore: def new_model(self) -> Gtk.ListStore:
return Gtk.ListStore( return Gtk.ListStore(
str, str, str, str, int, int, int, str, int, str, bool str, str, str, str, int, int, int, str, int, int, str, bool
) )
def resync_model(self, addr: str, qport: int) -> None: def resync_model(self, addr: str, qport: int) -> None:
@@ -1433,6 +1493,8 @@ class ModelManagerSingleton:
filters = App.right_panel.filters_vbox.get_filters() filters = App.right_panel.filters_vbox.get_filters()
refiltered = self.filter_toggle_on(filters) refiltered = self.filter_toggle_on(filters)
self.set_filtered(refiltered) self.set_filtered(refiltered)
self.set_success(True)
GLib.idle_add(App.treeview._filter_cleanup)
def convert_model_to_list(self, model: Gtk.ListStore) -> list: def convert_model_to_list(self, model: Gtk.ListStore) -> list:
return [[el for el in row] for row in model] return [[el for el in row] for row in model]
@@ -1442,6 +1504,9 @@ class ModelManagerSingleton:
rows = [] rows = []
self.filtered = rows self.filtered = rows
def get_filtered(self) -> list:
return self.filtered
def set_store(self, model: Gtk.ListStore | None) -> None: def set_store(self, model: Gtk.ListStore | None) -> None:
self.store = model self.store = model
@@ -1461,6 +1526,7 @@ class ModelManagerSingleton:
self.success = True self.success = True
self.filtered = None self.filtered = None
self.filter_cache = {} self.filter_cache = {}
self.ping_cache = {}
if full: if full:
self.control_model = None self.control_model = None
@@ -1610,9 +1676,12 @@ class TreeView(Gtk.TreeView):
addr = model.get_value(it, 7) addr = model.get_value(it, 7)
qport = model.get_value(it, 8) qport = model.get_value(it, 8)
model.remove(it) model.remove(it)
print(addr)
ModelManager.resync_model(addr, qport) block_signals()
thread = threading.Thread(
target=ModelManager.resync_model, args=(addr, qport)
)
thread.start()
def remove_from_history(self) -> None: def remove_from_history(self) -> None:
record = self.get_record_string() record = self.get_record_string()
@@ -1660,7 +1729,7 @@ class TreeView(Gtk.TreeView):
(model, pathlist) = sels (model, pathlist) = sels
path = pathlist[0] path = pathlist[0]
tree_iter = model.get_iter(path) tree_iter = model.get_iter(path)
mods = model.get_value(tree_iter, 10) mods = model.get_value(tree_iter, 11)
return mods return mods
def is_in_favs(self) -> bool: def is_in_favs(self) -> bool:
@@ -1973,7 +2042,7 @@ class TreeView(Gtk.TreeView):
path = pathlist[0] path = pathlist[0]
model = self.get_model() model = self.get_model()
if not model: if not model:
return return None
addr = model[path][7] addr = model[path][7]
qport = model[path][8] qport = model[path][8]
ip = addr.split(":")[0] ip = addr.split(":")[0]
@@ -1994,6 +2063,12 @@ class TreeView(Gtk.TreeView):
path = self.get_mpath() path = self.get_mpath()
model[path][6] = players model[path][6] = players
def enable_ping_column(self, state: bool) -> None:
columns = self.get_columns()
for column in columns:
if column.get_title() == "Ping":
column.set_visible(state)
def select_first_row(self): def select_first_row(self):
sel = self.get_selection() sel = self.get_selection()
self._on_tree_selection_changed(sel) self._on_tree_selection_changed(sel)
@@ -2102,8 +2177,11 @@ class TreeView(Gtk.TreeView):
case RowType.SCAN_LAN: case RowType.SCAN_LAN:
parsed = self._dump_lan(port) parsed = self._dump_lan(port)
case RowType.SERVER_BROWSER: case RowType.SERVER_BROWSER:
App.treeview.enable_ping_column(False)
App.right_panel.enable_ping_button(True)
parsed = self._dump_api() parsed = self._dump_api()
case RowType.SAVED_SERVERS: case RowType.SAVED_SERVERS:
App.treeview.enable_ping_column(True)
favs = query_favorites() favs = query_favorites()
if not favs: if not favs:
ModelManager.set_success(False) ModelManager.set_success(False)
@@ -2112,6 +2190,7 @@ class TreeView(Gtk.TreeView):
return return
parsed = self._dump_servers(favs) parsed = self._dump_servers(favs)
case RowType.RECENT_SERVERS: case RowType.RECENT_SERVERS:
App.treeview.enable_ping_column(True)
history = query_history() history = query_history()
if not history: if not history:
ModelManager.set_success(False) ModelManager.set_success(False)
@@ -2309,6 +2388,7 @@ class TreeView(Gtk.TreeView):
"Queue", "Queue",
"IP", "IP",
"Qport", "Qport",
"Ping",
] ]
for i, column_title in enumerate(browser_cols): for i, column_title in enumerate(browser_cols):
renderer = Gtk.CellRendererText() renderer = Gtk.CellRendererText()
@@ -2318,7 +2398,10 @@ class TreeView(Gtk.TreeView):
column.set_sort_column_id(i) column.set_sort_column_id(i)
if valid_json: if valid_json:
saved_size = data["cols"][column_title] try:
saved_size = data["cols"][column_title]
except KeyError:
saved_size = 100
column.set_fixed_width(saved_size) column.set_fixed_width(saved_size)
column.set_expand(True) column.set_expand(True)
else: else:
@@ -2428,6 +2511,7 @@ class TreeView(Gtk.TreeView):
model.clear() model.clear()
ModelManager.wipe_cache(full=True) ModelManager.wipe_cache(full=True)
App.right_panel.enable_ping_button(False)
App.right_panel.filters_vbox.reinit_panel() App.right_panel.filters_vbox.reinit_panel()
self.set_selection_mode(Gtk.SelectionMode.SINGLE) self.set_selection_mode(Gtk.SelectionMode.SINGLE)