Merge pull request #224 from aclist/prerelease/6.0.0

Prerelease/6.0.0
This commit is contained in:
aclist 2025-07-30 02:11:53 +09:00 committed by GitHub
commit 12b82cd1dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 3689 additions and 2024 deletions

View File

@ -1,5 +1,44 @@
# Changelog
## [6.0.0-beta.1] 2025-07-29
## Added
- Ping column to server browsing contexts
- Server details dialog to server browsing contexts
- Filter by modded servers
- Breadcrumbs showing current menu context to top of window
- Speed up load time and navigation of server tables
- More robust threading and cache system when filtering servers
- Dedicated changelog page
- Dedicated keybindings page
- 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
- Dynamic context menus for modded servers
- Additional keybindings for filter toggles
## Fixed
- Entry dialog sensitivity when validating API keys
- Normalized buttons in dialogs and restored proper padding
- GTK errors being emitted to stdout when inserting debug table
- Suppress errors during pre-boot checks when mods are not installed
- Centered filter checkboxes within panel
- Do not pop unhighlight/select stale buttons if no stale mods exist
- Improved keybinding interaction with side panels
- Key stickiness when quickly navigating through entries in tables
- Suppress typeahead search in mod dialogs
- Do not trigger global API cooldown if no LAN/favorite servers are found
- Fix table column expansion in server mod dialogs
## Changed
- Reduced global API cooldown from 60s to 30s
- Clarify dialog messages when DayZ path could not be found
- Auto-focus first item when opening context menus
- Opacity setting on side buttons when in a different context
- Refactored BM API key validation to account for new key format
## Dropped
- Ping readout in statusbar
## [5.8.0-beta.2] 2025-07-06
## Added
- Filter servers by official/unofficial status

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -o pipefail
version=5.8.0-beta.2
version=6.0.0.beta-1
#CONSTANTS
aid=221100
@ -542,13 +542,13 @@ fetch_a2s(){
logger INFO "Updated A2S helper to sha '$sha'"
}
fetch_dzq(){
local sum="9caed1445c45832f4af87736ba3f9637"
local sum="0a334e1e144e76e560419d155435c91e"
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=3088bbfb147b77bc7b6a9425581b439889ff3f7f
local sha=a22a9f428cbe075d7dda62f78000296955eea92a
local author="yepoleb"
local repo="dayzquery"
local url="https://raw.githubusercontent.com/$author/$repo/$sha/dayzquery.py"
@ -585,11 +585,11 @@ fetch_helpers_by_sum(){
[[ -f "$config_file" ]] && source "$config_file"
declare -A sums
sums=(
["ui.py"]="f128a97e744e9e11036d707198feb8a8"
["ui.py"]="d6db191dca9e50bec32458a04eaac3a6"
["query_v2.py"]="55d339ba02512ac69de288eb3be41067"
["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397"
["funcs"]="94287c75a5ae0a06a95dd439711e6fba"
["lan"]="c62e84ddd1457b71a85ad21da662b9af"
["funcs"]="1cef1a9fab4b6428fdba5572ad002614"
["servers.py"]="51839211af7f65bdc04bab5557bf9466"
)
local author="aclist"
local repo="dztui"
@ -631,7 +631,6 @@ fetch_helpers_by_sum(){
logger INFO "Updated '$full_path' to sum '$sum'"
fi
[[ $file == "funcs" ]] && chmod +x "$full_path"
[[ $file == "lan" ]] && chmod +x "$full_path"
done
return 0
}
@ -926,7 +925,7 @@ create_config(){
}
varcheck(){
local msg="Config file '$config_file' missing. Start first-time setup now?"
local msg2="The Steam paths set in your config file appear to be invalid. Restart first-time setup now?"
local msg2="The Steam paths set in your config file appear to be invalid (DayZ was moved or uninstalled). Restart first-time setup now?"
if [[ ! -f $config_file ]]; then
qdialog "$msg" "Yes" "Exit"
if [[ $? -eq 1 ]]; then
@ -1006,6 +1005,7 @@ legacy_cols(){
mv $cols_file.new $cols_file
}
stale_mod_signatures(){
[[ ! -f "$versions_file" ]] && return
local workshop_dir="$steam_path/steamapps/workshop/content/$aid"
if [[ -d $workshop_dir ]]; then
readarray -t old_mod_ids < <(awk -F, '{print $1}' $versions_file)

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash
set -o pipefail
version="5.8.0"
version="6.0.0-beta.1"
#CONSTANTS
aid=221100
@ -114,6 +114,7 @@ declare -A funcs=(
["get_dist"]="get_dist"
["test_cooldown"]="test_cooldown"
["query_config"]="query_config"
["query_favorites"]="query_favorites"
["start_cooldown"]="start_cooldown"
["List installed mods"]="list_mods"
["Delete selected mods"]="delete_local_mod"
@ -146,6 +147,12 @@ lan_scan(){
printf "%s\n" "$res"
fi
}
query_favorites(){
if [[ -z "${ip_list[@]}" ]]; then
return 1
fi
printf "%s\n" "${ip_list[@]}"
}
find_stale_mods(){
local res
local mods=()
@ -723,8 +730,8 @@ test_cooldown(){
local old_time=$(< $_cache_cooldown)
local cur_time=$(date +%s)
local delta=$(($cur_time - $old_time))
if [[ $delta -lt 60 ]]; then
local remains=$((60 - $delta))
if [[ $delta -lt 30 ]]; then
local remains=$((30 - $delta))
local suffix=$(pluralize "seconds" $remains)
printf "Global API cooldown in effect. Please wait %s %s." "$remains" "$suffix"
exit 1
@ -752,20 +759,20 @@ dump_servers(){
case "$subcontext" in
*Server[[:space:]]browser*)
local file="$_cache_servers"
if [[ ! $subcontext =~ Name ]]; then
if [[ ! $context =~ filter ]]; then
initialize_remote_servers
fi
;;
*My[[:space:]]saved[[:space:]]servers*)
local file="$_cache_my_servers"
if [[ ! $subcontext =~ Name ]]; then
if [[ ! $context =~ filter ]]; then
[[ -f $file ]] && rm $file
_iterate "$file" "${ip_list[@]}"
fi
;;
*Recent[[:space:]]servers*)
local file="$_cache_history"
if [[ ! $subcontext =~ Name ]]; then
if [[ ! $context =~ filter ]]; then
[[ -f $file ]] && rm $file
readarray -t iters < <(cat $history_file)
_iterate "$file" "${iters[@]}"
@ -774,7 +781,7 @@ dump_servers(){
*Scan[[:space:]]LAN[[:space:]]servers*)
local port=$(<<< "$subcontext" awk -F: '{print $2}')
local file="$_cache_lan"
if [[ ! $subcontext =~ Name ]]; then
if [[ ! $context =~ filter ]]; then
[[ -f $file ]] && rm $file
local lan=$(lan_scan $port)
readarray -t iters <<< "$lan"
@ -1095,10 +1102,10 @@ test_steam_api(){
}
test_bm_api(){
local key="$1"
if [[ ! $key =~ ^[0-9]+$ ]]; then
if [[ -z $key ]]; then
echo 1
return
fi
[[ -z $key ]] && return 1
local code=$(curl -ILs "$bm_api" \
-H "Authorization: Bearer "$key"" -G \
-d "filter[game]=$game" \
@ -1112,6 +1119,10 @@ update_config_val(){
case $1 in
"Change player name")
key="name"
if [[ -z "${value// }" ]]; then
printf "Invalid name"
return 2
fi
;;
"Change Steam API key")
key="steam_api"
@ -1348,6 +1359,9 @@ update_history(){
echo "$record" >> "$history_file"
}
update_symlinks(){
if [[ ! -d "$workshop_dir" ]]; then
return
fi
legacy_symlinks
symlinks
}

334
helpers/servers.py Normal file
View File

@ -0,0 +1,334 @@
import json
import math
import os
import re
import socket
import subprocess
import sys
import typing # noqa
from dataclasses import dataclass
from urllib import request, parse
from urllib.error import HTTPError
from typing import Union
sys.path.append("a2s")
import a2s # noqa
from a2s import dayzquery
params = [
r"\nor\1\map\chernarusplus\nor\1\map\sakhal\nor\1\map\enoch\empty\1\nor\1\map\namalsk",
r"\map\namalsk\empty\1",
r"\map\namalsk\noplayers\1",
r"\map\chernarusplus\empty\1",
r"\map\chernarusplus\noplayers\1",
r"\map\sakhal\empty\1",
r"\map\sakhal\noplayers\1",
r"\map\enoch\empty\1",
r"\map\enoch\noplayers\1",
]
def get_netmask() -> str:
hostname = os.uname()[1]
i = socket.gethostbyname(hostname)
netmask = i.rsplit(".", 1)[0]
return netmask
def test_ip(suffix: int, port: int) -> dict | None:
netmask = get_netmask()
hostname = f"{netmask}.{str(suffix)}"
ping = ["ping", "-c1", "-i", "0.1", "-w", "1"]
output = subprocess.run(ping + [hostname], capture_output=True)
if output.returncode == 0:
return query_direct(hostname, port)
return None
def sanitize(name: str) -> str:
name = re.sub(r"\r", r"", name)
name = re.sub(r"\n", r"", name)
name = re.sub(r"\x01", r"", name)
name = re.sub(r"\ufeff", r"", name)
name = re.sub(r"(^!\s*)", r"-", name)
name = re.sub(r"(^-\s*)", r"", name)
name = re.sub(r"(^-)", r"", name)
name = re.sub(r"(^\s*)", r"", name)
name = re.sub(r"\t", "", name)
return name
def parse_json(json: list) -> list:
"""
Server metadata is underspecified and server operators
tend to insert random garbage in the headers. In case
sanitization failed, discard malformed rows rather than
aborting outright.
"""
rows = []
for row in json:
try:
name = sanitize(row["name"])
if name == "":
continue
except KeyError:
continue
for key in [
"map",
"gametype",
"players",
"max_players",
"addr",
"gameport",
]:
try:
row[key]
except KeyError:
continue
r = row["gametype"].split(",")
if "no3rd" in r:
view = "1PP"
else:
view = "3PP"
if "external" in r:
provider = "Unoffic."
else:
provider = "Official"
if "mod" in r:
modded = True
else:
modded = False
try:
r = row["gametype"].split("lqs")
queue = r[1].split(",")[0]
except IndexError:
queue = 0
try:
test_time = re.search(r"[0-9]{2}:[0-9]{2}", row["gametype"])
if test_time:
time = test_time.group(0)
else:
time = "Unknown"
except AttributeError:
time = "Unknown"
try:
ip = row["addr"].split(":")[0] + ":" + str(row["gameport"])
qport = row["addr"].split(":")[1]
except IndexError:
continue
try:
ping = row["ping"]
except KeyError:
ping = 9999
mapname = row["map"].lower()
players = row["players"]
max_players = row["max_players"]
raw = [
name,
mapname,
view,
time,
int(players),
int(max_players),
int(queue),
ip,
int(qport),
ping,
provider,
modded,
]
rows.append(raw)
return rows
def query_direct(ip: str, qport: int, TIMEOUT=3.0) -> dict | None:
try:
info = a2s.info((ip, qport), TIMEOUT)
name = info.server_name
mapname = info.map_name
address = ip + ":" + str(qport)
gameport = str(info.port)
players = info.player_count
max_players = info.max_players
keywords = info.keywords
try:
ping = info.ping
ping = math.floor(info.ping * 1000)
except AttributeError:
ping = 9999
res = {}
res["name"] = name
res["map"] = mapname
res["gametype"] = keywords
res["players"] = players
res["max_players"] = max_players
res["addr"] = address
res["gameport"] = gameport
res["ping"] = ping
return res
except TimeoutError:
return None
except KeyError:
return None
@dataclass
class Res:
status: int
parsed: bool
json: Union[str, None]
@dataclass
class Ping:
addr: str
iteration: int
ping: int
@dataclass
class Details:
data: Union[list, None]
description: str
success: bool
def details(ip: str, qport: int) -> Details:
default_str = "None provided"
try:
info = a2s.info((ip, qport))
except TimeoutError:
return Details(None, default_str, False)
try:
rules = dayzquery.dayz_rules((ip, int(qport)))
except TimeoutError:
return Details(None, default_str, False)
try:
keywords = info.keywords.split(",")
except AttributeError:
return Details(None, default_str, False)
battleeye = "Disabled"
if "battleye" in keywords:
battleye = "Enabled"
day_accel = 0.0
night_accel = 0.0
for keyword in keywords:
if "etm" in keyword:
day_accel = float(keyword.lstrip("etm"))
day_accel = f"{day_accel:g}"
if "entm" in keywords:
night_accel = float(keyword.lstrip("entm"))
night_accel = f"{night_accel:g}"
try:
dlc = rules.dlc_flags
if dlc == 0:
dlc = "None"
if dlc == 2:
dlc = "Frostline"
except AttributeError:
dlc = "not specified"
try:
platform = rules.platform
if platform == "win":
platform = "Windows"
if platform == "?":
platform = "Linux"
except AttributeError:
platform = "not specified"
try:
description = rules.description.strip()
if description == "":
description = default_str
except AttributeError:
description = default_str
rows = [
["DLC", dlc],
["Battleye", battleye],
["Daytime acceleration", f"{day_accel}x"],
["Night-time acceleration", f"{night_accel}x"],
["Platform", platform],
]
return Details(rows, description, True)
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:
LIMIT = 10000
url = "https://api.steampowered.com/IGameServersService/GetServerList/v1/?"
payload: dict[str, Union[int, str]] = {
"filter": r"\appid\221100" + param,
"limit": LIMIT,
"key": key,
}
par = parse.urlencode(payload)
url = f"{url}{par}"
status = 200
parsed = True
data = None
try:
with request.urlopen(url) as response:
if response.status != 200:
status = response.status
try:
parsed = True
data = json.load(response)
except json.decoder.JSONDecodeError:
parsed = False
data = None
except HTTPError:
status = 403
parsed = False
data = None
finally:
return Res(status, parsed, data)

File diff suppressed because it is too large Load Diff