mirror of
https://github.com/aclist/dztui.git
synced 2026-08-31 12:16:55 +02:00
Compare commits
32 Commits
d56bc5a57d
...
998b15d5e8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
998b15d5e8 | ||
|
|
9d8bbec03a | ||
|
|
6425f3d8e3 | ||
|
|
ad64ba75c9 | ||
|
|
47f7622606 | ||
|
|
81c2d19a09 | ||
|
|
b500806815 | ||
|
|
60ce184d8f | ||
|
|
2edb81247b | ||
|
|
7b67222885 | ||
|
|
4ddb33cabb | ||
|
|
364f0b3482 | ||
|
|
5482f296a5 | ||
|
|
0d52c83ed9 | ||
|
|
b92e337016 | ||
|
|
ee73c2cfa3 | ||
|
|
8ffb1c7ea6 | ||
|
|
9040977447 | ||
|
|
da6cc9c5c3 | ||
|
|
c40d767dcc | ||
|
|
6f3b272bfe | ||
|
|
ffe3dac3a8 | ||
|
|
5fa8498c56 | ||
|
|
c0b5c5bd89 | ||
|
|
6d428ebb0d | ||
|
|
479be05452 | ||
|
|
95a775cd74 | ||
|
|
1bfb054be2 | ||
|
|
6b31bb3ab3 | ||
|
|
c5b13ff0f3 | ||
|
|
5a33591d6a | ||
|
|
62b00507e5 |
16
CHANGELOG.md
16
CHANGELOG.md
@ -1,5 +1,21 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [6.0.0-beta.5] 2025-09-12
|
||||||
|
## Added
|
||||||
|
- Support DayZ Experimental
|
||||||
|
- Show additional client information in Options menu
|
||||||
|
- Warn user of client version mismatches
|
||||||
|
- Support clickable hyperlinks
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
- Mods rarely not appearing in local mod list if download completed too quickly
|
||||||
|
- Statusbar not updating when clicking a row after spamming keyboard input
|
||||||
|
- Extraneous logs being generated when subscribing to mods
|
||||||
|
- Narrow width of columns in modlist dialogs occluding text
|
||||||
|
- Newline terminators in history file
|
||||||
|
- Floating point number calculation
|
||||||
|
- Window resizing too small if no prior resolution was set
|
||||||
|
|
||||||
## [6.0.0-beta.5] 2025-08-20
|
## [6.0.0-beta.5] 2025-08-20
|
||||||
## Fixed
|
## Fixed
|
||||||
- Servers returning malformed A2S_INFO blocking server browser from loading
|
- Servers returning malformed A2S_INFO blocking server browser from loading
|
||||||
|
|||||||
9
dzgui.sh
9
dzgui.sh
@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|
||||||
version=6.0.0.beta-5
|
version=6.0.0.beta-6
|
||||||
|
|
||||||
#CONSTANTS
|
#CONSTANTS
|
||||||
aid=221100
|
aid=221100
|
||||||
@ -585,11 +585,12 @@ fetch_helpers_by_sum(){
|
|||||||
[[ -f "$config_file" ]] && source "$config_file"
|
[[ -f "$config_file" ]] && source "$config_file"
|
||||||
declare -A sums
|
declare -A sums
|
||||||
sums=(
|
sums=(
|
||||||
["funcs"]="01d45663b7517eae866010df0eba746a"
|
["funcs"]="a286cc402bfccd39493fe32c53148a95"
|
||||||
["query_v2.py"]="55d339ba02512ac69de288eb3be41067"
|
["query_v2.py"]="55d339ba02512ac69de288eb3be41067"
|
||||||
["servers.py"]="ea5648df7121bb9dfeead9874bfcafcf"
|
["servers.py"]="7f83d5c1ca54acb12f1bd6657feb2ecf"
|
||||||
["ui.py"]="38b589e4b4fd9a9d3e049e7dcbdc8593"
|
["ui.py"]="cd9f4b3bc9b1922bb10cbc0c579cf2c0"
|
||||||
["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397"
|
["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397"
|
||||||
|
["pefile.py"]="21531f2c0d9dfa5f110cf6779f9d22c0"
|
||||||
)
|
)
|
||||||
local author="aclist"
|
local author="aclist"
|
||||||
local repo="dztui"
|
local repo="dztui"
|
||||||
|
|||||||
491
helpers/funcs
491
helpers/funcs
@ -1,9 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
version="6.0.0-beta.5"
|
version="6.0.0-beta.6"
|
||||||
|
|
||||||
#CONSTANTS
|
#CONSTANTS
|
||||||
aid=221100
|
aid=221100
|
||||||
|
exp=1024020
|
||||||
game="dayz"
|
game="dayz"
|
||||||
app_name="dzgui"
|
app_name="dzgui"
|
||||||
app_name_upper="DZGUI"
|
app_name_upper="DZGUI"
|
||||||
@ -46,6 +47,7 @@ _cache_my_servers="$cache_dir/$prefix.my_servers"
|
|||||||
_cache_history="$cache_dir/$prefix.history"
|
_cache_history="$cache_dir/$prefix.history"
|
||||||
_cache_launch="$cache_dir/$prefix.launch_mods"
|
_cache_launch="$cache_dir/$prefix.launch_mods"
|
||||||
_cache_address="$cache_dir/$prefix.launch_address"
|
_cache_address="$cache_dir/$prefix.launch_address"
|
||||||
|
_cache_binary="$cache_dir/$prefix.binary"
|
||||||
_cache_coords="$cache_path/$prefix.coords"
|
_cache_coords="$cache_path/$prefix.coords"
|
||||||
_cache_cooldown="$cache_path/$prefix.cooldown"
|
_cache_cooldown="$cache_path/$prefix.cooldown"
|
||||||
_cache_lan="$cache_path/$prefix.lan"
|
_cache_lan="$cache_path/$prefix.lan"
|
||||||
@ -82,7 +84,6 @@ forum_url="https://old.reddit.com/r/dzgui"
|
|||||||
sponsor_url="$gh_prefix/sponsors/$author"
|
sponsor_url="$gh_prefix/sponsors/$author"
|
||||||
battlemetrics_server_url="https://www.battlemetrics.com/servers/dayz"
|
battlemetrics_server_url="https://www.battlemetrics.com/servers/dayz"
|
||||||
steam_api_url="https://steamcommunity.com/dev/apikey"
|
steam_api_url="https://steamcommunity.com/dev/apikey"
|
||||||
#TODO: update link in docs
|
|
||||||
battlemetrics_api_url="https://www.battlemetrics.com/developers"
|
battlemetrics_api_url="https://www.battlemetrics.com/developers"
|
||||||
bm_api="https://api.battlemetrics.com/servers"
|
bm_api="https://api.battlemetrics.com/servers"
|
||||||
|
|
||||||
@ -94,23 +95,16 @@ fi
|
|||||||
|
|
||||||
declare -A funcs=(
|
declare -A funcs=(
|
||||||
["Highlight stale"]="find_stale_mods"
|
["Highlight stale"]="find_stale_mods"
|
||||||
["My servers"]="dump_servers"
|
|
||||||
["Change player name"]="update_config_val"
|
["Change player name"]="update_config_val"
|
||||||
["Change Steam API key"]="update_config_val"
|
["Change Steam API key"]="update_config_val"
|
||||||
["Change Battlemetrics API key"]="update_config_val"
|
["Change Battlemetrics API key"]="update_config_val"
|
||||||
["Change favorite server"]="add_record"
|
["Change favorite server"]="add_record"
|
||||||
["Quick-connect to favorite server"]="quick_connect"
|
|
||||||
["Add server by IP"]="add_record"
|
["Add server by IP"]="add_record"
|
||||||
["Add server by ID"]="add_record"
|
["Add server by ID"]="add_record"
|
||||||
["Connect by IP"]="validate_and_connect"
|
["try_connect"]="try_connect"
|
||||||
["Connect by ID"]="validate_and_connect"
|
|
||||||
["Connect from table"]="connect_from_table"
|
|
||||||
["find_id"]="find_id"
|
["find_id"]="find_id"
|
||||||
["toggle"]="toggle"
|
["toggle"]="toggle"
|
||||||
["Open link"]="open_link"
|
["Open link"]="open_link"
|
||||||
["filter"]="dump_servers"
|
|
||||||
["dump_servers"]="dump_servers"
|
|
||||||
["get_unique_maps"]="get_unique_maps"
|
|
||||||
["get_dist"]="get_dist"
|
["get_dist"]="get_dist"
|
||||||
["test_cooldown"]="test_cooldown"
|
["test_cooldown"]="test_cooldown"
|
||||||
["query_config"]="query_config"
|
["query_config"]="query_config"
|
||||||
@ -131,21 +125,25 @@ declare -A funcs=(
|
|||||||
["Remove from history"]="remove_from_history"
|
["Remove from history"]="remove_from_history"
|
||||||
["Force update local mods"]="force_update"
|
["Force update local mods"]="force_update"
|
||||||
["Resolve IP"]="resolve_ip"
|
["Resolve IP"]="resolve_ip"
|
||||||
["Handshake"]="final_handshake"
|
["Handshake"]="handshake"
|
||||||
|
["Handshake_EXP"]="handshake_exp"
|
||||||
["get_player_count"]="get_player_count"
|
["get_player_count"]="get_player_count"
|
||||||
["lan_scan"]="lan_scan"
|
|
||||||
["update_symlinks"]="update_symlinks"
|
["update_symlinks"]="update_symlinks"
|
||||||
)
|
)
|
||||||
|
|
||||||
lan_scan(){
|
clone_symlinks(){
|
||||||
local port="$1"
|
local path="$(< $_cache_binary)"
|
||||||
local res
|
path=$(dirname "$path")
|
||||||
res=$("$lan_helper" "$port")
|
for dir in $(find $game_dir -type l); do
|
||||||
if [[ -z $res ]]; then
|
local link=$(basename $dir)
|
||||||
printf "\n"
|
ln -sf "${dir}" "${path}/${link}"
|
||||||
else
|
done
|
||||||
printf "%s\n" "$res"
|
}
|
||||||
fi
|
handshake(){
|
||||||
|
final_handshake "$aid"
|
||||||
|
}
|
||||||
|
handshake_exp(){
|
||||||
|
final_handshake "$exp"
|
||||||
}
|
}
|
||||||
query_favorites(){
|
query_favorites(){
|
||||||
if [[ -z "${ip_list[@]}" ]]; then
|
if [[ -z "${ip_list[@]}" ]]; then
|
||||||
@ -207,44 +205,6 @@ get_player_count(){
|
|||||||
printf "%s\n%s" "$players" "$queue"
|
printf "%s\n%s" "$players" "$queue"
|
||||||
}
|
}
|
||||||
|
|
||||||
validate_and_connect(){
|
|
||||||
local context="$1"
|
|
||||||
local addr="$2"
|
|
||||||
|
|
||||||
local record
|
|
||||||
case "$context" in
|
|
||||||
"Connect by ID")
|
|
||||||
if [[ -z "$api_key" ]]; then
|
|
||||||
printf "No Battlemetrics API key set"
|
|
||||||
return 4
|
|
||||||
fi
|
|
||||||
record=$(map_id_to_ip "$addr")
|
|
||||||
if [[ $? -eq 1 ]]; then
|
|
||||||
logger WARN "Not a valid record: '$addr'"
|
|
||||||
printf "Not a valid ID"
|
|
||||||
return 2
|
|
||||||
fi
|
|
||||||
logger INFO "Battlemetrics ID resolved to IP $record"
|
|
||||||
;;
|
|
||||||
"Connect by IP")
|
|
||||||
if [[ $(validate_ip "$addr") -eq 1 ]]; then
|
|
||||||
printf "Not a valid IP format. Supply IP:Queryport"
|
|
||||||
return 2
|
|
||||||
fi
|
|
||||||
local ip=$(<<< $addr awk -F: '{print $1}')
|
|
||||||
local qport=$(<<< $addr awk -F: '{print $2}')
|
|
||||||
local res
|
|
||||||
res=$(a2s $ip $qport info)
|
|
||||||
if [[ ! $? -eq 0 ]]; then
|
|
||||||
printf "Timed out when querying the server. Is this a valid server?"
|
|
||||||
return 2
|
|
||||||
fi
|
|
||||||
local gameport="$(<<< $res jq -r '.[].gameport')"
|
|
||||||
record="${ip}:${gameport}:${qport}"
|
|
||||||
logger INFO "Record resolved to $record"
|
|
||||||
esac
|
|
||||||
try_connect "$record"
|
|
||||||
}
|
|
||||||
map_id_to_ip(){
|
map_id_to_ip(){
|
||||||
local id="$1"
|
local id="$1"
|
||||||
local res=$(curl -s "$bm_api" -H "Authorization: Bearer "$api_key"" \
|
local res=$(curl -s "$bm_api" -H "Authorization: Bearer "$api_key"" \
|
||||||
@ -310,32 +270,10 @@ add_record(){
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
connect_by_id(){
|
|
||||||
if [[ $(validate_ip "$addr") -eq 1 ]]; then
|
|
||||||
printf "Not a valid IP format. Supply IP:Queryport"
|
|
||||||
return 2
|
|
||||||
fi
|
|
||||||
local ip=$(<<< $addr awk -F: '{print $1}')
|
|
||||||
local qport=$(<<< $addr awk -F: '{print $2}')
|
|
||||||
local res
|
|
||||||
res=$(a2s $ip $qport info)
|
|
||||||
if [[ ! $? -eq 0 ]]; then
|
|
||||||
printf "Timed out when querying the server. Is this a valid server?"
|
|
||||||
return 2
|
|
||||||
fi
|
|
||||||
#res contains modlist
|
|
||||||
}
|
|
||||||
start_cooldown(){
|
start_cooldown(){
|
||||||
logger WARN "API response empty. Started 60s cooldown at $(date +%s)"
|
logger WARN "API response empty. Started 60s cooldown at $(date +%s)"
|
||||||
date +%s > $_cache_cooldown
|
date +%s > $_cache_cooldown
|
||||||
}
|
}
|
||||||
initialize_remote_servers(){
|
|
||||||
local file="$_cache_servers"
|
|
||||||
[[ -f $file ]] && rm "$file"
|
|
||||||
local res
|
|
||||||
res=$(get_remote_servers)
|
|
||||||
parse_server_json "$res" >> "$file"
|
|
||||||
}
|
|
||||||
is_dlc(){
|
is_dlc(){
|
||||||
local dlc
|
local dlc
|
||||||
local ip="$1"
|
local ip="$1"
|
||||||
@ -453,60 +391,10 @@ get_dist(){
|
|||||||
else
|
else
|
||||||
logger INFO "Resolved remote server geolocation to '$remote_lat, $remote_lon'"
|
logger INFO "Resolved remote server geolocation to '$remote_lat, $remote_lon'"
|
||||||
local dist=$($km_helper "$local_lat" "$local_lon" "$remote_lat" "$remote_lon")
|
local dist=$($km_helper "$local_lat" "$local_lon" "$remote_lat" "$remote_lon")
|
||||||
LC_NUMERIC=C printf "%d" "$dist"
|
LC_NUMERIC=C printf "%.0f" "$dist"
|
||||||
logger INFO "Distance: $dist km"
|
logger INFO "Distance: $dist km"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
get_remote_servers(){
|
|
||||||
params=(
|
|
||||||
"\\nor\1\map\chernarusplus\\nor\1\map\sakhal\\nor\1\map\enoch\empty\1\\nor\1\map\namalsk"
|
|
||||||
"\map\namalsk\empty\1"
|
|
||||||
"\map\namalsk\noplayers\1"
|
|
||||||
"\map\chernarusplus\empty\1"
|
|
||||||
"\map\chernarusplus\noplayers\1"
|
|
||||||
"\map\\sakhal\empty\1"
|
|
||||||
"\map\\sakhal\noplayers\1"
|
|
||||||
"\map\\enoch\empty\1"
|
|
||||||
"\map\\enoch\noplayers\1"
|
|
||||||
)
|
|
||||||
local limit=10000
|
|
||||||
local url="https://api.steampowered.com/IGameServersService/GetServerList/v1/?"
|
|
||||||
|
|
||||||
_fetch(){
|
|
||||||
local param="$1"
|
|
||||||
curl -LsG "$url" \
|
|
||||||
-d filter="\appid\221100${param}" \
|
|
||||||
-d limit=$limit \
|
|
||||||
-d key=$steam_api \
|
|
||||||
| jq -M -r '.response.servers'
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
for ((i=0; i <${#params[@]}; i++ )); do
|
|
||||||
_fetch "${params[$i]}" > $_cache_temp.${i}
|
|
||||||
done
|
|
||||||
|
|
||||||
# ubuntu variants do not support jq 1.7 chained operators
|
|
||||||
# https://github.com/jqlang/jq/releases/tag/jq-1.7
|
|
||||||
jq -n '[ [inputs]|add ][]' $_cache_temp.* && rm $_cache_temp.*
|
|
||||||
}
|
|
||||||
get_unique_maps(){
|
|
||||||
shift
|
|
||||||
local context="$1"
|
|
||||||
local filter_file
|
|
||||||
case "$context" in
|
|
||||||
"My saved servers")
|
|
||||||
filter_file="$_cache_my_servers"
|
|
||||||
;;
|
|
||||||
"Server browser")
|
|
||||||
filter_file="$_cache_servers"
|
|
||||||
;;
|
|
||||||
"Recent servers")
|
|
||||||
filter_file="$_cache_history"
|
|
||||||
esac
|
|
||||||
logger INFO "Map filter context is: '$context', using cached file at '$filter_file'"
|
|
||||||
< "$filter_file" awk -F$separator '{print $2}' | sort -u
|
|
||||||
}
|
|
||||||
query_config(){
|
query_config(){
|
||||||
[[ -n $2 ]] && local key=$2
|
[[ -n $2 ]] && local key=$2
|
||||||
keys=(
|
keys=(
|
||||||
@ -517,6 +405,7 @@ query_config(){
|
|||||||
"fav_label"
|
"fav_label"
|
||||||
"preferred_client"
|
"preferred_client"
|
||||||
"fullscreen"
|
"fullscreen"
|
||||||
|
"default_steam_path"
|
||||||
)
|
)
|
||||||
if [[ -n $key ]]; then
|
if [[ -n $key ]]; then
|
||||||
if [[ -n ${!key} ]]; then
|
if [[ -n ${!key} ]]; then
|
||||||
@ -531,154 +420,6 @@ query_config(){
|
|||||||
echo "${!i}"
|
echo "${!i}"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
filter_servers(){
|
|
||||||
local filtered="$(< "$1")"
|
|
||||||
shift
|
|
||||||
readarray -t filters < <(printf "%s\n" "$@")
|
|
||||||
|
|
||||||
for ((i=0; i< ${#filters[@]}; ++i)); do
|
|
||||||
if [[ ${filters[$i]} =~ Keyword ]]; then
|
|
||||||
keyword=$(<<< ${filters[$i]} awk -F␞ '{print $2}')
|
|
||||||
elif [[ ${filters[$i]} =~ Map ]]; then
|
|
||||||
map=$(<<< ${filters[$i]} awk -F= '{print $2}')
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
filter_ascii(){
|
|
||||||
if [[ ${filters[*]} =~ Non ]]; then
|
|
||||||
echo -n "$filtered"
|
|
||||||
else
|
|
||||||
<<< "$filtered" sed 's/␞/@@DZGUI_PLACEHOLDER@@/g' | grep -v -P '[^[:ascii:]]' | sed 's/@@DZGUI_PLACEHOLDER@@/␞/g'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
filter_time(){
|
|
||||||
if [[ ${filters[*]} =~ Day ]] && [[ ${filters[*]} =~ Night ]]; then
|
|
||||||
echo -n "$filtered"
|
|
||||||
elif [[ ${filters[*]} =~ Day ]]; then
|
|
||||||
<<< "$filtered" awk -F$separator '$4~/^([0][6-9]:|[1][0-6])/'
|
|
||||||
elif [[ ${filters[*]} =~ Night ]]; then
|
|
||||||
<<< "$filtered" awk -F$separator '$4~/^([1][7-9]:|[2][0-3]:|[0][0-5])/'
|
|
||||||
else
|
|
||||||
echo -n ""
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
filter_perspective(){
|
|
||||||
if [[ ${filters[*]} =~ 1PP ]] && [[ ${filters[*]} =~ 3PP ]]; then
|
|
||||||
echo -n "$filtered"
|
|
||||||
elif [[ ${filters[*]} =~ 1PP ]]; then
|
|
||||||
<<< "$filtered" awk '!/3PP/'
|
|
||||||
elif [[ ${filters[*]} =~ 3PP ]]; then
|
|
||||||
<<< "$filtered" awk '!/1PP/'
|
|
||||||
else
|
|
||||||
echo -n ""
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
filter_lowpop(){
|
|
||||||
if [[ ${filters[*]} =~ Low ]]; then
|
|
||||||
echo -n "$filtered"
|
|
||||||
else
|
|
||||||
<<< "$filtered" awk -F$separator '{if (($5 > 0) && ($5/$6)*100 >=30){print $0}}'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
filter_full(){
|
|
||||||
if [[ ${filters[*]} =~ Full ]]; then
|
|
||||||
echo -n "$filtered"
|
|
||||||
else
|
|
||||||
<<< "$filtered" awk -F$separator '$5 != $6'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
filter_empty(){
|
|
||||||
if [[ ${filters[*]} =~ Empty ]]; then
|
|
||||||
echo -n "$filtered"
|
|
||||||
else
|
|
||||||
<<< "$filtered" awk -F$separator '$5 != "0"'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
filter_map(){
|
|
||||||
if [[ $map == "All maps" ]]; then
|
|
||||||
echo "$filtered"
|
|
||||||
else
|
|
||||||
<<< "$filtered" awk -v var="$map" -F$separator '$2 == var'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
filter_keyword(){
|
|
||||||
keyword=$(sanitize "$keyword")
|
|
||||||
<<< "$filtered" awk -F$separator -v keyword="$keyword" 'tolower($0) ~ tolower(keyword)'
|
|
||||||
}
|
|
||||||
filter_duplicates(){
|
|
||||||
if [[ ${filters[*]} =~ Duplicate ]]; then
|
|
||||||
echo -n "$filtered"
|
|
||||||
else
|
|
||||||
<<< "$filtered" awk -F$separator '!seen[$1]++'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
filter_official(){
|
|
||||||
if [[ ${filters[*]} =~ Official ]]; then
|
|
||||||
echo -n "$filtered"
|
|
||||||
else
|
|
||||||
<<< "$filtered" awk -F$separator '$10 == "Community"'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
filter_community(){
|
|
||||||
if [[ ${filters[*]} =~ Unoffic ]]; then
|
|
||||||
echo -n "$filtered"
|
|
||||||
else
|
|
||||||
<<< "$filtered" awk -F$separator '$10 == "Official"'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
filtered=$(filter_perspective)
|
|
||||||
filtered=$(filter_full)
|
|
||||||
filtered=$(filter_empty)
|
|
||||||
filtered=$(filter_time)
|
|
||||||
filtered=$(filter_map)
|
|
||||||
filtered=$(filter_lowpop)
|
|
||||||
filtered=$(filter_ascii)
|
|
||||||
filtered=$(filter_duplicates)
|
|
||||||
filtered=$(filter_keyword)
|
|
||||||
filtered=$(filter_official)
|
|
||||||
filtered=$(filter_community)
|
|
||||||
|
|
||||||
if [[ -z "$filtered" ]]; then
|
|
||||||
logger WARN "Filter result is empty"
|
|
||||||
echo -n ""
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
logger INFO "Returning sorted server list back to UI"
|
|
||||||
printf "%s\n" "$filtered" | sort -k1
|
|
||||||
}
|
|
||||||
sanitize(){
|
|
||||||
echo "$1" | sed \
|
|
||||||
-e 's/\//\\\//g' \
|
|
||||||
-e 's/\$/\\$/g' \
|
|
||||||
-e 's/\[/\\[/g' \
|
|
||||||
-e 's/\]/\\]/g' \
|
|
||||||
-e 's/\#/\\#/g' \
|
|
||||||
-e 's/\./\\./g' \
|
|
||||||
-e 's/\^/\\^/g' \
|
|
||||||
-e 's/\=/\\=/g' \
|
|
||||||
-e 's/|/\\|/g' \
|
|
||||||
-e 's/\+/\\+/g' \
|
|
||||||
-e 's/(/\\(/g' \
|
|
||||||
-e 's/)/\\)/g'
|
|
||||||
}
|
|
||||||
parse_server_json(){
|
|
||||||
local response="$1"
|
|
||||||
# some servers pad SOH in name
|
|
||||||
<<< "$response" sed 's/\\u0001//g' | jq -r '
|
|
||||||
.[]|"\(.name)␞" +
|
|
||||||
"\(.map|if type == "string" then ascii_downcase else "null" end)␞" +
|
|
||||||
"\(if .gametype == null then "null" else (.gametype|split(",")|if any(. == "no3rd") then "1PP" else "3PP" end) end)␞" +
|
|
||||||
"\(if .gametype == null then "null" else (.gametype as $time|$time|test("[0-9]{2}:[0-9]{2}$") as $match|(if $match == true then ($time|scan("[0-9]{2}:[0-9]{2}$")) else "XXXX" end)) end)␞" +
|
|
||||||
"\(.players)␞" +
|
|
||||||
"\(.max_players)␞" +
|
|
||||||
"\(if .gametype == null then "0" elif .gametype|split("lqs")[1] == null then "0" else .gametype|split("lqs")[1]|split(",")[0] end)␞" +
|
|
||||||
"\(.addr|split(":")[0]):\(if .gameport == null then "XXXX" else .gameport end)␞" +
|
|
||||||
"\(.addr|split(":")[1])␞" +
|
|
||||||
"\(if .gametype == null then "null" else (.gametype|split(",")|if any(. == "external") then "Community" else "Official" end) end)"
|
|
||||||
' | sort -k1
|
|
||||||
}
|
|
||||||
align_versions_file(){
|
align_versions_file(){
|
||||||
shift
|
shift
|
||||||
local mod="$1"
|
local mod="$1"
|
||||||
@ -737,62 +478,6 @@ test_cooldown(){
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
dump_servers(){
|
|
||||||
local context="$1"
|
|
||||||
local subcontext="$2"
|
|
||||||
local ip
|
|
||||||
local qport
|
|
||||||
local res
|
|
||||||
_iterate(){
|
|
||||||
local file="$1"
|
|
||||||
shift
|
|
||||||
for server in "$@"; do
|
|
||||||
ip=$(<<< $server awk -F: '{print $1}')
|
|
||||||
qport=$(<<< $server awk -F: '{print $3}')
|
|
||||||
res=$(a2s "$ip" "$qport" info)
|
|
||||||
if [[ ! $? -eq 0 ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
parse_server_json "$res" >> "$file"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
case "$subcontext" in
|
|
||||||
*Server[[:space:]]browser*)
|
|
||||||
local file="$_cache_servers"
|
|
||||||
if [[ ! $context =~ filter ]]; then
|
|
||||||
initialize_remote_servers
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*My[[:space:]]saved[[:space:]]servers*)
|
|
||||||
local file="$_cache_my_servers"
|
|
||||||
if [[ ! $context =~ filter ]]; then
|
|
||||||
[[ -f $file ]] && rm $file
|
|
||||||
_iterate "$file" "${ip_list[@]}"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*Recent[[:space:]]servers*)
|
|
||||||
local file="$_cache_history"
|
|
||||||
if [[ ! $context =~ filter ]]; then
|
|
||||||
[[ -f $file ]] && rm $file
|
|
||||||
readarray -t iters < <(cat $history_file)
|
|
||||||
_iterate "$file" "${iters[@]}"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*Scan[[:space:]]LAN[[:space:]]servers*)
|
|
||||||
local port=$(<<< "$subcontext" awk -F: '{print $2}')
|
|
||||||
local file="$_cache_lan"
|
|
||||||
if [[ ! $context =~ filter ]]; 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'"
|
|
||||||
filter_servers "$file" "$@"
|
|
||||||
}
|
|
||||||
redact(){
|
redact(){
|
||||||
sed 's@\(/home/\)[^/]*@\1REDACTED@g'
|
sed 's@\(/home/\)[^/]*@\1REDACTED@g'
|
||||||
}
|
}
|
||||||
@ -1201,24 +886,6 @@ open_link(){
|
|||||||
xdg-open "$url"
|
xdg-open "$url"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
quick_connect(){
|
|
||||||
if [[ -z $fav_server ]]; then
|
|
||||||
printf "No favorite server currently set"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
try_connect "$fav_server"
|
|
||||||
}
|
|
||||||
connect_from_table(){
|
|
||||||
shift
|
|
||||||
local record="$1"
|
|
||||||
try_connect "$record"
|
|
||||||
}
|
|
||||||
pretty_print(){
|
|
||||||
while read -r line; do
|
|
||||||
printf "\t%s\n" "$line"
|
|
||||||
done < "$@"
|
|
||||||
}
|
|
||||||
generate_log(){
|
generate_log(){
|
||||||
source $config_file
|
source $config_file
|
||||||
cat <<-DOC > $system_log
|
cat <<-DOC > $system_log
|
||||||
@ -1408,10 +1075,17 @@ try_fallback(){
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
try_connect(){
|
try_connect(){
|
||||||
|
shift
|
||||||
local record="$1"
|
local record="$1"
|
||||||
|
local appid="$2"
|
||||||
|
local binary="$3"
|
||||||
|
|
||||||
local ip=$(<<< $record awk -F: '{print $1}')
|
local ip=$(<<< $record awk -F: '{print $1}')
|
||||||
local gameport=$(<<< $record awk -F: '{print $2}')
|
local gameport=$(<<< $record awk -F: '{print $2}')
|
||||||
local qport=$(<<< $record awk -F: '{print $3}')
|
local qport=$(<<< $record awk -F: '{print $3}')
|
||||||
|
|
||||||
|
[[ $appid -eq $exp ]] && echo "$binary" > $_cache_binary
|
||||||
|
|
||||||
local remote_mods
|
local remote_mods
|
||||||
remote_mods=$(a2s $ip $qport rules)
|
remote_mods=$(a2s $ip $qport rules)
|
||||||
if [[ $? -eq 1 ]]; then
|
if [[ $? -eq 1 ]]; then
|
||||||
@ -1436,11 +1110,11 @@ try_connect(){
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
case $auto_install in
|
case $auto_install in
|
||||||
"") manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods";;
|
"") manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods" "" "$appid";;
|
||||||
1|2) manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods" "auto" ;;
|
1|2) manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods" "auto" "$appid" ;;
|
||||||
esac
|
esac
|
||||||
else
|
else
|
||||||
launch "$ip" "$gameport" "$sanitized_mods"
|
launch "$ip" "$gameport" "$sanitized_mods" "$appid"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
check_architecture(){
|
check_architecture(){
|
||||||
@ -1451,68 +1125,6 @@ check_architecture(){
|
|||||||
echo 0
|
echo 0
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
focus_beta_client(){
|
|
||||||
_wid(){
|
|
||||||
wmctrl -ilx |\
|
|
||||||
awk 'tolower($3) == "steamwebhelper.steam"' |\
|
|
||||||
awk '$5 ~ /^Steam|Steam Games List/' |\
|
|
||||||
awk '{print $1}'
|
|
||||||
}
|
|
||||||
$steam_cmd steam://open/library 2>/dev/null 1>&2 &&
|
|
||||||
$steam_cmd steam://open/console 2>/dev/null 1>&2 &&
|
|
||||||
sleep 1s
|
|
||||||
until [[ -n $(_wid) ]]; do
|
|
||||||
sleep 0.1s
|
|
||||||
done
|
|
||||||
wmctrl -ia $(_wid)
|
|
||||||
sleep 0.1s
|
|
||||||
|
|
||||||
local wid=$(xdotool getactivewindow)
|
|
||||||
local geo=$(xdotool getwindowgeometry $wid)
|
|
||||||
local pos=$(<<< "$geo" awk 'NR==2 {print $2}' | sed 's/,/ /')
|
|
||||||
local dim=$(<<< "$geo" awk 'NR==3 {print $2}' | sed 's/x/ /')
|
|
||||||
local pos1=$(<<< "$pos" awk '{print $1}')
|
|
||||||
local pos2=$(<<< "$pos" awk '{print $2}')
|
|
||||||
local dim1=$(<<< "$dim" awk '{print $1}')
|
|
||||||
local dim2=$(<<< "$dim" awk '{print $2}')
|
|
||||||
local dim1=$(((dim1/2)+pos1))
|
|
||||||
local dim2=$(((dim2/2)+pos2))
|
|
||||||
|
|
||||||
xdotool mousemove $dim1 $dim2
|
|
||||||
xdotool click 1
|
|
||||||
$steam_cmd steam://open/library 2>/dev/null 1>&2 &&
|
|
||||||
$steam_cmd steam://open/console 2>/dev/null 1>&2
|
|
||||||
}
|
|
||||||
auto_mod_install(){
|
|
||||||
# currently unused, merged with manual method
|
|
||||||
local ip="$1"
|
|
||||||
local gameport="$2"
|
|
||||||
local diff="$3"
|
|
||||||
local sanitized_mods="$4"
|
|
||||||
|
|
||||||
console_dl "$diff" &&
|
|
||||||
$steam_cmd steam://open/downloads
|
|
||||||
|
|
||||||
local total=$(<<< "$diff" wc -l)
|
|
||||||
until [[ -z $(compare "$diff") ]]; do
|
|
||||||
local missing=$(compare "$diff" | wc -l)
|
|
||||||
echo "# Downloaded $(($total-missing)) of $total mods. ESC cancels"
|
|
||||||
done | $steamsafe_zenity --pulsate --progress --title="DZG Watcher" --auto-close --no-cancel --width=500 2>/dev/null
|
|
||||||
if [[ ! $? -eq 0 ]]; then
|
|
||||||
echo "User aborted connect process. Steam may have mods pending for download."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local diff=$(compare "$sanitized_mods")
|
|
||||||
|
|
||||||
if [[ -z $diff ]]; then
|
|
||||||
#wipe old version file and replace with latest stamps
|
|
||||||
rm "$versions_file"
|
|
||||||
check_timestamps
|
|
||||||
logger INFO "Local modlist matches remote, initiating launch request"
|
|
||||||
launch "$ip" "$gameport" "$sanitized_mods"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
force_update(){
|
force_update(){
|
||||||
if [[ ! $auto_install -eq 1 ]]; then
|
if [[ ! $auto_install -eq 1 ]]; then
|
||||||
printf "Only available when mod auto-install is ON"
|
printf "Only available when mod auto-install is ON"
|
||||||
@ -1520,21 +1132,10 @@ force_update(){
|
|||||||
fi
|
fi
|
||||||
rm "$versions_file"
|
rm "$versions_file"
|
||||||
local update=$(check_timestamps)
|
local update=$(check_timestamps)
|
||||||
manual_mod_install "null" "null" "$update" "null" "force"
|
manual_mod_install "null" "null" "$update" "null" "force" "null"
|
||||||
echo "Finished requesting mod updates."
|
echo "Finished requesting mod updates."
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
console_dl(){
|
|
||||||
readarray -t modids <<< "$@"
|
|
||||||
focus_beta_client
|
|
||||||
sleep 1.5s
|
|
||||||
for i in "${modids[@]}"; do
|
|
||||||
xdotool type --delay 0 "workshop_download_item $aid $i"
|
|
||||||
sleep 0.5s
|
|
||||||
xdotool key Return
|
|
||||||
sleep 0.5s
|
|
||||||
done
|
|
||||||
}
|
|
||||||
get_local_stamps(){
|
get_local_stamps(){
|
||||||
readarray -t modlist < <(printf "%s\n" "$@")
|
readarray -t modlist < <(printf "%s\n" "$@")
|
||||||
local max="${#modlist[@]}"
|
local max="${#modlist[@]}"
|
||||||
@ -1573,8 +1174,6 @@ check_timestamps(){
|
|||||||
local aligned=$(<<< "$local_stamps" jq -r '.response.publishedfiledetails[]|"\(.publishedfileid),\(.time_updated)"')
|
local aligned=$(<<< "$local_stamps" jq -r '.response.publishedfiledetails[]|"\(.publishedfileid),\(.time_updated)"')
|
||||||
readarray -t remote_ids < <(<<< "$aligned" awk -F, '{print $1}')
|
readarray -t remote_ids < <(<<< "$aligned" awk -F, '{print $1}')
|
||||||
readarray -t remote_times < <(<<< "$aligned" awk -F, '{print $2}')
|
readarray -t remote_times < <(<<< "$aligned" awk -F, '{print $2}')
|
||||||
readarray -t old_ids < <(< $versions_file awk -F, '{print $1}')
|
|
||||||
readarray -t old_times < <(< $versions_file awk -F, '{print $2}')
|
|
||||||
|
|
||||||
if [[ ! -f $versions_file ]]; then
|
if [[ ! -f $versions_file ]]; then
|
||||||
logger INFO "No prior versions file found, creating"
|
logger INFO "No prior versions file found, creating"
|
||||||
@ -1584,6 +1183,9 @@ check_timestamps(){
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
readarray -t old_ids < <(< $versions_file awk -F, '{print $1}')
|
||||||
|
readarray -t old_times < <(< $versions_file awk -F, '{print $2}')
|
||||||
|
|
||||||
declare -A remote_version
|
declare -A remote_version
|
||||||
declare -A local_version
|
declare -A local_version
|
||||||
|
|
||||||
@ -1626,6 +1228,9 @@ concat_mods(){
|
|||||||
local encoded_id
|
local encoded_id
|
||||||
local link
|
local link
|
||||||
for i in "${concat_arr[@]}"; do
|
for i in "${concat_arr[@]}"; do
|
||||||
|
until [[ -f $workshop_dir/$i/meta.cpp ]]; do
|
||||||
|
sleep 0.1s
|
||||||
|
done
|
||||||
id=$(awk -F"= " '/publishedid/ {print $2}' "$workshop_dir"/$i/meta.cpp | awk -F\; '{print $1}')
|
id=$(awk -F"= " '/publishedid/ {print $2}' "$workshop_dir"/$i/meta.cpp | awk -F\; '{print $1}')
|
||||||
encoded_id=$(encode $id)
|
encoded_id=$(encode $id)
|
||||||
link="@$encoded_id;"
|
link="@$encoded_id;"
|
||||||
@ -1644,6 +1249,8 @@ launch(){
|
|||||||
local ip="$1"
|
local ip="$1"
|
||||||
local gameport="$2"
|
local gameport="$2"
|
||||||
local mods="$3"
|
local mods="$3"
|
||||||
|
local appid="$4"
|
||||||
|
|
||||||
local concat
|
local concat
|
||||||
if [[ -n $mods ]]; then
|
if [[ -n $mods ]]; then
|
||||||
concat=$(concat_mods "$mods")
|
concat=$(concat_mods "$mods")
|
||||||
@ -1652,18 +1259,22 @@ launch(){
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
update_symlinks
|
update_symlinks
|
||||||
|
[[ $appid -eq $exp ]] && clone_symlinks
|
||||||
|
|
||||||
if [[ $debug -eq 1 ]]; then
|
if [[ $debug -eq 1 ]]; then
|
||||||
local launch_options="$steam_cmd -applaunch $aid -connect=$ip:$gameport -nolauncher -nosplash -name=$name -skipintro -mod=$concat"
|
local launch_options="$steam_cmd -applaunch $appid -connect=$ip:$gameport -nolauncher -nosplash -name=$name -skipintro -mod=$concat"
|
||||||
printf "Debug mode: these options would have been used to launch the game: $launch_options"
|
printf "Debug mode: these options would have been used to launch the game: $launch_options"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
echo "$concat" > "$_cache_launch"
|
echo "$concat" > "$_cache_launch"
|
||||||
echo "$ip:$gameport" > "$_cache_address"
|
echo "$ip:$gameport" > "$_cache_address"
|
||||||
logger INFO "Saved launch params: '$concat'"
|
logger INFO "Saved launch params: '$concat'"
|
||||||
printf "Launch conditions satisfied. DayZ will now launch after you confirm this dialog."
|
printf "Launch conditions satisfied. DayZ will launch after you confirm this dialog."
|
||||||
return 100
|
[[ $appid == "$aid" ]] && return 100
|
||||||
|
[[ $appid == "$exp" ]] && return 101
|
||||||
}
|
}
|
||||||
final_handshake(){
|
final_handshake(){
|
||||||
|
local appid="$1"
|
||||||
local saved_mods=$(< "$_cache_launch")
|
local saved_mods=$(< "$_cache_launch")
|
||||||
local saved_address=$(< "$_cache_address")
|
local saved_address=$(< "$_cache_address")
|
||||||
local res=$(is_dayz_running)
|
local res=$(is_dayz_running)
|
||||||
@ -1680,7 +1291,7 @@ final_handshake(){
|
|||||||
params+=("-skipintro")
|
params+=("-skipintro")
|
||||||
params+=("-name=$name")
|
params+=("-name=$name")
|
||||||
params+=("-mod=$saved_mods")
|
params+=("-mod=$saved_mods")
|
||||||
$steam_cmd -applaunch $aid "${params[@]}" &
|
$steam_cmd -applaunch $appid "${params[@]}" &
|
||||||
until [[ $(is_dayz_running) -eq 1 ]]; do
|
until [[ $(is_dayz_running) -eq 1 ]]; do
|
||||||
sleep 0.1s
|
sleep 0.1s
|
||||||
done
|
done
|
||||||
@ -1694,6 +1305,7 @@ manual_mod_install(){
|
|||||||
local diff="$3"
|
local diff="$3"
|
||||||
local sanitized_mods="$4"
|
local sanitized_mods="$4"
|
||||||
local mode="$5"
|
local mode="$5"
|
||||||
|
local appid="$6"
|
||||||
local ex="$state_path/dzg.watcher"
|
local ex="$state_path/dzg.watcher"
|
||||||
|
|
||||||
readarray -t stage_mods <<< "$diff"
|
readarray -t stage_mods <<< "$diff"
|
||||||
@ -1702,7 +1314,6 @@ manual_mod_install(){
|
|||||||
_watcher(){
|
_watcher(){
|
||||||
for((i=0;i<${#stage_mods[@]};i++)); do
|
for((i=0;i<${#stage_mods[@]};i++)); do
|
||||||
[[ -f $ex ]] && return 1
|
[[ -f $ex ]] && return 1
|
||||||
log ${stage_mods[$i]}
|
|
||||||
|
|
||||||
if [[ $mode == "auto" ]] || [[ $mode == "force" ]]; then
|
if [[ $mode == "auto" ]] || [[ $mode == "force" ]]; then
|
||||||
$steam_cmd "steam://url/CommunityFilePage/${stage_mods[$i]}+workshop_download_item $aid ${stage_mods[$i]}"
|
$steam_cmd "steam://url/CommunityFilePage/${stage_mods[$i]}+workshop_download_item $aid ${stage_mods[$i]}"
|
||||||
@ -1752,7 +1363,7 @@ manual_mod_install(){
|
|||||||
rm "$versions_file"
|
rm "$versions_file"
|
||||||
check_timestamps
|
check_timestamps
|
||||||
fi
|
fi
|
||||||
launch "$ip" "$gameport" "$sanitized_mods"
|
launch "$ip" "$gameport" "$sanitized_mods" "$appid"
|
||||||
else
|
else
|
||||||
printf "User aborted download process, or some mods may have failed to download. Try connecting again to resync."
|
printf "User aborted download process, or some mods may have failed to download. Try connecting again to resync."
|
||||||
exit 1
|
exit 1
|
||||||
@ -1776,7 +1387,7 @@ foreground(){
|
|||||||
}
|
}
|
||||||
main(){
|
main(){
|
||||||
local params="$(printf '"%s", ' "$@")"
|
local params="$(printf '"%s", ' "$@")"
|
||||||
logger INFO "Received request from UI constructor with params [${params::-2}]"
|
logger INFO "Received request from UI with params [${params::-2}]"
|
||||||
func=${funcs["$1"]}
|
func=${funcs["$1"]}
|
||||||
[[ -z $func ]] && return 1
|
[[ -z $func ]] && return 1
|
||||||
if [[ -z $2 ]]; then
|
if [[ -z $2 ]]; then
|
||||||
|
|||||||
492
helpers/pefile.py
Normal file
492
helpers/pefile.py
Normal file
@ -0,0 +1,492 @@
|
|||||||
|
import json
|
||||||
|
import struct
|
||||||
|
import typing # noqa
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
from shlex import shlex
|
||||||
|
from typing import BinaryIO, Union
|
||||||
|
|
||||||
|
# https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
|
||||||
|
endian = "<"
|
||||||
|
IMAGE_DIRECTORY_ENTRY = 2
|
||||||
|
VERSION_RESOURCE = 16
|
||||||
|
RESOURCE_NODE = ".rsrc"
|
||||||
|
PE32_x86 = "0x10b"
|
||||||
|
PE32_x64 = "0x20b"
|
||||||
|
VS_VERSION_INFO_MAGIC = "0xfeef04bd0000"
|
||||||
|
VS_VERSION_INFO_ID = "VS_VERSION_INFO"
|
||||||
|
|
||||||
|
|
||||||
|
class VersionMatch(Enum):
|
||||||
|
LOCAL_OLDER = 1
|
||||||
|
LOCAL_NEWER = 2
|
||||||
|
SAME_VERSION = 3
|
||||||
|
|
||||||
|
|
||||||
|
class u8:
|
||||||
|
fmt = "B"
|
||||||
|
|
||||||
|
|
||||||
|
class u16:
|
||||||
|
fmt = "H"
|
||||||
|
|
||||||
|
|
||||||
|
class u32:
|
||||||
|
fmt = "L"
|
||||||
|
|
||||||
|
|
||||||
|
class u64:
|
||||||
|
fmt = "Q"
|
||||||
|
|
||||||
|
|
||||||
|
class i8:
|
||||||
|
fmt = "b"
|
||||||
|
|
||||||
|
|
||||||
|
class i16:
|
||||||
|
fmt = "h"
|
||||||
|
|
||||||
|
|
||||||
|
class i32:
|
||||||
|
fmt = "l"
|
||||||
|
|
||||||
|
|
||||||
|
class i64:
|
||||||
|
fmt = "q"
|
||||||
|
|
||||||
|
|
||||||
|
class PackedData:
|
||||||
|
@classmethod
|
||||||
|
def unpack(cls, data: BinaryIO):
|
||||||
|
r = []
|
||||||
|
for key, value in cls.__annotations__.items():
|
||||||
|
if value == str:
|
||||||
|
f = data.read(8).rstrip(b"\x00\x00").decode()
|
||||||
|
else:
|
||||||
|
fmt = endian + (value.fmt)
|
||||||
|
size = struct.calcsize(fmt)
|
||||||
|
f = struct.unpack(fmt, data.read(size))[0]
|
||||||
|
r.append(f)
|
||||||
|
return cls(*r)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class COFF_FILE_HDR(PackedData):
|
||||||
|
machine_type: u16
|
||||||
|
number_of_sections: u16
|
||||||
|
timestamp: u32
|
||||||
|
pointer_to_symbol_table: u32
|
||||||
|
number_of_symbols: u32
|
||||||
|
size_of_optional_header: u16
|
||||||
|
characteristics: u16
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class OPTIONAL_HDR_X86(PackedData):
|
||||||
|
magic: u16
|
||||||
|
major_linker_ver: u8
|
||||||
|
minor_linker_ver: u8
|
||||||
|
size_of_code: u32
|
||||||
|
size_of_initialized_data: u32
|
||||||
|
size_of_uninitialized_data: u32
|
||||||
|
address_of_entry_point: u32
|
||||||
|
base_of_code: u32
|
||||||
|
base_of_data: u32
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class OPTIONAL_HDR_X64(PackedData):
|
||||||
|
magic: u16
|
||||||
|
major_linker_ver: u8
|
||||||
|
minor_linker_ver: u8
|
||||||
|
size_of_code: u32
|
||||||
|
size_of_initialized_data: u32
|
||||||
|
size_of_uninitialized_data: u32
|
||||||
|
address_of_entry_point: u32
|
||||||
|
base_of_code: u32
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class OPTIONAL_HDR_WIN_X86(PackedData):
|
||||||
|
image_base: u32
|
||||||
|
section_alignment: u32
|
||||||
|
file_alignment: u32
|
||||||
|
major_operating_system_version: u16
|
||||||
|
minor_operating_system_version: u16
|
||||||
|
major_image_version: u16
|
||||||
|
minor_image_version: u16
|
||||||
|
major_subsystem_version: u16
|
||||||
|
minor_subsystem_version: u16
|
||||||
|
win32_version_value: u32
|
||||||
|
size_of_image: u32
|
||||||
|
size_of_headers: u32
|
||||||
|
checksum: u32
|
||||||
|
subsystem: u16
|
||||||
|
dll_characteristics: u16
|
||||||
|
size_of_stack_reserve: u32
|
||||||
|
size_of_stack_commit: u32
|
||||||
|
size_of_heap_reserve: u32
|
||||||
|
size_of_heap_commit: u32
|
||||||
|
loader_flags: u32
|
||||||
|
number_of_rva_and_sizes: u32
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class OPTIONAL_HDR_WIN_X64(PackedData):
|
||||||
|
image_base: u64
|
||||||
|
section_alignment: u32
|
||||||
|
file_alignment: u32
|
||||||
|
major_operating_system_version: u16
|
||||||
|
minor_operating_system_version: u16
|
||||||
|
major_image_version: u16
|
||||||
|
minor_image_version: u16
|
||||||
|
major_subsystem_version: u16
|
||||||
|
minor_subsystem_version: u16
|
||||||
|
win32_version_value: u32
|
||||||
|
size_of_image: u32
|
||||||
|
size_of_headers: u32
|
||||||
|
checksum: u32
|
||||||
|
subsystem: u16
|
||||||
|
dll_characteristics: u16
|
||||||
|
size_of_stack_reserve: u64
|
||||||
|
size_of_stack_commit: u64
|
||||||
|
size_of_heap_reserve: u64
|
||||||
|
size_of_heap_commit: u64
|
||||||
|
loader_flags: u32
|
||||||
|
number_of_rva_and_sizes: u32
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class DATA_DIR(PackedData):
|
||||||
|
virtual_address: u32
|
||||||
|
size: u32
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class SECTION_HDR(PackedData):
|
||||||
|
name: str
|
||||||
|
virtual_size: u32
|
||||||
|
virtual_address: u32
|
||||||
|
size_of_raw_data: u32
|
||||||
|
pointer_to_raw_data: u32
|
||||||
|
pointer_to_relocations: u32
|
||||||
|
pointer_to_line_numbers: u32
|
||||||
|
number_of_relocations: u16
|
||||||
|
number_of_line_numbers: u16
|
||||||
|
characteristics: u32
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class RESOURCE_DIRECTORY_TABLE(PackedData):
|
||||||
|
characteristics: u32
|
||||||
|
timestamp: u32
|
||||||
|
major_version: u16
|
||||||
|
minor_version: u16
|
||||||
|
number_of_name_entries: u16
|
||||||
|
number_of_id_entries: u16
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class RESOURCE_DIRECTORY_ENTRY(PackedData):
|
||||||
|
"""
|
||||||
|
This field is either a string identifying a data leaf
|
||||||
|
(if the high bit is set) or an ID to another nested directory
|
||||||
|
(if the high bit is clear). The outermost level is always a
|
||||||
|
directory. If it is a name, the lower 31 bits are the offset from the
|
||||||
|
beginning of the resource section's raw data to the name
|
||||||
|
(the name consists of 16 bits length and trailing wide characters,
|
||||||
|
in Unicode, not 0-terminated).
|
||||||
|
"""
|
||||||
|
|
||||||
|
name_or_id: u32
|
||||||
|
data_or_subdir: u32
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class RESOURCE_DATA_ENTRY(PackedData):
|
||||||
|
data_rva: u32
|
||||||
|
size: u32
|
||||||
|
codepage: u32
|
||||||
|
reserved: u32
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class VS_VERSION_INFO_HDR(PackedData):
|
||||||
|
size: u16
|
||||||
|
value_length: u16
|
||||||
|
value_type: u16
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class DayZVersion:
|
||||||
|
major: int
|
||||||
|
minor: int
|
||||||
|
patch: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class FileVersion:
|
||||||
|
major: int
|
||||||
|
minor: int
|
||||||
|
build: int
|
||||||
|
revision: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class Result:
|
||||||
|
local: Union[str, None]
|
||||||
|
remote: str
|
||||||
|
build: str
|
||||||
|
path: Union[Path, None]
|
||||||
|
match: VersionMatch
|
||||||
|
error: Union[Exception, None]
|
||||||
|
|
||||||
|
|
||||||
|
class PeFileError(Exception):
|
||||||
|
"""Expected contents missing from headers or resource nodes"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AppNotInstalledError(Exception):
|
||||||
|
"""App not present in user's libraryfolders"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AppMovedError(Exception):
|
||||||
|
"""VDF points to a nonexistent location on disk"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class VDFLoadError(Exception):
|
||||||
|
"""Malformed VDF or JSON conversion"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def parse_version_number(data: BinaryIO):
|
||||||
|
# https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
|
||||||
|
minor = struct.unpack("<L", data.read(4))[0] >> 16 & 0xFFFF
|
||||||
|
major = struct.unpack("<L", data.read(4))[0] >> 0 & 0xFFFF
|
||||||
|
build = struct.unpack("<L", data.read(4))[0] >> 0 & 0xFFFF
|
||||||
|
revision = struct.unpack("<L", data.read(4))[0] >> 16 & 0xFFFF
|
||||||
|
return FileVersion(major, minor, build, revision)
|
||||||
|
|
||||||
|
|
||||||
|
def seek_to_hex(address: str, data: BinaryIO) -> None:
|
||||||
|
data.seek(int(address, base=16))
|
||||||
|
|
||||||
|
|
||||||
|
def seek_to_pe_stub(data: BinaryIO) -> None:
|
||||||
|
MAGIC = "0x3c"
|
||||||
|
seek_to_hex(MAGIC, data)
|
||||||
|
e_lfanew = hex(struct.unpack("<h", data.read(2))[0])
|
||||||
|
seek_to_hex(e_lfanew, data)
|
||||||
|
pe_stub = data.read(4).rstrip(b"\x00\x00\x00\x00").decode()
|
||||||
|
if pe_stub != "PE":
|
||||||
|
raise PeFileError("missing PE header data")
|
||||||
|
|
||||||
|
|
||||||
|
def get_dayz_version(file: Path) -> DayZVersion | Exception:
|
||||||
|
try:
|
||||||
|
version = get_version(file)
|
||||||
|
except Exception as e:
|
||||||
|
return e
|
||||||
|
patch = str(version.build) + str(version.revision)
|
||||||
|
dz_vers = DayZVersion(version.major, version.minor, int(patch))
|
||||||
|
return dz_vers
|
||||||
|
|
||||||
|
|
||||||
|
def dayz_version_to_str(v: DayZVersion) -> str:
|
||||||
|
return ".".join(str(el) for el in [v.major, v.minor, v.patch])
|
||||||
|
|
||||||
|
|
||||||
|
def dayz_version_from_str(v: str) -> DayZVersion:
|
||||||
|
vers = v.split(".")
|
||||||
|
assert len(vers) == 3
|
||||||
|
return DayZVersion(*[int(el) for el in vers])
|
||||||
|
|
||||||
|
|
||||||
|
def get_version(file):
|
||||||
|
with open(file, "rb") as f:
|
||||||
|
seek_to_pe_stub(f)
|
||||||
|
COFF = COFF_FILE_HDR.unpack(f)
|
||||||
|
|
||||||
|
pos = f.tell()
|
||||||
|
blob = f.read()
|
||||||
|
magic = hex(struct.unpack("<H", (blob[0:2]))[0])
|
||||||
|
f.seek(pos)
|
||||||
|
|
||||||
|
if magic == PE32_x86:
|
||||||
|
OPTIONAL_HDR_X86.unpack(f)
|
||||||
|
OBJW = OPTIONAL_HDR_WIN_X86.unpack(f)
|
||||||
|
else:
|
||||||
|
OPTIONAL_HDR_X64.unpack(f)
|
||||||
|
OBJW = OPTIONAL_HDR_WIN_X64.unpack(f)
|
||||||
|
|
||||||
|
if OBJW.number_of_rva_and_sizes < 1:
|
||||||
|
raise PeFileError("no data resource directory")
|
||||||
|
|
||||||
|
data_dirs = []
|
||||||
|
for rva in range(OBJW.number_of_rva_and_sizes):
|
||||||
|
data_dir = DATA_DIR.unpack(f)
|
||||||
|
data_dirs.append(data_dir)
|
||||||
|
|
||||||
|
res_dir = data_dirs[IMAGE_DIRECTORY_ENTRY]
|
||||||
|
dir_va = res_dir.virtual_address
|
||||||
|
|
||||||
|
for section in range(COFF.number_of_sections):
|
||||||
|
hdr = SECTION_HDR.unpack(f)
|
||||||
|
if hdr.name == RESOURCE_NODE:
|
||||||
|
va = hdr.virtual_address
|
||||||
|
ptr = hdr.pointer_to_raw_data
|
||||||
|
offset = dir_va - va + ptr
|
||||||
|
seek_to_hex(hex(offset), f)
|
||||||
|
break
|
||||||
|
|
||||||
|
if hdr.name != RESOURCE_NODE:
|
||||||
|
raise PeFileError("no root resource node found")
|
||||||
|
|
||||||
|
table = RESOURCE_DIRECTORY_TABLE.unpack(f)
|
||||||
|
total = table.number_of_name_entries + table.number_of_id_entries
|
||||||
|
|
||||||
|
for entry in range(total):
|
||||||
|
entry = RESOURCE_DIRECTORY_ENTRY.unpack(f)
|
||||||
|
if entry.name_or_id == VERSION_RESOURCE:
|
||||||
|
while entry.data_or_subdir & (1 << 31):
|
||||||
|
shift = entry.data_or_subdir & ~(1 << 31)
|
||||||
|
seek_to_hex(hex(offset + shift), f)
|
||||||
|
table = RESOURCE_DIRECTORY_TABLE.unpack(f)
|
||||||
|
total = (
|
||||||
|
table.number_of_name_entries
|
||||||
|
+ table.number_of_id_entries
|
||||||
|
)
|
||||||
|
for entry in range(total):
|
||||||
|
entry = RESOURCE_DIRECTORY_ENTRY.unpack(f)
|
||||||
|
break
|
||||||
|
if entry.name_or_id > VERSION_RESOURCE:
|
||||||
|
raise PeFileError("no version info node found")
|
||||||
|
seek_to_hex(hex(offset + entry.data_or_subdir), f)
|
||||||
|
|
||||||
|
data = RESOURCE_DATA_ENTRY.unpack(f)
|
||||||
|
# https://stackoverflow.com/questions/2170843/va-virtual-address-rva-relative-virtual-address
|
||||||
|
offset = data.data_rva - hdr.virtual_address + hdr.pointer_to_raw_data
|
||||||
|
seek_to_hex(hex(offset), f)
|
||||||
|
|
||||||
|
hdr = VS_VERSION_INFO_HDR.unpack(f)
|
||||||
|
# https://learn.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo
|
||||||
|
byte_len = len(VS_VERSION_INFO_ID.encode("utf-16le"))
|
||||||
|
label = f.read(byte_len).decode("utf-16le")
|
||||||
|
if label != VS_VERSION_INFO_ID:
|
||||||
|
raise PeFileError(f"header identifier != '{VS_VERSION_INFO_ID}'")
|
||||||
|
f.read(32 - byte_len)
|
||||||
|
|
||||||
|
identifier = hex(struct.unpack("<Q", f.read(8))[0])
|
||||||
|
if identifier != VS_VERSION_INFO_MAGIC:
|
||||||
|
raise PeFileError(
|
||||||
|
f"{VS_VERSION_INFO_ID} address != '{VS_VERSION_INFO_MAGIC}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
version = parse_version_number(f)
|
||||||
|
except Exception as e:
|
||||||
|
return PeFileError(e)
|
||||||
|
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
def get_pefile_path(path: str, appid: int) -> Path:
|
||||||
|
binary = "DayZ_x64.exe"
|
||||||
|
identifier = {221100: "DayZ", 1024020: "DayZ Exp"}
|
||||||
|
name = identifier[appid]
|
||||||
|
|
||||||
|
pe_path = None
|
||||||
|
path = path + "/steamapps/libraryfolders.vdf"
|
||||||
|
|
||||||
|
with open(path, "r") as f:
|
||||||
|
try:
|
||||||
|
j = json.loads(vdf_to_json(f))
|
||||||
|
except Exception:
|
||||||
|
raise VDFLoadError("Failed to parse libraryfolders")
|
||||||
|
|
||||||
|
for obj in j["libraryfolders"]:
|
||||||
|
if str(appid) in j["libraryfolders"][obj]["apps"]:
|
||||||
|
pe_path = j["libraryfolders"][obj]["path"]
|
||||||
|
pe_path += f"/steamapps/common/{name}/{binary}"
|
||||||
|
break
|
||||||
|
|
||||||
|
if pe_path is None:
|
||||||
|
raise AppNotInstalledError(
|
||||||
|
f"Failed to find a libraryfolder for the appid '{appid}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
pe_path = Path(pe_path)
|
||||||
|
if pe_path.exists() is False:
|
||||||
|
raise AppMovedError(
|
||||||
|
f"Path '{pe_path}' specified in libraryfolders does not exist"
|
||||||
|
)
|
||||||
|
|
||||||
|
return pe_path
|
||||||
|
|
||||||
|
|
||||||
|
def compare_versions(local: DayZVersion, remote: DayZVersion):
|
||||||
|
"""
|
||||||
|
packaging.version module is not available OOTB on some distributions
|
||||||
|
"""
|
||||||
|
if dayz_version_to_str(local) == dayz_version_to_str(remote):
|
||||||
|
return VersionMatch.SAME_VERSION
|
||||||
|
|
||||||
|
if local.major < remote.major:
|
||||||
|
return VersionMatch.LOCAL_OLDER
|
||||||
|
if local.major > remote.major:
|
||||||
|
return VersionMatch.LOCAL_NEWER
|
||||||
|
if local.major == remote.major:
|
||||||
|
if local.minor < remote.minor:
|
||||||
|
return VersionMatch.LOCAL_OLDER
|
||||||
|
if local.minor > remote.minor:
|
||||||
|
return VersionMatch.LOCAL_NEWER
|
||||||
|
if local.minor == remote.minor:
|
||||||
|
if local.patch < remote.patch:
|
||||||
|
return VersionMatch.LOCAL_OLDER
|
||||||
|
if local.patch > remote.patch:
|
||||||
|
return VersionMatch.LOCAL_NEWER
|
||||||
|
if local.patch == remote.patch:
|
||||||
|
return VersionMatch.SAME_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
def vdf_to_json(stream):
|
||||||
|
def _istr(indent, string):
|
||||||
|
return (indent * " ") + string
|
||||||
|
|
||||||
|
jbuf = "{\n"
|
||||||
|
lex = shlex(stream)
|
||||||
|
indent = 1
|
||||||
|
|
||||||
|
while True:
|
||||||
|
tok = lex.get_token()
|
||||||
|
if not tok:
|
||||||
|
return jbuf + "}\n"
|
||||||
|
if tok == "}":
|
||||||
|
indent -= 1
|
||||||
|
jbuf += _istr(indent, "}")
|
||||||
|
ntok = lex.get_token()
|
||||||
|
lex.push_token(ntok)
|
||||||
|
if ntok and ntok != "}":
|
||||||
|
jbuf += ","
|
||||||
|
jbuf += "\n"
|
||||||
|
else:
|
||||||
|
ntok = lex.get_token()
|
||||||
|
if ntok == "{":
|
||||||
|
jbuf += _istr(indent, tok + ": {\n")
|
||||||
|
indent += 1
|
||||||
|
else:
|
||||||
|
jbuf += _istr(indent, tok + ": " + ntok)
|
||||||
|
ntok = lex.get_token()
|
||||||
|
lex.push_token(ntok)
|
||||||
|
if ntok != "}":
|
||||||
|
jbuf += ","
|
||||||
|
jbuf += "\n"
|
||||||
@ -29,6 +29,18 @@ params = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BmAPIError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BmIdError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidIpError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_netmask() -> str:
|
def get_netmask() -> str:
|
||||||
hostname = os.uname()[1]
|
hostname = os.uname()[1]
|
||||||
i = socket.gethostbyname(hostname)
|
i = socket.gethostbyname(hostname)
|
||||||
@ -189,38 +201,57 @@ def query_direct(ip: str, qport: int, TIMEOUT=3.0) -> dict | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(slots=True, frozen=True)
|
||||||
class Res:
|
class Res:
|
||||||
status: int
|
status: int
|
||||||
parsed: bool
|
parsed: bool
|
||||||
json: Union[str, None]
|
json: Union[str, None]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(slots=True, frozen=True)
|
||||||
class Ping:
|
class Ping:
|
||||||
addr: str
|
addr: str
|
||||||
iteration: int
|
iteration: int
|
||||||
ping: int
|
ping: int
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(slots=True, frozen=True)
|
||||||
class Details:
|
class Details:
|
||||||
data: Union[list, None]
|
data: Union[list, None]
|
||||||
description: str
|
description: str
|
||||||
success: bool
|
success: bool
|
||||||
|
|
||||||
|
|
||||||
def is_passworded(ip: str, qport: int) -> bool:
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class Prereqs:
|
||||||
|
password: bool
|
||||||
|
gameport: int
|
||||||
|
appid: Union[int, None]
|
||||||
|
version: Union[str, None]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class Record:
|
||||||
|
"""
|
||||||
|
The gameport field is manipulated by the RowType.CONN_BY_IP method
|
||||||
|
"""
|
||||||
|
ip: str
|
||||||
|
gameport: int
|
||||||
|
qport: int
|
||||||
|
|
||||||
|
|
||||||
|
def get_prereqs(ip: str, qport: int) -> Prereqs:
|
||||||
try:
|
try:
|
||||||
info = a2s.info((ip, qport))
|
info = a2s.info((ip, qport))
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
return False
|
return Prereqs(False, 0, None, None)
|
||||||
|
|
||||||
try:
|
gameport = getattr(info, "port", 0)
|
||||||
password = info.password_protected
|
is_password = getattr(info, "password_protected", False)
|
||||||
except AttributeError:
|
appid = getattr(info, "game_id", None)
|
||||||
return False
|
version = getattr(info, "version", None)
|
||||||
return password
|
|
||||||
|
return Prereqs(is_password, gameport, appid, version)
|
||||||
|
|
||||||
|
|
||||||
def details(ip: str, qport: int) -> Details:
|
def details(ip: str, qport: int) -> Details:
|
||||||
@ -249,10 +280,10 @@ def details(ip: str, qport: int) -> Details:
|
|||||||
for keyword in keywords:
|
for keyword in keywords:
|
||||||
if "etm" in keyword:
|
if "etm" in keyword:
|
||||||
day_accel = float(keyword.lstrip("etm"))
|
day_accel = float(keyword.lstrip("etm"))
|
||||||
day_accel = f"{day_accel:g}"
|
day_accel = float(f"{day_accel:g}")
|
||||||
if "entm" in keywords:
|
if "entm" in keywords:
|
||||||
night_accel = float(keyword.lstrip("entm"))
|
night_accel = float(keyword.lstrip("entm"))
|
||||||
night_accel = f"{night_accel:g}"
|
night_accel = float(f"{night_accel:g}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
password = info.password_protected
|
password = info.password_protected
|
||||||
@ -343,12 +374,12 @@ def ping(iteration: int, row: list) -> Ping:
|
|||||||
return Ping(addr, iteration, ping)
|
return Ping(addr, iteration, ping)
|
||||||
|
|
||||||
|
|
||||||
def query_api(key: str, param: str) -> Res:
|
def query_api(key: str, appid: int, param: str) -> Res:
|
||||||
LIMIT = 10000
|
LIMIT = 10000
|
||||||
url = "https://api.steampowered.com/IGameServersService/GetServerList/v1/?"
|
url = "https://api.steampowered.com/IGameServersService/GetServerList/v1/?"
|
||||||
|
|
||||||
payload: dict[str, Union[int, str]] = {
|
payload: dict[str, Union[int, str]] = {
|
||||||
"filter": r"\appid\221100" + param,
|
"filter": r"\appid" + fr"\{appid}" + param,
|
||||||
"limit": LIMIT,
|
"limit": LIMIT,
|
||||||
"key": key,
|
"key": key,
|
||||||
}
|
}
|
||||||
@ -375,3 +406,62 @@ def query_api(key: str, param: str) -> Res:
|
|||||||
data = None
|
data = None
|
||||||
finally:
|
finally:
|
||||||
return Res(status, parsed, data)
|
return Res(status, parsed, data)
|
||||||
|
|
||||||
|
|
||||||
|
def query_bm_api(api_key: str, bm_id: str) -> Record:
|
||||||
|
if bm_id.isnumeric() is False:
|
||||||
|
raise BmIdError("ID must be numeric only")
|
||||||
|
|
||||||
|
payload: dict[str, Union[int, str]] = {
|
||||||
|
"sort": "-players",
|
||||||
|
"filter[game]": "dayz",
|
||||||
|
"filter[ids][whitelist]": bm_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
url = "https://api.battlemetrics.com/servers?"
|
||||||
|
par = parse.urlencode(payload)
|
||||||
|
url = f"{url}{par}"
|
||||||
|
|
||||||
|
hdr = {"Authorization": "Bearer " + api_key}
|
||||||
|
r = request.Request(url, headers=hdr)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with request.urlopen(r) as response:
|
||||||
|
try:
|
||||||
|
j = json.load(response)
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
raise BmAPIError("Malformed response from Battlemetrics")
|
||||||
|
|
||||||
|
if len(j["data"]) < 1:
|
||||||
|
raise BmAPIError("Not a valid Battlemetrics ID")
|
||||||
|
j = j["data"][0]["attributes"]
|
||||||
|
return Record(j["ip"], j["port"], j["portQuery"])
|
||||||
|
except HTTPError:
|
||||||
|
raise BmAPIError("Failed to query Battlemetrics")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_ip(addr: str):
|
||||||
|
fields = addr.split(":")
|
||||||
|
if len(fields) != 2:
|
||||||
|
raise InvalidIpError("Address must be formatted as IP:Queryport")
|
||||||
|
|
||||||
|
ip = fields[0]
|
||||||
|
port = fields[1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
int(port)
|
||||||
|
except ValueError:
|
||||||
|
raise InvalidIpError(f"'{port}' is not a valid port")
|
||||||
|
|
||||||
|
if int(port) > 65535 or int(port) < 0:
|
||||||
|
raise InvalidIpError(f"'{port}' is not a valid port")
|
||||||
|
|
||||||
|
try:
|
||||||
|
socket.inet_aton(ip)
|
||||||
|
except OSError:
|
||||||
|
raise InvalidIpError(f"'{ip}' is not a valid IP")
|
||||||
|
|
||||||
|
ip = addr.split(":")[0]
|
||||||
|
qport = int(addr.split(":")[1])
|
||||||
|
record = Record(ip, 0, qport)
|
||||||
|
return record
|
||||||
|
|||||||
431
helpers/ui.py
431
helpers/ui.py
@ -13,22 +13,30 @@ import threading
|
|||||||
import typing # noqa
|
import typing # noqa
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from collections.abc import Callable
|
||||||
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 typing import Literal, Self, Any
|
from typing import Literal, Self, Any
|
||||||
|
|
||||||
sys.path.append("servers")
|
|
||||||
import servers as Servers # noqa E402
|
import servers as Servers # noqa E402
|
||||||
|
import pefile as PeFile # noqa E402
|
||||||
|
|
||||||
|
from pefile import (
|
||||||
|
VDFLoadError,
|
||||||
|
AppNotInstalledError,
|
||||||
|
AppMovedError,
|
||||||
|
PeFileError,
|
||||||
|
)
|
||||||
|
from pefile import VersionMatch, DayZVersion
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "")
|
locale.setlocale(locale.LC_ALL, "")
|
||||||
|
|
||||||
import gi # noqa E402
|
import gi # noqa E402
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa: E402
|
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402
|
||||||
|
|
||||||
# https://bugzilla.gnome.org/show_bug.cgi?id=708676
|
# https://bugzilla.gnome.org/show_bug.cgi?id=708676
|
||||||
warnings.filterwarnings("ignore", ".*g_value_get_int", Warning)
|
warnings.filterwarnings("ignore", ".*g_value_get_int", Warning)
|
||||||
@ -38,6 +46,9 @@ app_name_lower = app_name.lower()
|
|||||||
app_name_abbr = "dzg"
|
app_name_abbr = "dzg"
|
||||||
delimiter = "␞"
|
delimiter = "␞"
|
||||||
|
|
||||||
|
APPID_DAYZ = 221100
|
||||||
|
APPID_DAYZ_EXP = 1024020
|
||||||
|
|
||||||
cache: dict[str, int] = {}
|
cache: dict[str, int] = {}
|
||||||
config_vals: list[str] = []
|
config_vals: list[str] = []
|
||||||
|
|
||||||
@ -88,6 +99,13 @@ If this issue persists, your API key may be defunct.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class Record:
|
||||||
|
ip: str
|
||||||
|
gameport: int
|
||||||
|
qport: int
|
||||||
|
|
||||||
|
|
||||||
class Preferences(Enum):
|
class Preferences(Enum):
|
||||||
STEAM = 1
|
STEAM = 1
|
||||||
BM = 2
|
BM = 2
|
||||||
@ -350,6 +368,12 @@ class RowType(EnumWithAttrs):
|
|||||||
"wait_msg": "Waiting for DayZ",
|
"wait_msg": "Waiting for DayZ",
|
||||||
"type": Command.ONESHOT,
|
"type": Command.ONESHOT,
|
||||||
}
|
}
|
||||||
|
HANDSHAKE_EXP = {
|
||||||
|
"label": "Handshake_EXP",
|
||||||
|
"tooltip": None,
|
||||||
|
"wait_msg": "Waiting for DayZ",
|
||||||
|
"type": Command.ONESHOT,
|
||||||
|
}
|
||||||
DELETE_SELECTED = {
|
DELETE_SELECTED = {
|
||||||
"label": "Delete selected mods",
|
"label": "Delete selected mods",
|
||||||
"tooltip": None,
|
"tooltip": None,
|
||||||
@ -667,13 +691,15 @@ def save_res_and_quit(*args) -> None:
|
|||||||
def suppress_signal(
|
def suppress_signal(
|
||||||
owner: Gtk.Widget, widget: Gtk.Widget, func_name: str, state: bool
|
owner: Gtk.Widget, widget: Gtk.Widget, func_name: str, state: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
func = getattr(owner, func_name)
|
func = getattr(owner, func_name)
|
||||||
if state:
|
if state:
|
||||||
logger.debug(f"Unblocking {func_name} for {widget}")
|
logger.debug(f"Blocking {func_name} for {widget}")
|
||||||
widget.handler_block_by_func(func)
|
widget.handler_block_by_func(func)
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Blocking {func_name} for {widget}")
|
logger.debug(f"Unblocking {func_name} for {widget}")
|
||||||
widget.handler_unblock_by_func(func)
|
widget.handler_unblock_by_func(func)
|
||||||
|
App.treeview.sel_blocked = state
|
||||||
|
|
||||||
|
|
||||||
def pluralize(plural: str, count: int) -> str:
|
def pluralize(plural: str, count: int) -> str:
|
||||||
@ -705,6 +731,7 @@ def format_metadata(row_sel: str) -> str:
|
|||||||
"fav_label": config_vals[4],
|
"fav_label": config_vals[4],
|
||||||
"preferred_client": config_vals[5],
|
"preferred_client": config_vals[5],
|
||||||
"fullscreen": config_vals[6],
|
"fullscreen": config_vals[6],
|
||||||
|
"default_steam_path": config_vals[7],
|
||||||
}
|
}
|
||||||
if row is None:
|
if row is None:
|
||||||
return ""
|
return ""
|
||||||
@ -780,7 +807,7 @@ def set_surrounding_margins(widget: Gtk.Widget, margin: int) -> None:
|
|||||||
def query_history() -> list | None:
|
def query_history() -> list | None:
|
||||||
try:
|
try:
|
||||||
with open(history_file, "r") as f:
|
with open(history_file, "r") as f:
|
||||||
rows = [row for row in f]
|
rows = [row.rstrip("\n") for row in f]
|
||||||
except OSError:
|
except OSError:
|
||||||
rows = None
|
rows = None
|
||||||
finally:
|
finally:
|
||||||
@ -899,6 +926,11 @@ def process_shell_return_code(
|
|||||||
if final_conf == 1 or final_conf is None:
|
if final_conf == 1 or final_conf is None:
|
||||||
return
|
return
|
||||||
process_tree_option(RowType.HANDSHAKE)
|
process_tree_option(RowType.HANDSHAKE)
|
||||||
|
case 101: # final handshake, exp
|
||||||
|
final_conf = spawn_dialog(msg, Popup.CONFIRM)
|
||||||
|
if final_conf == 1 or final_conf is None:
|
||||||
|
return
|
||||||
|
process_tree_option(RowType.HANDSHAKE_EXP)
|
||||||
case 255: # dzgui version update
|
case 255: # dzgui version update
|
||||||
msg = "Update complete. Please close DZGUI and restart."
|
msg = "Update complete. Please close DZGUI and restart."
|
||||||
spawn_dialog(msg, Popup.QUIT)
|
spawn_dialog(msg, Popup.QUIT)
|
||||||
@ -907,6 +939,10 @@ def process_shell_return_code(
|
|||||||
def call_on_thread(
|
def call_on_thread(
|
||||||
state: bool, subproc: str, msg: str, args: str, choice: RowType = None
|
state: bool, subproc: str, msg: str, args: str, choice: RowType = None
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
Exclusively used for threaded subprocesses
|
||||||
|
"""
|
||||||
|
|
||||||
def _background(subproc: str, args: str, dialog):
|
def _background(subproc: str, args: str, dialog):
|
||||||
def _load() -> None:
|
def _load() -> None:
|
||||||
wait_dialog.destroy()
|
wait_dialog.destroy()
|
||||||
@ -970,6 +1006,22 @@ def process_tree_option(choice: RowType) -> None:
|
|||||||
App.grid.notebook.set_page_by_enum(NotebookPage.CHANGELOG)
|
App.grid.notebook.set_page_by_enum(NotebookPage.CHANGELOG)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if command == RowType.QUICK_CONNECT:
|
||||||
|
record = query_config("fav_server")[0]
|
||||||
|
if record == "":
|
||||||
|
spawn_dialog("No favorite server currently set", Popup.NOTIFY)
|
||||||
|
return
|
||||||
|
|
||||||
|
record = str_to_record(record)
|
||||||
|
thread_new_with_dialog(
|
||||||
|
App.treeview.prepare_connection,
|
||||||
|
parse_shell_output,
|
||||||
|
"Querying server",
|
||||||
|
command,
|
||||||
|
[record],
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
match command.dict["type"]:
|
match command.dict["type"]:
|
||||||
case Command.HELP:
|
case Command.HELP:
|
||||||
call_bash_func("Open link", cmd_string)
|
call_bash_func("Open link", cmd_string)
|
||||||
@ -986,6 +1038,60 @@ def process_tree_option(choice: RowType) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def parse_shell_output(proc: subprocess.CompletedProcess, row: RowType):
|
||||||
|
out = proc.stdout.splitlines()
|
||||||
|
try:
|
||||||
|
msg = out[-1]
|
||||||
|
except IndexError:
|
||||||
|
msg = ""
|
||||||
|
process_shell_return_code(msg, proc.returncode, row)
|
||||||
|
|
||||||
|
|
||||||
|
def thread_new_with_dialog(
|
||||||
|
func: Callable,
|
||||||
|
callback: Callable | None,
|
||||||
|
msg: str,
|
||||||
|
row: RowType | None,
|
||||||
|
args: list,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Pop a GenericDialog transient to App.treeview and
|
||||||
|
call a function on a thread, with optional callback.
|
||||||
|
Chiefly used for connection-related subprocesses.
|
||||||
|
|
||||||
|
After completion, the dialog is destroyed in the main event loop
|
||||||
|
and additional exception handling occurs.
|
||||||
|
|
||||||
|
This is intended as a bridge between legacy shell methods and the UI.
|
||||||
|
A more abstracted version of call_on_thread() for when extra threaded
|
||||||
|
processing occurs before calls to shell subprocesses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def background(*args):
|
||||||
|
def cleanup():
|
||||||
|
App.treeview.dialog_hide()
|
||||||
|
if exception is not None:
|
||||||
|
spawn_dialog(str(exception), Popup.NOTIFY)
|
||||||
|
process_user_input(row)
|
||||||
|
return
|
||||||
|
if callback is not None and proc is not None:
|
||||||
|
callback(proc, row)
|
||||||
|
|
||||||
|
exception = None
|
||||||
|
proc = None
|
||||||
|
try:
|
||||||
|
proc = func(*args)
|
||||||
|
except Exception as e:
|
||||||
|
exception = e
|
||||||
|
GLib.idle_add(cleanup)
|
||||||
|
return
|
||||||
|
GLib.idle_add(cleanup)
|
||||||
|
|
||||||
|
App.treeview.dialog_show(msg)
|
||||||
|
thread = threading.Thread(target=background, args=(args))
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
|
||||||
def process_toggle(command: RowType) -> None:
|
def process_toggle(command: RowType) -> None:
|
||||||
cmd_string = command.dict["label"]
|
cmd_string = command.dict["label"]
|
||||||
match command:
|
match command:
|
||||||
@ -1010,11 +1116,54 @@ def process_toggle(command: RowType) -> None:
|
|||||||
proc = call_out("toggle", cmd_string)
|
proc = call_out("toggle", cmd_string)
|
||||||
|
|
||||||
|
|
||||||
|
def str_to_record(record: str) -> Record | None:
|
||||||
|
r = record.split(":")
|
||||||
|
if len(r) != 3:
|
||||||
|
return None
|
||||||
|
return Record(r[0], int(r[1]), int(r[2]))
|
||||||
|
|
||||||
|
|
||||||
|
def record_to_str(record: Record) -> str:
|
||||||
|
return f"{record.ip}:{record.gameport}:{record.qport}"
|
||||||
|
|
||||||
|
|
||||||
|
def connect_by_ip(enum: RowType, response: str) -> None:
|
||||||
|
def _prep(response: str) -> None:
|
||||||
|
record = Servers.validate_ip(response)
|
||||||
|
proc = App.treeview.prepare_connection(record)
|
||||||
|
return proc
|
||||||
|
|
||||||
|
thread_new_with_dialog(
|
||||||
|
_prep, parse_shell_output, "Querying IP", enum, [response]
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def connect_by_id(enum: RowType, response: str, key: str) -> None:
|
||||||
|
def _prep(key: str, response: str) -> None:
|
||||||
|
record = Servers.query_bm_api(key, response)
|
||||||
|
proc = App.treeview.prepare_connection(record)
|
||||||
|
return proc
|
||||||
|
|
||||||
|
thread_new_with_dialog(
|
||||||
|
_prep, parse_shell_output, "Querying API", enum, [key, response]
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def process_user_input(enum: RowType) -> None:
|
def process_user_input(enum: RowType) -> None:
|
||||||
prompt = enum.dict["prompt"]
|
prompt = enum.dict["prompt"]
|
||||||
link_label = enum.dict["link_label"]
|
link_label = enum.dict["link_label"]
|
||||||
cmd_string = enum.dict["label"]
|
cmd_string = enum.dict["label"]
|
||||||
|
|
||||||
|
if enum == RowType.CONN_BY_ID:
|
||||||
|
key = query_config("api_key")[0]
|
||||||
|
if len(key) == 0:
|
||||||
|
spawn_dialog(
|
||||||
|
"No Battlemetrics API key is set; see Options", Popup.NOTIFY
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
user_entry = EntryDialog(prompt, Popup.ENTRY, link_label)
|
user_entry = EntryDialog(prompt, Popup.ENTRY, link_label)
|
||||||
response = user_entry.get_input()
|
response = user_entry.get_input()
|
||||||
|
|
||||||
@ -1023,6 +1172,14 @@ def process_user_input(enum: RowType) -> None:
|
|||||||
return
|
return
|
||||||
logger.info(f"User entered: '{response}'")
|
logger.info(f"User entered: '{response}'")
|
||||||
|
|
||||||
|
if enum == RowType.CONN_BY_IP:
|
||||||
|
connect_by_ip(enum, response)
|
||||||
|
return
|
||||||
|
|
||||||
|
if enum == RowType.CONN_BY_ID:
|
||||||
|
connect_by_id(enum, response, key)
|
||||||
|
return
|
||||||
|
|
||||||
show_wait_dialog = True
|
show_wait_dialog = True
|
||||||
wait_msg = "Working"
|
wait_msg = "Working"
|
||||||
call_on_thread(
|
call_on_thread(
|
||||||
@ -1107,6 +1264,8 @@ class OuterWindow(Gtk.Window):
|
|||||||
w, h = res["width"], res["height"]
|
w, h = res["width"], res["height"]
|
||||||
logger.info(f"Restoring window size to {w},{h}")
|
logger.info(f"Restoring window size to {w},{h}")
|
||||||
self.set_default_size(w, h)
|
self.set_default_size(w, h)
|
||||||
|
else:
|
||||||
|
self.set_default_size(1400, 800)
|
||||||
|
|
||||||
def _on_delete_event(
|
def _on_delete_event(
|
||||||
self, window: "OuterWindow", event: Gdk.EventKey
|
self, window: "OuterWindow", event: Gdk.EventKey
|
||||||
@ -1650,6 +1809,7 @@ class TreeView(Gtk.TreeView):
|
|||||||
self.view = WindowContext.MAIN_MENU
|
self.view = WindowContext.MAIN_MENU
|
||||||
self.page = WindowContext.MAIN_MENU
|
self.page = WindowContext.MAIN_MENU
|
||||||
self.subpage = None
|
self.subpage = None
|
||||||
|
self.sel_blocked = False
|
||||||
|
|
||||||
self.set_fixed_height_mode(True)
|
self.set_fixed_height_mode(True)
|
||||||
|
|
||||||
@ -1783,7 +1943,7 @@ class TreeView(Gtk.TreeView):
|
|||||||
it = self.get_current_iter()
|
it = self.get_current_iter()
|
||||||
name = model.get_value(it, 0)
|
name = model.get_value(it, 0)
|
||||||
record = self.get_record_dict()
|
record = self.get_record_dict()
|
||||||
DetailsDialog(name, record["ip"], record["qport"])
|
DetailsDialog(name, record.ip, record.qport)
|
||||||
|
|
||||||
def show_mods(self) -> None:
|
def show_mods(self) -> None:
|
||||||
record = self.get_record_string()
|
record = self.get_record_string()
|
||||||
@ -1970,7 +2130,7 @@ class TreeView(Gtk.TreeView):
|
|||||||
if not record:
|
if not record:
|
||||||
grid.statusbar.update_server_meta()
|
grid.statusbar.update_server_meta()
|
||||||
return
|
return
|
||||||
ip = record["ip"]
|
ip = record.ip
|
||||||
if ip in cache:
|
if ip in cache:
|
||||||
km = cache[ip]
|
km = cache[ip]
|
||||||
grid.statusbar.append_distance(km)
|
grid.statusbar.append_distance(km)
|
||||||
@ -2038,6 +2198,7 @@ class TreeView(Gtk.TreeView):
|
|||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
if is_navkey(event.keyval):
|
if is_navkey(event.keyval):
|
||||||
|
if self.sel_blocked is False:
|
||||||
suppress_signal(
|
suppress_signal(
|
||||||
App.treeview,
|
App.treeview,
|
||||||
App.treeview.selected_row,
|
App.treeview.selected_row,
|
||||||
@ -2078,6 +2239,7 @@ class TreeView(Gtk.TreeView):
|
|||||||
Suppresses spamming on keydown
|
Suppresses spamming on keydown
|
||||||
"""
|
"""
|
||||||
if is_navkey(event.keyval):
|
if is_navkey(event.keyval):
|
||||||
|
if self.sel_blocked is True:
|
||||||
suppress_signal(
|
suppress_signal(
|
||||||
App.treeview,
|
App.treeview,
|
||||||
App.treeview.selected_row,
|
App.treeview.selected_row,
|
||||||
@ -2127,8 +2289,8 @@ class TreeView(Gtk.TreeView):
|
|||||||
addr = model[path][7]
|
addr = model[path][7]
|
||||||
qport = model[path][8]
|
qport = model[path][8]
|
||||||
ip = addr.split(":")[0]
|
ip = addr.split(":")[0]
|
||||||
qport = str(qport)
|
gameport = int(addr.split(":")[1])
|
||||||
return {"ip": ip, "qport": qport}
|
return Record(ip, gameport, qport)
|
||||||
|
|
||||||
def update_players(self, players: int) -> None:
|
def update_players(self, players: int) -> None:
|
||||||
model = self.get_model()
|
model = self.get_model()
|
||||||
@ -2175,9 +2337,7 @@ class TreeView(Gtk.TreeView):
|
|||||||
record = self.get_record_dict()
|
record = self.get_record_dict()
|
||||||
if not record:
|
if not record:
|
||||||
return
|
return
|
||||||
ip = record["ip"]
|
data = call_out("get_player_count", record.ip, str(record.qport))
|
||||||
qport = record["qport"]
|
|
||||||
data = call_out("get_player_count", ip, qport)
|
|
||||||
if data.returncode == 1:
|
if data.returncode == 1:
|
||||||
wait_dialog.destroy()
|
wait_dialog.destroy()
|
||||||
return
|
return
|
||||||
@ -2187,10 +2347,13 @@ class TreeView(Gtk.TreeView):
|
|||||||
key = query_config("steam_api")[0]
|
key = query_config("steam_api")[0]
|
||||||
job = Servers.query_api
|
job = Servers.query_api
|
||||||
params = Servers.params
|
params = Servers.params
|
||||||
with ThreadPoolExecutor() as executor:
|
|
||||||
futures = [executor.submit(job, key, param) for param in params]
|
|
||||||
wait(futures)
|
|
||||||
serv = []
|
serv = []
|
||||||
|
with ThreadPoolExecutor() as executor:
|
||||||
|
futures = [
|
||||||
|
executor.submit(job, key, APPID_DAYZ, param)
|
||||||
|
for param in params
|
||||||
|
]
|
||||||
|
wait(futures)
|
||||||
for future in futures:
|
for future in futures:
|
||||||
res = future.result()
|
res = future.result()
|
||||||
if res.status != 200 or not res.parsed:
|
if res.status != 200 or not res.parsed:
|
||||||
@ -2200,6 +2363,12 @@ class TreeView(Gtk.TreeView):
|
|||||||
return
|
return
|
||||||
j = res.json
|
j = res.json
|
||||||
serv += j["response"]["servers"]
|
serv += j["response"]["servers"]
|
||||||
|
|
||||||
|
res = Servers.query_api(key, APPID_DAYZ_EXP, "")
|
||||||
|
if res.status == 200 and res.parsed is True:
|
||||||
|
j = res.json
|
||||||
|
serv += j["response"]["servers"]
|
||||||
|
|
||||||
parsed = Servers.parse_json(serv)
|
parsed = Servers.parse_json(serv)
|
||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
@ -2554,6 +2723,10 @@ class TreeView(Gtk.TreeView):
|
|||||||
column.set_cell_data_func(
|
column.set_cell_data_func(
|
||||||
renderer, self._format_float, func_data=None
|
renderer, self._format_float, func_data=None
|
||||||
)
|
)
|
||||||
|
if column_title == "Mod":
|
||||||
|
column.set_fixed_width(500)
|
||||||
|
else:
|
||||||
|
column.set_fixed_width(150)
|
||||||
else:
|
else:
|
||||||
# WindowContext.TABLE_LOG uses undecorated columns
|
# WindowContext.TABLE_LOG uses undecorated columns
|
||||||
column = Gtk.TreeViewColumn(column_title, renderer, text=i)
|
column = Gtk.TreeViewColumn(column_title, renderer, text=i)
|
||||||
@ -2637,27 +2810,15 @@ class TreeView(Gtk.TreeView):
|
|||||||
)
|
)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
def _background_connection(
|
def dialog_hide(self) -> None:
|
||||||
self, dialog: "GenericDialog", record: str
|
if hasattr(self, "wait_dialog"):
|
||||||
) -> None:
|
self.wait_dialog.destroy()
|
||||||
def load():
|
|
||||||
dialog.destroy()
|
|
||||||
out = proc.stdout.splitlines()
|
|
||||||
msg = out[-1]
|
|
||||||
process_shell_return_code(msg, proc.returncode, record)
|
|
||||||
|
|
||||||
proc = call_out("Connect from table", record)
|
def dialog_show(self, msg: str) -> None:
|
||||||
GLib.idle_add(load)
|
if hasattr(self, "wait_dialog"):
|
||||||
|
self.wait_dialog.destroy()
|
||||||
def _attempt_connection(self) -> None:
|
self.wait_dialog = GenericDialog(msg, Popup.WAIT)
|
||||||
record = self.get_record_string()
|
self.wait_dialog.show_all()
|
||||||
msg = "Querying server and aligning mods"
|
|
||||||
wait_dialog = GenericDialog(msg, Popup.WAIT)
|
|
||||||
wait_dialog.show_all()
|
|
||||||
thread = threading.Thread(
|
|
||||||
target=self._background_connection, args=(wait_dialog, record)
|
|
||||||
)
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
def is_row_to_server_context(self, view: RowType) -> bool:
|
def is_row_to_server_context(self, view: RowType) -> bool:
|
||||||
"""Row activation that jumps into a server table"""
|
"""Row activation that jumps into a server table"""
|
||||||
@ -2697,6 +2858,117 @@ class TreeView(Gtk.TreeView):
|
|||||||
def get_view(self):
|
def get_view(self):
|
||||||
return self.view
|
return self.view
|
||||||
|
|
||||||
|
def prepare_connection(
|
||||||
|
self, record: Record
|
||||||
|
) -> subprocess.CompletedProcess | None:
|
||||||
|
"""
|
||||||
|
Always called on a thread with a dialog on the transient parent window
|
||||||
|
"""
|
||||||
|
prereqs = Servers.get_prereqs(record.ip, record.qport)
|
||||||
|
if prereqs.appid is None:
|
||||||
|
logger.warning(f"Query to '{record.ip}:{record.qport}' timed out")
|
||||||
|
msg = "Timed out when querying server, check IP or try again later"
|
||||||
|
spawn_dialog(msg, Popup.NOTIFY)
|
||||||
|
return None
|
||||||
|
|
||||||
|
build = "DayZ" if prereqs.appid == APPID_DAYZ else "DayZ Experimental"
|
||||||
|
steam_path = query_config("default_steam_path")[0]
|
||||||
|
|
||||||
|
if len(steam_path) < 1:
|
||||||
|
logger.critical(
|
||||||
|
"Config file has no value set for 'default_steam_path'"
|
||||||
|
)
|
||||||
|
msg = f"Local Steam installation is not set, possibly malformed config file."
|
||||||
|
spawn_dialog(msg, Popup.NOTIFY)
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
pefile_path = PeFile.get_pefile_path(steam_path, prereqs.appid)
|
||||||
|
except AppNotInstalledError:
|
||||||
|
logger.critical(
|
||||||
|
f"'{prereqs.appid}' not found in user's libraryfolders"
|
||||||
|
)
|
||||||
|
msg = (
|
||||||
|
f"This server is running {build}. You can install "
|
||||||
|
f"{build} by searching for it in your Steam library. "
|
||||||
|
f"If you recently installed {build} or moved it to a different drive, "
|
||||||
|
"restart Steam to allow these changes to synchronize, then try again."
|
||||||
|
)
|
||||||
|
spawn_dialog(msg, Popup.NOTIFY)
|
||||||
|
return None
|
||||||
|
except AppMovedError:
|
||||||
|
logger.critical(
|
||||||
|
f"Library folder synch error for '{prereqs.appid}'"
|
||||||
|
)
|
||||||
|
msg = (
|
||||||
|
f"Steam is reporting that {build} is installed at a non-existent location. "
|
||||||
|
f"If you recently installed {build} or moved it to a different drive, "
|
||||||
|
"restart Steam to allow these changes to synchronize, then try again."
|
||||||
|
)
|
||||||
|
spawn_dialog(msg, Popup.NOTIFY)
|
||||||
|
return None
|
||||||
|
except (VDFLoadError, PeFileError, Exception) as e:
|
||||||
|
logger.critical(e)
|
||||||
|
msg = "Steam settings or DayZ installation may be corrupted. Try restarting Steam."
|
||||||
|
spawn_dialog(msg, Popup.NOTIFY)
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
local_vers = PeFile.get_dayz_version(pefile_path)
|
||||||
|
except (PeFileError, Exception) as e:
|
||||||
|
"""
|
||||||
|
Currently permissive; file exists, but was unparseable.
|
||||||
|
"""
|
||||||
|
logger.warning(f"Failed to parse PE file: {e}")
|
||||||
|
local_vers = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
remote_vers = PeFile.dayz_version_from_str(prereqs.version)
|
||||||
|
except Exception:
|
||||||
|
remote_vers = None
|
||||||
|
|
||||||
|
if local_vers is not None and remote_vers is not None:
|
||||||
|
match = PeFile.compare_versions(local_vers, remote_vers)
|
||||||
|
|
||||||
|
match match:
|
||||||
|
case VersionMatch.LOCAL_OLDER:
|
||||||
|
msg = (
|
||||||
|
f"This server is running a newer build ({prereqs.version}) of {build} than "
|
||||||
|
f"your local version. You may be unable to connect. Proceed anyway?"
|
||||||
|
)
|
||||||
|
res = spawn_dialog(msg, Popup.CONFIRM)
|
||||||
|
if res is True:
|
||||||
|
return None
|
||||||
|
case VersionMatch.LOCAL_NEWER:
|
||||||
|
msg = (
|
||||||
|
f"This server is running an out-of-date build ({prereqs.version}) of {build}. "
|
||||||
|
"You may be unable to connect. Proceed anyway?"
|
||||||
|
)
|
||||||
|
res = spawn_dialog(msg, Popup.CONFIRM)
|
||||||
|
if res is True:
|
||||||
|
return None
|
||||||
|
case VersionMatch.SAME_VERSION:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if prereqs.password is True:
|
||||||
|
msg = (
|
||||||
|
"This server is password-protected and you will be "
|
||||||
|
"prompted when connecting. Do you want to proceed?"
|
||||||
|
)
|
||||||
|
res = spawn_dialog(msg, Popup.CONFIRM)
|
||||||
|
if res is True:
|
||||||
|
return None
|
||||||
|
|
||||||
|
"""
|
||||||
|
When using RowType.CONN_BY_IP, the gameport needs to be interpolated
|
||||||
|
"""
|
||||||
|
record.gameport = prereqs.gameport
|
||||||
|
addr = record_to_str(record)
|
||||||
|
proc = call_out(
|
||||||
|
"try_connect", addr, str(prereqs.appid), str(pefile_path)
|
||||||
|
)
|
||||||
|
return proc
|
||||||
|
|
||||||
@signal_emission
|
@signal_emission
|
||||||
@update_window_labels
|
@update_window_labels
|
||||||
def _on_row_activated(
|
def _on_row_activated(
|
||||||
@ -2765,15 +3037,13 @@ class TreeView(Gtk.TreeView):
|
|||||||
record = self.get_record_dict()
|
record = self.get_record_dict()
|
||||||
if record is None:
|
if record is None:
|
||||||
return
|
return
|
||||||
if Servers.is_passworded(record["ip"], int(record["qport"])):
|
thread_new_with_dialog(
|
||||||
msg = (
|
self.prepare_connection,
|
||||||
"This server is password-protected and you will be "
|
parse_shell_output,
|
||||||
"prompted when connecting. Do you want to proceed?"
|
"Querying server",
|
||||||
|
None,
|
||||||
|
[record],
|
||||||
)
|
)
|
||||||
res = spawn_dialog(msg, Popup.CONFIRM)
|
|
||||||
if res is True:
|
|
||||||
return
|
|
||||||
self._attempt_connection()
|
|
||||||
case _: # any other non-server option from the main menu
|
case _: # any other non-server option from the main menu
|
||||||
process_tree_option(output)
|
process_tree_option(output)
|
||||||
|
|
||||||
@ -3030,7 +3300,7 @@ class LanDialog(Gtk.MessageDialog):
|
|||||||
|
|
||||||
|
|
||||||
class DetailsDialog(GenericDialog):
|
class DetailsDialog(GenericDialog):
|
||||||
def __init__(self, server_name: str, ip: str, qport: str):
|
def __init__(self, server_name: str, ip: str, qport: int):
|
||||||
super().__init__(server_name, Popup.DETAILS)
|
super().__init__(server_name, Popup.DETAILS)
|
||||||
|
|
||||||
dialog_box = self.get_content_area()
|
dialog_box = self.get_content_area()
|
||||||
@ -3038,7 +3308,7 @@ class DetailsDialog(GenericDialog):
|
|||||||
self.set_size_request(800, 700)
|
self.set_size_request(800, 700)
|
||||||
|
|
||||||
self.ip = ip.split(":")[0]
|
self.ip = ip.split(":")[0]
|
||||||
self.qport = int(qport)
|
self.qport = qport
|
||||||
self.store = Gtk.ListStore(str, str, Pango.Weight)
|
self.store = Gtk.ListStore(str, str, Pango.Weight)
|
||||||
|
|
||||||
self.view = Gtk.TreeView(
|
self.view = Gtk.TreeView(
|
||||||
@ -3121,11 +3391,22 @@ class DetailsDialog(GenericDialog):
|
|||||||
for row in response.data:
|
for row in response.data:
|
||||||
self.store.append(row + [Pango.Weight.BOLD])
|
self.store.append(row + [Pango.Weight.BOLD])
|
||||||
self.view.set_model(self.store)
|
self.view.set_model(self.store)
|
||||||
self.description.set_text(response.description)
|
|
||||||
|
text = response.description
|
||||||
|
reg = r"\s(www\.*?)"
|
||||||
|
text = re.sub(reg, " http://" + r"\1", text)
|
||||||
|
reg2 = r"(http.*?)([ ,\r\n]|$)"
|
||||||
|
text = re.sub(reg2, comp(r"\1") + r"\2", text)
|
||||||
|
|
||||||
|
self.description.set_markup(text)
|
||||||
self.success = response.success
|
self.success = response.success
|
||||||
GLib.idle_add(self._load)
|
GLib.idle_add(self._load)
|
||||||
|
|
||||||
|
|
||||||
|
def comp(string):
|
||||||
|
return f'<a href="{string}">{string}</a>'
|
||||||
|
|
||||||
|
|
||||||
class ModDialog(GenericDialog):
|
class ModDialog(GenericDialog):
|
||||||
def __init__(self, record: str):
|
def __init__(self, record: str):
|
||||||
msg = "Enter/double click a row to open in Steam Workshop."
|
msg = "Enter/double click a row to open in Steam Workshop."
|
||||||
@ -3150,7 +3431,13 @@ class ModDialog(GenericDialog):
|
|||||||
column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
|
column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
|
||||||
self.view.append_column(column)
|
self.view.append_column(column)
|
||||||
column.set_sort_column_id(i)
|
column.set_sort_column_id(i)
|
||||||
|
match column_title:
|
||||||
|
case "Mod":
|
||||||
column.set_fixed_width(350)
|
column.set_fixed_width(350)
|
||||||
|
case "ID":
|
||||||
|
column.set_fixed_width(200)
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
dialogBox.pack_end(self.scrollable, True, True, 0)
|
dialogBox.pack_end(self.scrollable, True, True, 0)
|
||||||
|
|
||||||
wait_dialog = GenericDialog("Fetching modlist", Popup.WAIT)
|
wait_dialog = GenericDialog("Fetching modlist", Popup.WAIT)
|
||||||
@ -3174,12 +3461,10 @@ class ModDialog(GenericDialog):
|
|||||||
self.run()
|
self.run()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
addr = App.treeview.get_record_dict()
|
record = App.treeview.get_record_dict()
|
||||||
if not addr:
|
if not record:
|
||||||
return
|
return
|
||||||
ip = addr["ip"]
|
data = call_out("show_server_modlist", record.ip, str(record.qport))
|
||||||
qport = addr["qport"]
|
|
||||||
data = call_out("show_server_modlist", ip, qport)
|
|
||||||
mod_count = self._parse_modlist_rows(data)
|
mod_count = self._parse_modlist_rows(data)
|
||||||
self.view.set_model(modlist_store)
|
self.view.set_model(modlist_store)
|
||||||
GLib.idle_add(_load)
|
GLib.idle_add(_load)
|
||||||
@ -3458,6 +3743,9 @@ class Options(Gtk.Box):
|
|||||||
[LeftLabel("Force update local mods"), self.force_button, eb2],
|
[LeftLabel("Force update local mods"), self.force_button, eb2],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
self.dayz_version_label = Gtk.Label(label="-")
|
||||||
|
self.dayz_exp_version_label = Gtk.Label(label="-")
|
||||||
|
|
||||||
self.branch_combo = Gtk.ComboBoxText()
|
self.branch_combo = Gtk.ComboBoxText()
|
||||||
self.branch_combo.append_text("Stable")
|
self.branch_combo.append_text("Stable")
|
||||||
self.branch_combo.append_text("Testing")
|
self.branch_combo.append_text("Testing")
|
||||||
@ -3470,7 +3758,11 @@ class Options(Gtk.Box):
|
|||||||
)
|
)
|
||||||
eb = InfoEventBox(msg)
|
eb = InfoEventBox(msg)
|
||||||
|
|
||||||
version_rows = [[LeftLabel("Branch"), self.branch_combo, eb]]
|
version_rows = [
|
||||||
|
[LeftLabel("DayZ"), self.dayz_version_label],
|
||||||
|
[LeftLabel("DayZ Experimental"), self.dayz_exp_version_label],
|
||||||
|
[LeftLabel("DZGUI branch"), self.branch_combo, eb],
|
||||||
|
]
|
||||||
|
|
||||||
api_grid = self._make_grid(api_rows)
|
api_grid = self._make_grid(api_rows)
|
||||||
prefs_grid = self._make_grid(pref_rows)
|
prefs_grid = self._make_grid(pref_rows)
|
||||||
@ -3714,6 +4006,7 @@ class Options(Gtk.Box):
|
|||||||
name = config_vals[3]
|
name = config_vals[3]
|
||||||
client = config_vals[5]
|
client = config_vals[5]
|
||||||
fullscreen = config_vals[6]
|
fullscreen = config_vals[6]
|
||||||
|
default_steam_path = config_vals[7]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
steam = query_config("steam_api")[0]
|
steam = query_config("steam_api")[0]
|
||||||
@ -3756,6 +4049,27 @@ class Options(Gtk.Box):
|
|||||||
if field[0] == "":
|
if field[0] == "":
|
||||||
field[1].get_children()[1].set_sensitive(False)
|
field[1].get_children()[1].set_sensitive(False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
pe_file_path = PeFile.get_pefile_path(
|
||||||
|
default_steam_path, APPID_DAYZ
|
||||||
|
)
|
||||||
|
vers = PeFile.get_dayz_version(pe_file_path)
|
||||||
|
dayz_version = PeFile.dayz_version_to_str(vers)
|
||||||
|
except Exception:
|
||||||
|
dayz_version = "-"
|
||||||
|
|
||||||
|
try:
|
||||||
|
exp_file_path = PeFile.get_pefile_path(
|
||||||
|
default_steam_path, APPID_DAYZ_EXP
|
||||||
|
)
|
||||||
|
vers = PeFile.get_dayz_version(exp_file_path)
|
||||||
|
dayz_exp_version = PeFile.dayz_version_to_str(vers)
|
||||||
|
except Exception:
|
||||||
|
dayz_exp_version = "-"
|
||||||
|
|
||||||
|
self.dayz_version_label.set_text(dayz_version)
|
||||||
|
self.dayz_exp_version_label.set_text(dayz_exp_version)
|
||||||
|
|
||||||
if branch == "testing":
|
if branch == "testing":
|
||||||
self.branch_combo.set_active(1)
|
self.branch_combo.set_active(1)
|
||||||
else:
|
else:
|
||||||
@ -4011,8 +4325,8 @@ class Notebook(Gtk.Notebook):
|
|||||||
if hasattr(page, "steam_entry"):
|
if hasattr(page, "steam_entry"):
|
||||||
"""
|
"""
|
||||||
Gtk.Notebook focuses the first input field when changing pages;
|
Gtk.Notebook focuses the first input field when changing pages;
|
||||||
this workaround unhighlights the selected region and makes entry fields
|
this workaround unhighlights the selected region and makes entry
|
||||||
unfocusable prior to the page 'switch-page' signal,
|
fields unfocusable prior to the page 'switch-page' signal,
|
||||||
then makes them focusable again
|
then makes them focusable again
|
||||||
"""
|
"""
|
||||||
entries = page.steam_entry, page.bm_entry
|
entries = page.steam_entry, page.bm_entry
|
||||||
@ -4265,9 +4579,8 @@ class ModSelectionPanel(Gtk.Box):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Highlight stale",
|
"label": "Highlight stale",
|
||||||
"tooltip": """Shows locally-installed mods
|
"tooltip": "Shows locally-installed mods which are not\n"
|
||||||
which are not used by any server
|
"used by any server in your Saved Servers",
|
||||||
in your Saved Servers""",
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user