mirror of
https://github.com/aclist/dztui.git
synced 2026-08-26 09:47:36 +02:00
commit
12b82cd1dd
39
CHANGELOG.md
39
CHANGELOG.md
@ -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
|
||||
|
||||
16
dzgui.sh
16
dzgui.sh
@ -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)
|
||||
|
||||
@ -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
334
helpers/servers.py
Normal 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)
|
||||
5292
helpers/ui.py
5292
helpers/ui.py
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user