Compare commits

...
15 Commits
Author SHA1 Message Date
aclist 252f07cbec Merge pull request #287 from aclist/prerelease/6.0.6-beta.1
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
chore: update symlink hash logic
2026-05-17 23:42:18 +09:00
aclist a0f610ead7 chore: update symlink hash logic 2026-05-17 23:40:35 +09:00
aclist ca4bcfc1a5 Merge pull request #285 from aclist/fix/server-mods-only
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
change: only update mods local to server
2026-04-17 20:57:03 +09:00
aclist ac4892d4f0 change: only update mods local to server 2026-04-17 20:47:25 +09:00
aclist 240b660923 Merge pull request #284 from aclist/prerelease/6.0.5-beta.1
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: update module checksums
2026-04-13 16:56:02 +09:00
aclist 150dfbca72 fix: update module checksums 2026-04-13 16:54:46 +09:00
aclist 4b78114174 Merge pull request #281 from aclist/fix/glib-unix-allversions
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
fix: use conditional logic when testing GLibUnix
2026-04-13 04:38:50 +09:00
aclist d1bde9901d fix: use conditional logic when testing GLibUnix 2026-04-13 03:43:22 +09:00
aclist c5eba364d9 Merge pull request #278 from aclist/fix/glib-unix
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: use GLibUnix namespace
2026-04-10 19:03:06 +09:00
aclist 5460b53c26 fix: version number in changelog 2026-04-10 18:52:04 +09:00
aclist 99301b3312 fix: uncomment functions 2026-04-10 18:50:58 +09:00
aclist 8d30af9dee fix: use GLibUnix namespace 2026-04-10 16:50:31 +09:00
aclist 8a8109b7df chore: add preview image
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-03-30 20:56:16 +09:00
aclist e4eaf25c8f chore: update readme 2026-03-30 20:16:58 +09:00
aclist 0daf6089b9 fix: raise error correctly
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-01-26 16:53:42 +09:00
8 changed files with 287 additions and 346 deletions

No files matched your search

+3
View File
@@ -1 +1,4 @@
tags tags
__pycache__
pre-commit
tests/
+30 -3
View File
@@ -1,5 +1,32 @@
# Changelog # Changelog
## [6.0.6-beta.1] 2026-05-17
## Changed
- Update symlink hash logic to align with DZGUI 7 beta
- Update A2S helper to latest commit
## [6.0.5-beta.2] 2026-04-17
## Changed
- During server connection attempts, only download mods local to specific server
## [6.0.5-beta.1] 2026-04-13
## Fixed
- Update A2S module checksums to support servers with missing description field
## Changed
- Bump version to reflect hotfixes on stable branch
## [6.0.2-beta.5] 2026-04-12
## Fixed
- Use conditional logic when testing GLibUnix namespaces
## [6.0.2-beta.4] 2026-04-10
## Fixed
- Use GLibUnix namespace when setting up SIGINT handling
## [6.0.2-beta.3] 2026-01-26
## Fixed
- Raise error correctly from module
## [6.0.2-beta.2] 2026-01-18 ## [6.0.2-beta.2] 2026-01-18
## Fixed ## Fixed
- Helpers not being downloaded (uncommented boilerplate) - Helpers not being downloaded (uncommented boilerplate)
@@ -589,7 +616,7 @@ As part of this change, version 4.0.0 introduces the ability to connect to or ad
If you encounter any problems with this new release or with the migration of configs, please do not hesitate to submit a bug report. If you encounter any problems with this new release or with the migration of configs, please do not hesitate to submit a bug report.
Attention Fedora 38 users: problems with upstream GNOME packages causing crashes have been reported to GNOME development and a fix has been issued. You have the choice of compiling the zenity package Attention Fedora 38 users: problems with upstream GNOME packages causing crashes have been reported to GNOME development and a fix has been issued. You have the choice of compiling the zenity package
from source or waiting until the latest version is merged into Fedora's package manager. from source or waiting until the latest version is merged into Fedora's package manager.
### Added ### Added
@@ -614,7 +641,7 @@ from source or waiting until the latest version is merged into Fedora's package
- Store complete IP:Port instead of server IDs - Store complete IP:Port instead of server IDs
- Make Battlemetrics API key optional: this is only used for the 'Connect by ID' and 'Add server by ID' methods and is not required. If you prefer, you can simply connect/add by IP. - Make Battlemetrics API key optional: this is only used for the 'Connect by ID' and 'Add server by ID' methods and is not required. If you prefer, you can simply connect/add by IP.
- Prevent the application from launching in Game Mode on Steam Deck: Steam Deck's kiosk mode has problems sending keyboard input to third party applications. To prevent unintended usage, DZGUI now warns the user to launch the app in Desktop Mode if they attempt to use it from Game Mode. Adding DZGUI as a Non-Steam Game does work on desktop PCs, but is not recommended due to the way Steam handles subshells. For best results, launch DZGUI directly via the script/applications menu (PC) or via the desktop icon (Steam Deck). - Prevent the application from launching in Game Mode on Steam Deck: Steam Deck's kiosk mode has problems sending keyboard input to third party applications. To prevent unintended usage, DZGUI now warns the user to launch the app in Desktop Mode if they attempt to use it from Game Mode. Adding DZGUI as a Non-Steam Game does work on desktop PCs, but is not recommended due to the way Steam handles subshells. For best results, launch DZGUI directly via the script/applications menu (PC) or via the desktop icon (Steam Deck).
- Omit null servers from list: servers that time out or send an empty response are now omitted entirely from the My Servers list, as they will not return meaningful metadata unless they are online. - Omit null servers from list: servers that time out or send an empty response are now omitted entirely from the My Servers list, as they will not return meaningful metadata unless they are online.
The My Servers list thus shows online and accessible servers The My Servers list thus shows online and accessible servers
## [3.3.0] 2023-05-16 ## [3.3.0] 2023-05-16
@@ -633,7 +660,7 @@ from source or waiting until the latest version is merged into Fedora's package
- First-time setup: sudo escalation when checking system map count for the first time - First-time setup: sudo escalation when checking system map count for the first time
### Fixed ### Fixed
- Steam Deck: non-ASCII delimiter causing setup menu to despawn on some devices - Steam Deck: non-ASCII delimiter causing setup menu to despawn on some devices
- Don't add items in My Servers multiple times to array when the list of favorites is paginated - Don't add items in My Servers multiple times to array when the list of favorites is paginated
- Trigger progress dialogs sooner and in sequence to reduce appearance of visual lag - Trigger progress dialogs sooner and in sequence to reduce appearance of visual lag
- First-time setup: break out of dialogs correctly when user backs out - First-time setup: break out of dialogs correctly when user backs out
+9 -1
View File
@@ -11,6 +11,15 @@ This overcomes certain limitations in the Linux client and helps prepare the gam
- Concatenate launch options to pass to Steam - Concatenate launch options to pass to Steam
- Connect to mod-enabled LAN servers - Connect to mod-enabled LAN servers
## Design philosophy
DZGUI follows a specific set of guidelines, enumerated below:
- Provide a free and open-source tool under GPL
- Do not use undocumented, third-party servers to aggregate data; no central "DZGUI" server
- All connections go to Steam and DayZ servers directly; the tool should function in perpetuity without intermediary endpoints
- Target Linux desktop and Steam deck specifically; not a cross-platform tool
## Setup and usage ## Setup and usage
Refer to the documentation for installation and setup instructions: Refer to the documentation for installation and setup instructions:
@@ -18,7 +27,6 @@ Refer to the documentation for installation and setup instructions:
- [GitHub](https://aclist.github.io/dzgui/index.html) - [GitHub](https://aclist.github.io/dzgui/index.html)
- [Mirror (Codeberg)](https://aclist.codeberg.page) - [Mirror (Codeberg)](https://aclist.codeberg.page)
![A screenshot of DZGUI](/images/example.png) ![A screenshot of DZGUI](/images/example.png)
## Attribution ## Attribution
+13 -10
View File
@@ -2,7 +2,7 @@
set -o pipefail set -o pipefail
src_path="$(readlink -e "$0")" src_path="$(readlink -e "$0")"
version=6.0.2-beta.2 version=6.0.6-beta.1
#CONSTANTS #CONSTANTS
aid=221100 aid=221100
@@ -386,7 +386,7 @@ prompt_dl(){
Your branch: $branch Your branch: $branch
Your version: $version Your version: $version
Upstream version: $upstream Upstream version: $upstream
Version updates introduce important bug fixes and are encouraged. Attempt to download the latest version? Version updates introduce important bug fixes and are encouraged. Attempt to download the latest version?
EOF EOF
} }
@@ -567,9 +567,12 @@ get_hash(){
md5sum "$1" | awk '{print $1}' md5sum "$1" | awk '{print $1}'
} }
fetch_a2s(){ fetch_a2s(){
# this file is currently monolithic sum="d26da2fe03df2b95436f823f0496887b"
[[ -d $helpers_path/a2s ]] && { logger INFO "A2S helper is current"; return 0; } if [[ -d $helpers_path/a2s ]] && [[ $(get_hash "$helpers_path/a2s/info.py") == $sum ]]; then
local sha=c7590ffa9a6d0c6912e17ceeab15b832a1090640 logger INFO "A2S helper is current"
return 0
fi
local sha=b40eb24cdbb06ebd08272f224257fe5a81610e86
local author="yepoleb" local author="yepoleb"
local repo="python-a2s" local repo="python-a2s"
local url="https://github.com/$author/$repo/tarball/$sha" local url="https://github.com/$author/$repo/tarball/$sha"
@@ -583,13 +586,13 @@ fetch_a2s(){
logger INFO "Updated A2S helper to sha '$sha'" logger INFO "Updated A2S helper to sha '$sha'"
} }
fetch_dzq(){ fetch_dzq(){
local sum="0a334e1e144e76e560419d155435c91e" local sum="0d316badbb8d943c5f078e73862cac3e"
local file="$helpers_path/a2s/dayzquery.py" local file="$helpers_path/a2s/dayzquery.py"
if [[ -f $file ]] && [[ $(get_hash "$file") == $sum ]]; then if [[ -f $file ]] && [[ $(get_hash "$file") == $sum ]]; then
logger INFO "DZQ is current" logger INFO "DZQ is current"
return 0 return 0
fi fi
local sha=a22a9f428cbe075d7dda62f78000296955eea92a local sha="07483b88ed096327ebca752f5e177011b604d7fe"
local author="yepoleb" local author="yepoleb"
local repo="dayzquery" local repo="dayzquery"
local url="https://raw.githubusercontent.com/$author/$repo/$sha/dayzquery.py" local url="https://raw.githubusercontent.com/$author/$repo/$sha/dayzquery.py"
@@ -626,12 +629,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"]="69e83db00cbc7674e241565c88aed83d" ["funcs"]="1cd26343cd68a5382e3377a9757497fa"
["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067"
["servers.py"]="ed442c3aecf33f777d59dcf53650d263" ["servers.py"]="ed442c3aecf33f777d59dcf53650d263"
["ui.py"]="d09b9d8bed3854efd51377289786c5ac" ["ui.py"]="cf52a3d5883afe02a013d8673df494fa"
["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397"
["pefile.py"]="21531f2c0d9dfa5f110cf6779f9d22c0" ["pefile.py"]="b452974a84bff1d821872fcebf59e380"
) )
local author="aclist" local author="aclist"
local repo="dztui" local repo="dztui"
+145 -147
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -o pipefail set -o pipefail
version="6.0.2-beta.1" version="6.0.5-beta.2"
#CONSTANTS #CONSTANTS
aid=221100 aid=221100
@@ -690,6 +690,7 @@ toggle(){
auto_install="1" auto_install="1"
else else
auto_install="" auto_install=""
rm "$versions_file"
fi fi
;; ;;
Toggle[[:space:]]debug[[:space:]]mode) Toggle[[:space:]]debug[[:space:]]mode)
@@ -929,13 +930,19 @@ query_defunct(){
<<< "$result" awk '{print $2}' <<< "$result" awk '{print $2}'
} }
encode(){ encode(){
echo "$1" | md5sum | cut -c -8 echo -n "$1" | md5sum | cut -c -8
} }
compare(){ compare(){
local modlist="$@" local modlist="$@"
diff=$(comm -23 <(<<< "$modlist" sort -u) <(installed_mods | sort)) diff=$(comm -23 <(<<< "$modlist" sort -u) <(installed_mods | sort))
echo "$diff" echo "$diff"
} }
unlink_all(){
readarray -t links < <(find "$game_dir" -maxdepth 1 -mindepth 1 -type l)
for link in "${links[@]}"; do
unlink "$link"
done
}
legacy_symlinks(){ legacy_symlinks(){
logger INFO "Removing legacy symlinks" logger INFO "Removing legacy symlinks"
@@ -1026,7 +1033,10 @@ update_symlinks(){
if [[ ! -d "$workshop_dir" ]]; then if [[ ! -d "$workshop_dir" ]]; then
return return
fi fi
legacy_symlinks if [[ ! -d "$game_dir" ]]; then
return
fi
unlink_all
symlinks symlinks
} }
try_fallback(){ try_fallback(){
@@ -1077,41 +1087,42 @@ try_connect(){
local appid="$2" local appid="$2"
local binary="$3" local binary="$3"
local ip=$(<<< $record awk -F: '{print $1}') IFS=":" read -ra address <<< "$record"
local gameport=$(<<< $record awk -F: '{print $2}') local ip
local qport=$(<<< $record awk -F: '{print $3}') local gameport
local qport
ip=${address[0]}
gameport=${address[1]}
qport=${address[2]}
[[ $appid -eq $exp ]] && echo "$binary" > $_cache_binary [[ $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
printf "Failed to fetch server modlist, possibly timed out" printf "Failed to fetch server modlist, possibly timed out"
return 1 return 1
fi fi
logger INFO "Server returned modlist: $(<<< $remote_mods tr '\n' ' ')" logger INFO "Server returned modlist: $(<<< "$remote_mods" tr '\n' ' ')"
local sanitized_mods=$(query_defunct "$remote_mods")
local diff=$(compare "$sanitized_mods") local sanitized_mods
sanitized_mods=$(query_defunct "$remote_mods")
logger INFO "Connection attempt for $ip:$qport" logger INFO "Connection attempt for $ip:$qport"
update_history "$record" update_history "$record"
if [[ -n $auto_install ]]; then case $auto_install in
logger INFO "Merging modlists" "") install_mods "$sanitized_mods" "manual" ;;
diff=$(merge_modlists "$diff") 1|2) install_mods "$sanitized_mods" "auto" ;;
diff=$(query_defunct "$diff") esac
fi
if [[ -n $diff ]]; then # NOTE: clean diff indicates that all requisite mods were downloaded
if [[ $(check_architecture) -eq 1 ]] && [[ $(test_display_mode) == "gm" ]]; then diff=$(compare "$sanitized_mods")
printf "Use Desktop Mode to download mods on Steam Deck" if [[ -z $diff ]]; then
return 1
fi
case $auto_install in
"") manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods" "" "$appid";;
1|2) manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods" "auto" "$appid" ;;
esac
else
launch "$ip" "$gameport" "$sanitized_mods" "$appid" launch "$ip" "$gameport" "$sanitized_mods" "$appid"
else
printf "User aborted download process, or some mods may have failed to download. Try connecting again to resync."
exit 1
fi fi
} }
check_architecture(){ check_architecture(){
@@ -1127,13 +1138,15 @@ force_update(){
printf "Only available when mod auto-install is ON" printf "Only available when mod auto-install is ON"
return 1 return 1
fi fi
# NOTE: forcibly regenerate signatures by removing file
rm "$versions_file" rm "$versions_file"
local update=$(check_timestamps) local mods
manual_mod_install "null" "null" "$update" "null" "force" "null" mods=$(ls -1 "$workshop_dir")
install_mods "$mods" "force"
echo "Finished requesting mod updates." echo "Finished requesting mod updates."
return 0 return 0
} }
get_local_stamps(){ get_timestamps(){
readarray -t modlist < <(printf "%s\n" "$@") readarray -t modlist < <(printf "%s\n" "$@")
local max="${#modlist[@]}" local max="${#modlist[@]}"
_concat(){ _concat(){
@@ -1158,67 +1171,6 @@ update_stamps(){
printf "%s\n" "${stamps[$i]}" >> $versions_file printf "%s\n" "${stamps[$i]}" >> $versions_file
done done
} }
check_timestamps(){
readarray -t local_modlist < <(ls -1 $workshop_dir)
local max=${#local_modlist[@]}
logger INFO "Local mod count: $max"
[[ $max -eq 0 ]] && return 1
local local_stamps=$(get_local_stamps "${local_modlist[@]}")
if [[ -z $local_stamps ]]; then
logger WARN "Timestamp query returned empty response"
return 1
fi
local aligned=$(<<< "$local_stamps" jq -r '.response.publishedfiledetails[]|"\(.publishedfileid),\(.time_updated)"')
readarray -t remote_ids < <(<<< "$aligned" awk -F, '{print $1}')
readarray -t remote_times < <(<<< "$aligned" awk -F, '{print $2}')
if [[ ! -f $versions_file ]]; then
logger INFO "No prior versions file found, creating"
update_stamps "$aligned"
#force refresh all mods if versions file was missing
printf "%s\n" "${remote_ids[@]}"
return 0
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 local_version
for((i = 0; i < ${#remote_ids[@]}; ++i)); do
remote_version[${remote_ids[$i]}]=${remote_times[$i]}
done
needs_update=()
for((i=0;i<${#old_ids[@]};i++)); do
local id=${old_ids[$i]}
local time=${old_times[$i]}
if [[ $time != ${remote_version[$id]} ]]; then
logger WARN "Mod '$id' timestamp '$time' != '${remote_version[$id]}'"
needs_update+=($id)
fi
done
printf "%s\n" "${needs_update[@]}"
}
merge_modlists(){
local diff="$1"
_sort(){
printf "%s\n" "$diff"
printf "%s\n" "${needs_update[@]}"
}
readarray -t needs_update < <(check_timestamps)
if [[ -z ${needs_update[@]} ]]; then
echo "$diff"
return 0
fi
if [[ -z "$diff" ]] && [[ ${#needs_update[@]} -gt 0 ]]; then
printf "%s\n" "${needs_update[@]}"
else
# remove duplicates
_sort | sort -u
fi
}
concat_mods(){ concat_mods(){
readarray -t concat_arr <<< "$@" readarray -t concat_arr <<< "$@"
local id local id
@@ -1260,7 +1212,7 @@ launch(){
if [[ $debug -eq 1 ]]; then if [[ $debug -eq 1 ]]; then
local launch_options="$steam_cmd -applaunch $appid -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: %s" "$launch_options"
return 0 return 0
fi fi
echo "$concat" > "$_cache_launch" echo "$concat" > "$_cache_launch"
@@ -1296,75 +1248,121 @@ final_handshake(){
printf "\n" printf "\n"
return 6 return 6
} }
manual_mod_install(){ watcher(){
local ip="$1" local -n to_download="$1"
local gameport="$2" local -n signatures="$2"
local diff="$3" local mode="$3"
local sanitized_mods="$4"
local mode="$5"
local appid="$6"
local ex="$state_path/dzg.watcher"
readarray -t stage_mods <<< "$diff" declare -i i=0
[[ -f $ex ]] && rm $ex local remote_time
local length
length=${#to_download[@]}
for mod in "${!to_download[@]}"; do
remote_time=${to_download[$mod]}
[[ -f $ex ]] && return 1
case $mode in
"auto" | "force")
$steam_cmd "steam://url/CommunityFilePage/${mod}+workshop_download_item ${aid} ${mod}"
echo "# Opening workshop page for ${mod}. If you see no progress, you may be out of disk space."
;;
*)
$steam_cmd "steam://url/CommunityFilePage/${mod}"
echo "# Opening workshop page for ${mod}. If you see no progress after subscribing, try unsubscribing and resubscribing again until the download commences."
;;
esac
sleep 1s
foreground
_watcher(){ until [[ -d $downloads_dir/${mod} ]]; do
for((i=0;i<${#stage_mods[@]};i++)); do
[[ -f $ex ]] && return 1 [[ -f $ex ]] && return 1
sleep 0.1s
if [[ $mode == "auto" ]] || [[ $mode == "force" ]]; then # NOTE: fallback for instantaneous downloads
$steam_cmd "steam://url/CommunityFilePage/${stage_mods[$i]}+workshop_download_item $aid ${stage_mods[$i]}" if [[ $mode == "auto" || $mode == "force" ]]; then
echo "# Opening workshop page for ${stage_mods[$i]}. If you see no progress, you may be out of disk space." if [[ -d $workshop_dir/${mod} ]]; then
else
$steam_cmd "steam://url/CommunityFilePage/${stage_mods[$i]}"
echo "# Opening workshop page for ${stage_mods[$i]}. If you see no progress after subscribing, try unsubscribing and resubscribing again until the download commences."
fi
sleep 1s
foreground
until [[ -d $downloads_dir/${stage_mods[$i]} ]]; do
[[ -f $ex ]] && return 1
sleep 0.1s
if [[ -d $workshop_dir/${stage_mods[$i]} ]]; then
break break
fi fi
done
foreground
if [[ $mode == "auto" ]] || [[ $mode == "force" ]]; then
echo "# Steam is downloading ${stage_mods[$i]} (mod $((i+1)) of ${#stage_mods[@]}). You do not need to manually Subscribe."
else
echo "# Steam is downloading ${stage_mods[$i]} (mod $((i+1)) of ${#stage_mods[@]})"
fi fi
until [[ -d $workshop_dir/${stage_mods[$i]} ]]; do
[[ -f $ex ]] && return 1
sleep 0.1s
done
foreground
echo "# ${stage_mods[$i]} moved to mods dir"
done done
echo "100"
}
_watcher > >($steamsafe_zenity --pulsate --progress --auto-close --title="DZG Watcher" --width=500 2>/dev/null; rc=$?; [[ $rc -eq 1 ]] && touch $ex)
if [[ $mode == "force" ]]; then foreground
rm "$versions_file" case $mode in
check_timestamps "auto" | "force")
return 0 echo "# Steam is downloading ${mod} (mod $((i+1)) of ${length}). You do not need to manually Subscribe."
;;
*)
echo "# Steam is downloading ${mod} (mod $((i+1)) of ${length})"
;;
esac
until [[ -d $workshop_dir/${mod} ]]; do
[[ -f $ex ]] && return 1
sleep 0.1s
done
foreground
if [[ ! -v signatures["${mod}"] ]]; then
echo "$mod,$remote_time" >> "$versions_file"
else
sed -i "s/$mod,${signatures[$mod]}/$mod,$remote_time/" "$versions_file"
fi
echo "# ${mod} moved to mods dir"
i+=1
done
echo "100"
}
install_mods(){
local sanitized_mods="$1"
local mode="$2"
local ex="$state_path/dzg.watcher"
[[ -f $ex ]] && rm "$ex"
if [[ -f "$versions_file" ]]; then
readarray -t local_mods < "$versions_file"
else
local_mods=()
fi fi
local diff=$(compare "$sanitized_mods") declare -A mod_signatures
if [[ -z $diff ]]; then local local_id
if [[ $mode == "auto" ]]; then local local_time
rm "$versions_file" for mod in "${local_mods[@]}"; do
check_timestamps IFS="," read -ra fields <<< "$mod"
local_id="${fields[0]}"
local_time="${fields[1]}"
mod_signatures[$local_id]=$local_time
done
readarray -t remote_mods <<< "$sanitized_mods"
declare -A download
local remote_timestamps
local remote_id
local remote_time
remote_timestamps=$(get_timestamps "${remote_mods[@]}")
for mod in $(<<< "$remote_timestamps" jq -r '.response.publishedfiledetails[]|"\(.publishedfileid),\(.time_updated)"'); do
IFS="," read -ra fields <<< "$mod"
remote_id="${fields[0]}"
remote_time="${fields[1]}"
# NOTE: mod is not present in versions file
if [[ ! -v mod_signatures["$remote_id"] ]]; then
download["$remote_id"]=$remote_time
continue
fi fi
launch "$ip" "$gameport" "$sanitized_mods" "$appid" # NOTE: mod timestamp is out of date
else local_time="${mod_signatures["$remote_id"]}"
printf "User aborted download process, or some mods may have failed to download. Try connecting again to resync." if [[ $remote_time != "$local_time" ]]; then
download["$remote_id"]=$remote_time
continue
fi
done
[[ ${#download[@]} -lt 1 ]] && return
if [[ $(check_architecture) -eq 1 ]] && [[ $(test_display_mode) == "gm" ]]; then
printf "Use Desktop Mode to download mods on Steam Deck"
exit 1 exit 1
fi fi
watcher download mod_signatures "$mode" > >($steamsafe_zenity --pulsate --progress --auto-close --title="DZG Watcher" --width=500 2>/dev/null; rc=$?; [[ $rc -eq 1 ]] && touch "$ex")
} }
test_display_mode(){ test_display_mode(){
pgrep -a gamescope | grep -q "generate-drm-mode" pgrep -a gamescope | grep -q "generate-drm-mode"
+1 -1
View File
@@ -295,7 +295,7 @@ def get_dayz_version(file: Path) -> DayZVersion | Exception:
try: try:
version = get_version(file) version = get_version(file)
except Exception as e: except Exception as e:
return e raise e
patch = str(version.build) + str(version.revision) patch = str(version.build) + str(version.revision)
dz_vers = DayZVersion(version.major, version.minor, int(patch)) dz_vers = DayZVersion(version.major, version.minor, int(patch))
return dz_vers return dz_vers
+86 -184
View File
@@ -38,8 +38,10 @@ if TYPE_CHECKING:
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 gi.require_version("GLibUnix", "2.0")
from gi.repository import Gtk, GLib, GLibUnix, 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)
@@ -852,9 +854,7 @@ def call_out(command: str, *args: str) -> subprocess.CompletedProcess:
arg_ar = [] arg_ar = []
for i in args: for i in args:
arg_ar.append(i) arg_ar.append(i)
logger.info( logger.info(f"Context '{name}' calling subprocess '{command}' with args '{arg_ar}'")
f"Context '{name}' calling subprocess '{command}' with args '{arg_ar}'"
)
proc = subprocess.run( proc = subprocess.run(
["/usr/bin/env", "bash", funcs, command] + arg_ar, ["/usr/bin/env", "bash", funcs, command] + arg_ar,
capture_output=True, capture_output=True,
@@ -880,9 +880,7 @@ def spawn_dialog(msg: str, mode: Popup) -> bool:
return False return False
def process_shell_return_code( def process_shell_return_code(msg: str, code: int, original_input: RowType) -> None:
msg: str, code: int, original_input: RowType
) -> None:
logger.info( logger.info(
f"Processing return code '{code}' for the input " f"Processing return code '{code}' for the input "
f"'{original_input}', returned message '{msg}'" f"'{original_input}', returned message '{msg}'"
@@ -909,9 +907,7 @@ def process_shell_return_code(
spawn_dialog("Invalid Steam API key, reverting", Popup.NOTIFY) spawn_dialog("Invalid Steam API key, reverting", Popup.NOTIFY)
App.notebook.settings.revert(Preferences.STEAM) App.notebook.settings.revert(Preferences.STEAM)
case 79: # failed settings update (bm) case 79: # failed settings update (bm)
spawn_dialog( spawn_dialog("Invalid Battlemetrics API key, reverting", Popup.NOTIFY)
"Invalid Battlemetrics API key, reverting", Popup.NOTIFY
)
App.notebook.settings.revert(Preferences.BM) App.notebook.settings.revert(Preferences.BM)
case 80: # pop toast after successful settings change case 80: # pop toast after successful settings change
config_vals.clear() config_vals.clear()
@@ -977,9 +973,7 @@ def call_on_thread(
if state: if state:
wait_dialog = GenericDialog(msg, Popup.WAIT) wait_dialog = GenericDialog(msg, Popup.WAIT)
wait_dialog.show_all() wait_dialog.show_all()
thread = threading.Thread( thread = threading.Thread(target=_background, args=(subproc, args, wait_dialog))
target=_background, args=(subproc, args, wait_dialog)
)
thread.start() thread.start()
else: else:
""" """
@@ -1004,18 +998,14 @@ def process_tree_option(choice: RowType) -> None:
record = App.treeview.get_record_string() record = App.treeview.get_record_string()
wait_msg = command.dict["wait_msg"] wait_msg = command.dict["wait_msg"]
show_wait_dialog = True show_wait_dialog = True
call_on_thread( call_on_thread(show_wait_dialog, cmd_string, wait_msg, record, choice=choice)
show_wait_dialog, cmd_string, wait_msg, record, choice=choice
)
return return
# modlist highlight stale action # modlist highlight stale action
if context == WindowContext.TABLE_MODS and command == RowType.HIGHLIGHT: if context == WindowContext.TABLE_MODS and command == RowType.HIGHLIGHT:
wait_msg = command.dict["wait_msg"] wait_msg = command.dict["wait_msg"]
show_wait_dialog = True show_wait_dialog = True
call_on_thread( call_on_thread(show_wait_dialog, cmd_string, wait_msg, "", choice=choice)
show_wait_dialog, cmd_string, wait_msg, "", choice=choice
)
return return
if command == RowType.CHANGELOG: if command == RowType.CHANGELOG:
@@ -1083,12 +1073,8 @@ def prepare_connection(rowtype: RowType, record: Record) -> None:
thread = threading.Thread(target=background, args=(rowtype, record)) thread = threading.Thread(target=background, args=(rowtype, record))
thread.start() thread.start()
def try_connect(
addr: str, def try_connect(addr: str, appid: str, path: str, row: RowType) -> None:
appid: str,
path: str,
row: RowType
) -> None:
def background(addr, appid, path): def background(addr, appid, path):
def cleanup(): def cleanup():
App.treeview.dialog_hide() App.treeview.dialog_hide()
@@ -1151,6 +1137,7 @@ def connect_by_ip(enum: RowType, response: str) -> None:
return return
prepare_connection(enum, record) prepare_connection(enum, record)
def connect_by_id(enum: RowType, response: str, key: str) -> None: def connect_by_id(enum: RowType, response: str, key: str) -> None:
def _prep(key: str, response: str) -> None: def _prep(key: str, response: str) -> None:
def cleanup() -> None: def cleanup() -> None:
@@ -1179,9 +1166,7 @@ def process_user_input(enum: RowType) -> None:
if enum == RowType.CONN_BY_ID: if enum == RowType.CONN_BY_ID:
key = query_config("api_key")[0] key = query_config("api_key")[0]
if len(key) == 0: if len(key) == 0:
spawn_dialog( spawn_dialog("No Battlemetrics API key is set; see Options", Popup.NOTIFY)
"No Battlemetrics API key is set; see Options", Popup.NOTIFY
)
return return
user_entry = EntryDialog(prompt, Popup.ENTRY, link_label) user_entry = EntryDialog(prompt, Popup.ENTRY, link_label)
@@ -1202,9 +1187,7 @@ def process_user_input(enum: RowType) -> None:
show_wait_dialog = True show_wait_dialog = True
wait_msg = "Working" wait_msg = "Working"
call_on_thread( call_on_thread(show_wait_dialog, cmd_string, wait_msg, response, choice=enum)
show_wait_dialog, cmd_string, wait_msg, response, choice=enum
)
return return
@@ -1292,9 +1275,7 @@ class OuterWindow(Gtk.Window):
logger.info(f"Using default window size {w},{h}") logger.info(f"Using default window size {w},{h}")
self.set_default_size(w, h) self.set_default_size(w, h)
def _on_delete_event( def _on_delete_event(self, window: "OuterWindow", event: Gdk.EventKey) -> None:
self, window: "OuterWindow", event: Gdk.EventKey
) -> None:
self.halt_proc_and_quit() self.halt_proc_and_quit()
def halt_proc_and_quit(self) -> None: def halt_proc_and_quit(self) -> None:
@@ -1361,8 +1342,7 @@ class RightPanel(Gtk.Box):
self.pack_start(self.filters_vbox, False, False, 0) self.pack_start(self.filters_vbox, False, False, 0)
debug_tooltip = ( debug_tooltip = (
"Used to perform a dry run without\n" "Used to perform a dry run without\n" "actually connecting to a server"
"actually connecting to a server"
) )
ping_tooltip = ( ping_tooltip = (
"Refresh the ping for visible servers.\n" "Refresh the ping for visible servers.\n"
@@ -1431,8 +1411,7 @@ class RightPanel(Gtk.Box):
rows = ModelManager.get_filtered() rows = ModelManager.get_filtered()
with ThreadPoolExecutor(100) as executor: with ThreadPoolExecutor(100) as executor:
futures = [ futures = [
executor.submit(Servers.ping, i, row) executor.submit(Servers.ping, i, row) for i, row in enumerate(rows)
for i, row in enumerate(rows)
] ]
wait(futures) wait(futures)
for future in futures: for future in futures:
@@ -1808,9 +1787,7 @@ class ModelManagerSingleton:
class TreeView(Gtk.TreeView): class TreeView(Gtk.TreeView):
__gsignals__ = { __gsignals__ = {"on_distcalc_started": (GObject.SignalFlags.RUN_FIRST, None, ())}
"on_distcalc_started": (GObject.SignalFlags.RUN_FIRST, None, ())
}
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@@ -1929,9 +1906,7 @@ class TreeView(Gtk.TreeView):
if self.current_proc and self.current_proc.is_alive(): if self.current_proc and self.current_proc.is_alive():
self.current_proc.terminate() self.current_proc.terminate()
def _delete_note( def _delete_note(self, button: Gtk.Button, user_entry: Gtk.Box, addr: str) -> None:
self, button: Gtk.Button, user_entry: Gtk.Box, addr: str
) -> None:
box = button.get_parent() box = button.get_parent()
dialog = box.get_parent() dialog = box.get_parent()
try: try:
@@ -2023,9 +1998,7 @@ class TreeView(Gtk.TreeView):
model.remove(it) model.remove(it)
block_signals() block_signals()
thread = threading.Thread( thread = threading.Thread(target=ModelManager.resync_model, args=(addr, qport))
target=ModelManager.resync_model, args=(addr, qport)
)
thread.start() thread.start()
def remove_from_history(self) -> None: def remove_from_history(self) -> None:
@@ -2049,16 +2022,12 @@ class TreeView(Gtk.TreeView):
if hasattr(TreeView, menu_item.action): if hasattr(TreeView, menu_item.action):
func = getattr(TreeView, menu_item.action) func = getattr(TreeView, menu_item.action)
msg = ( msg = (
f"User clicked context menu '{menu_item.get_label()}', " f"User clicked context menu '{menu_item.get_label()}', " f"calls {func}"
f"calls {func}"
) )
logger.info(msg) logger.info(msg)
func(self) func(self)
else: else:
msg = ( msg = f"Context menu function for '{menu_item.action}' " f"does not exist"
f"Context menu function for '{menu_item.action}' "
f"does not exist"
)
u_msg = ( u_msg = (
f"Something went wrong when accessing the method " f"Something went wrong when accessing the method "
f"'{menu_item.action}'" f"'{menu_item.action}'"
@@ -2099,9 +2068,7 @@ class TreeView(Gtk.TreeView):
return True return True
return False return False
def _on_button_release( def _on_button_release(self, widget: Gtk.Widget, event: Gdk.EventButton) -> None:
self, widget: Gtk.Widget, event: Gdk.EventButton
) -> None:
if event.type is Gdk.EventType.BUTTON_RELEASE and event.button != 3: if event.type is Gdk.EventType.BUTTON_RELEASE and event.button != 3:
return return
@@ -2185,9 +2152,7 @@ class TreeView(Gtk.TreeView):
if event.type is Gdk.EventType.KEY_PRESS and event.keyval is Gdk.KEY_l: if event.type is Gdk.EventType.KEY_PRESS and event.keyval is Gdk.KEY_l:
if self.is_selection_empty(): if self.is_selection_empty():
return return
self.menu.popup_at_widget( self.menu.popup_at_widget(widget, Gdk.Gravity.CENTER, Gdk.Gravity.WEST)
widget, Gdk.Gravity.CENTER, Gdk.Gravity.WEST
)
else: else:
self.menu.popup_at_pointer(event) self.menu.popup_at_pointer(event)
self.menu.select_first(False) self.menu.select_first(False)
@@ -2201,9 +2166,7 @@ class TreeView(Gtk.TreeView):
return None return None
call_out("start_cooldown", "", "") call_out("start_cooldown", "", "")
thread = threading.Thread( thread = threading.Thread(target=self._background_player_count, args=())
target=self._background_player_count, args=()
)
thread.start() thread.start()
def get_current_iter(self) -> Gtk.TreeIter | None: def get_current_iter(self) -> Gtk.TreeIter | None:
@@ -2218,9 +2181,7 @@ class TreeView(Gtk.TreeView):
context = App.treeview.get_subpage_label() context = App.treeview.get_subpage_label()
row_sel = self.get_value_at_index(0) row_sel = self.get_value_at_index(0)
logger.info( logger.info(f"Tree selection for context '{context}' changed to '{row_sel}'")
f"Tree selection for context '{context}' changed to '{row_sel}'"
)
if self.current_proc and self.current_proc.is_alive(): if self.current_proc and self.current_proc.is_alive():
self.current_proc.terminate() self.current_proc.terminate()
@@ -2278,9 +2239,7 @@ class TreeView(Gtk.TreeView):
self.set_cursor(path) self.set_cursor(path)
return None return None
def _on_keypress( def _on_keypress(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> bool | None:
self, treeview: Gtk.TreeView, event: Gdk.EventKey
) -> bool | None:
keyname = Gdk.keyval_name(event.keyval) keyname = Gdk.keyval_name(event.keyval)
grid = App.grid grid = App.grid
if event.state is Gdk.ModifierType.CONTROL_MASK: if event.state is Gdk.ModifierType.CONTROL_MASK:
@@ -2335,9 +2294,7 @@ class TreeView(Gtk.TreeView):
return False return False
return None return None
def _on_key_release( def _on_key_release(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> None:
self, treeview: Gtk.TreeView, event: Gdk.EventKey
) -> None:
""" """
Suppresses spamming on keydown Suppresses spamming on keydown
""" """
@@ -2452,10 +2409,7 @@ class TreeView(Gtk.TreeView):
params = Servers.params params = Servers.params
serv = [] serv = []
with ThreadPoolExecutor() as executor: with ThreadPoolExecutor() as executor:
futures = [ futures = [executor.submit(job, key, APPID_DAYZ, param) for param in params]
executor.submit(job, key, APPID_DAYZ, param)
for param in params
]
wait(futures) wait(futures)
for future in futures: for future in futures:
res = future.result() res = future.result()
@@ -2477,10 +2431,7 @@ class TreeView(Gtk.TreeView):
def _dump_lan(self, port: int) -> list | None: def _dump_lan(self, port: int) -> list | None:
with ThreadPoolExecutor() as executor: with ThreadPoolExecutor() as executor:
futures = [ futures = [executor.submit(Servers.test_ip, i, port) for i in range(1, 256)]
executor.submit(Servers.test_ip, i, port)
for i in range(1, 256)
]
wait(futures) wait(futures)
servers = [] servers = []
for future in futures: for future in futures:
@@ -2500,7 +2451,7 @@ class TreeView(Gtk.TreeView):
if len(ips) == 0: if len(ips) == 0:
return [] return []
# NOTE: block malformed records # NOTE: block malformed records
ips = [ip for ip in ips if len(ip.split(":")) == 3 and ip.split(":")[2] != "" ] ips = [ip for ip in ips if len(ip.split(":")) == 3 and ip.split(":")[2] != ""]
with ThreadPoolExecutor() as executor: with ThreadPoolExecutor() as executor:
futures = [ futures = [
executor.submit( executor.submit(
@@ -2605,9 +2556,7 @@ class TreeView(Gtk.TreeView):
ModelManager.set_store(App.treeview.get_model()) ModelManager.set_store(App.treeview.get_model())
App.treeview.set_model(None) App.treeview.set_model(None)
thread = threading.Thread( thread = threading.Thread(target=ModelManager.filter, args=(mode, *args))
target=ModelManager.filter, args=(mode, *args)
)
thread.start() thread.start()
def _background_quad(self, dialog: "GenericDialog", mode: RowType) -> None: def _background_quad(self, dialog: "GenericDialog", mode: RowType) -> None:
@@ -2766,13 +2715,9 @@ class TreeView(Gtk.TreeView):
self.initialize_columns() self.initialize_columns()
self.set_selection_mode(Gtk.SelectionMode.SINGLE) self.set_selection_mode(Gtk.SelectionMode.SINGLE)
self.wait_dialog = GenericDialog( self.wait_dialog = GenericDialog("Fetching server metadata", Popup.WAIT)
"Fetching server metadata", Popup.WAIT
)
self.wait_dialog.show_all() self.wait_dialog.show_all()
thread = threading.Thread( thread = threading.Thread(target=self._query_servers, args=(mode, port))
target=self._query_servers, args=(mode, port)
)
thread.start() thread.start()
def _format_float( def _format_float(
@@ -2951,9 +2896,7 @@ class TreeView(Gtk.TreeView):
def get_view(self): def get_view(self):
return self.view return self.view
def get_prereqs( def get_prereqs(self, record: Record) -> tuple[bool, str, str | None, "Prereqs"]:
self, record: Record
) -> tuple[bool, str, str|None, "Prereqs"]:
""" """
Always called on a thread with a dialog on the transient parent window Always called on a thread with a dialog on the transient parent window
""" """
@@ -2967,18 +2910,14 @@ class TreeView(Gtk.TreeView):
steam_path = query_config("default_steam_path")[0] steam_path = query_config("default_steam_path")[0]
if len(steam_path) < 1: if len(steam_path) < 1:
logger.critical( logger.critical("Config file has no value set for 'default_steam_path'")
"Config file has no value set for 'default_steam_path'"
)
msg = "Local Steam installation is not set, possibly malformed config file." msg = "Local Steam installation is not set, possibly malformed config file."
return (False, msg, None, prereqs) return (False, msg, None, prereqs)
try: try:
pefile_path = PeFile.get_pefile_path(steam_path, prereqs.appid) pefile_path = PeFile.get_pefile_path(steam_path, prereqs.appid)
except AppNotInstalledError: except AppNotInstalledError:
logger.critical( logger.critical(f"'{prereqs.appid}' not found in user's libraryfolders")
f"'{prereqs.appid}' not found in user's libraryfolders"
)
msg = ( msg = (
f"This server is running {build}. You can install " f"This server is running {build}. You can install "
f"{build} by searching for it in your Steam library. " f"{build} by searching for it in your Steam library. "
@@ -2987,9 +2926,7 @@ class TreeView(Gtk.TreeView):
) )
return (False, msg, None, prereqs) return (False, msg, None, prereqs)
except AppMovedError: except AppMovedError:
logger.critical( logger.critical(f"Library folder synch error for '{prereqs.appid}'")
f"Library folder synch error for '{prereqs.appid}'"
)
msg = ( msg = (
f"Steam is reporting that {build} is installed at a non-existent location. " 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, " f"If you recently installed {build} or moved it to a different drive, "
@@ -3044,7 +2981,6 @@ class TreeView(Gtk.TreeView):
return (True, "", pefile_path, prereqs) return (True, "", pefile_path, prereqs)
@signal_emission @signal_emission
@update_window_labels @update_window_labels
def _on_row_activated( def _on_row_activated(
@@ -3198,7 +3134,7 @@ class GenericDialog(Gtk.MessageDialog):
self.outer.set_margin_end(30) self.outer.set_margin_end(30)
def _on_dialog_delete( def _on_dialog_delete(
self, response_id: Gtk.ResponseType, event: Gdk.Event self, response_id: Gtk.ResponseType, event: Gdk.Event
) -> Literal[True]: ) -> Literal[True]:
""" """
Prevent manual dialog destruction Prevent manual dialog destruction
@@ -3277,9 +3213,7 @@ class LanDialog(Gtk.MessageDialog):
self.connect("delete-event", self.restore_context) self.connect("delete-event", self.restore_context)
self.entry.connect("insert-text", self._on_text_typed) self.entry.connect("insert-text", self._on_text_typed)
self.entry.get_property("buffer").connect( self.entry.get_property("buffer").connect("deleted-text", self._on_text_deleted)
"deleted-text", self._on_text_deleted
)
def _validate(self, text: str) -> None: def _validate(self, text: str) -> None:
if self._is_invalid(text): if self._is_invalid(text):
@@ -3395,9 +3329,7 @@ class DetailsDialog(GenericDialog):
for i, column_title in enumerate(["Item", "Details"]): for i, column_title in enumerate(["Item", "Details"]):
renderer = Gtk.CellRendererText(xalign=0) renderer = Gtk.CellRendererText(xalign=0)
if i == 0: if i == 0:
column = Gtk.TreeViewColumn( column = Gtk.TreeViewColumn(column_title, renderer, text=i, weight=2)
column_title, renderer, text=i, weight=2
)
else: else:
column = Gtk.TreeViewColumn(column_title, renderer, text=i) column = Gtk.TreeViewColumn(column_title, renderer, text=i)
column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
@@ -3413,12 +3345,8 @@ class DetailsDialog(GenericDialog):
scrollable_message = Gtk.ScrolledWindow() scrollable_message = Gtk.ScrolledWindow()
desc = Gtk.Label(label="Server message", valign=Gtk.Align.START) desc = Gtk.Label(label="Server message", valign=Gtk.Align.START)
add_class(desc, "details-heading") add_class(desc, "details-heading")
box = Gtk.Box( box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER)
orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER self.description = Gtk.Label(justify=Gtk.Justification.CENTER, wrap=True)
)
self.description = Gtk.Label(
justify=Gtk.Justification.CENTER, wrap=True
)
sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
sep.set_margin_bottom(10) sep.set_margin_bottom(10)
for el in desc, sep, self.description: for el in desc, sep, self.description:
@@ -3456,9 +3384,7 @@ class DetailsDialog(GenericDialog):
self.run() self.run()
self.destroy() self.destroy()
def _background( def _background(self, dialog: "GenericDialog", ip: str, qport: int) -> None:
self, dialog: "GenericDialog", ip: str, qport: int
) -> None:
response = Servers.details(self.ip, self.qport) response = Servers.details(self.ip, self.qport)
if response.success: if response.success:
for row in response.data: for row in response.data:
@@ -3515,9 +3441,7 @@ class ModDialog(GenericDialog):
wait_dialog = GenericDialog("Fetching modlist", Popup.WAIT) wait_dialog = GenericDialog("Fetching modlist", Popup.WAIT)
wait_dialog.show_all() wait_dialog.show_all()
thread = threading.Thread( thread = threading.Thread(target=self._background, args=(wait_dialog, record))
target=self._background, args=(wait_dialog, record)
)
thread.start() thread.start()
def _background(self, dialog: "GenericDialog", record: str) -> None: def _background(self, dialog: "GenericDialog", record: str) -> None:
@@ -3542,9 +3466,7 @@ class ModDialog(GenericDialog):
self.view.set_model(modlist_store) self.view.set_model(modlist_store)
GLib.idle_add(_load) GLib.idle_add(_load)
def _parse_modlist_rows( def _parse_modlist_rows(self, data: subprocess.CompletedProcess) -> bool | int:
self, data: subprocess.CompletedProcess
) -> bool | int:
lines = data.stdout.splitlines() lines = data.stdout.splitlines()
hits = len(lines) hits = len(lines)
reader = csv.reader(lines, delimiter=delimiter) reader = csv.reader(lines, delimiter=delimiter)
@@ -3574,9 +3496,7 @@ class ModDialog(GenericDialog):
class LinkDialog(GenericDialog): class LinkDialog(GenericDialog):
def __init__( def __init__(self, text: str, link: str | None, command: RowType, uid: str = ""):
self, text: str, link: str | None, command: RowType, uid: str = ""
):
super().__init__(text, Popup.NOTIFY) super().__init__(text, Popup.NOTIFY)
text = textwrap.dedent(text) text = textwrap.dedent(text)
@@ -3794,7 +3714,12 @@ class Options(Gtk.Box):
client_store = Gtk.ListStore(str, str) client_store = Gtk.ListStore(str, str)
client_store.append(["steam", "steam"]) client_store.append(["steam", "steam"])
client_store.append(["flatpak", "flatpak run com.valvesoftware.Steam"]) client_store.append(["flatpak", "flatpak run com.valvesoftware.Steam"])
client_store.append(["flatpak (container)", "flatpak-spawn --host flatpak run com.valvesoftware.Steam"]) client_store.append(
[
"flatpak (container)",
"flatpak-spawn --host flatpak run com.valvesoftware.Steam",
]
)
renderer_text = Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END) renderer_text = Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END)
self.client_combo = Gtk.ComboBox.new_with_model(client_store) self.client_combo = Gtk.ComboBox.new_with_model(client_store)
@@ -3818,8 +3743,7 @@ class Options(Gtk.Box):
self.force_button.set_sensitive(False) self.force_button.set_sensitive(False)
msg = ( msg = (
"Manual: prompt to subscribe to mods in Steam. " "Manual: prompt to subscribe to mods in Steam. " "Auto: unmanned downloads."
"Auto: unmanned downloads."
) )
eb = InfoEventBox(msg) eb = InfoEventBox(msg)
@@ -3878,9 +3802,7 @@ class Options(Gtk.Box):
self.make_frame(mods_grid, "Mods"), self.make_frame(mods_grid, "Mods"),
self.make_frame(version_grid, "Version"), self.make_frame(version_grid, "Version"),
]: ]:
grid.attach( grid.attach(frame, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
frame, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT
)
row += 1 row += 1
self.scrollable = Gtk.ScrolledWindow(vexpand=True) self.scrollable = Gtk.ScrolledWindow(vexpand=True)
@@ -3926,9 +3848,7 @@ class Options(Gtk.Box):
return box return box
def _on_field_activated( def _on_field_activated(self, entry: Gtk.Entry, context: Preferences) -> None:
self, entry: Gtk.Entry, context: Preferences
) -> None:
text = entry.get_text() text = entry.get_text()
button = entry.sibling button = entry.sibling
if not self._is_valid_text(text, context): if not self._is_valid_text(text, context):
@@ -3949,9 +3869,7 @@ class Options(Gtk.Box):
for record in rows: for record in rows:
col = 1 col = 1
for el in record: for el in record:
grid.attach( grid.attach(el, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
el, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT
)
col += 1 col += 1
row += 1 row += 1
return grid return grid
@@ -4004,9 +3922,7 @@ class Options(Gtk.Box):
return return
process_toggle(RowType.TGL_BRANCH) process_toggle(RowType.TGL_BRANCH)
def _on_radio_toggled( def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None:
self, button: Gtk.RadioButton, context: Preferences
) -> None:
if App.treeview.subpage is not RowType.OPTIONS: if App.treeview.subpage is not RowType.OPTIONS:
return return
match context: match context:
@@ -4150,18 +4066,14 @@ class Options(Gtk.Box):
field[1].get_children()[1].set_sensitive(False) field[1].get_children()[1].set_sensitive(False)
try: try:
pe_file_path = PeFile.get_pefile_path( pe_file_path = PeFile.get_pefile_path(default_steam_path, APPID_DAYZ)
default_steam_path, APPID_DAYZ
)
vers = PeFile.get_dayz_version(pe_file_path) vers = PeFile.get_dayz_version(pe_file_path)
dayz_version = PeFile.dayz_version_to_str(vers) dayz_version = PeFile.dayz_version_to_str(vers)
except Exception: except Exception:
dayz_version = "-" dayz_version = "-"
try: try:
exp_file_path = PeFile.get_pefile_path( exp_file_path = PeFile.get_pefile_path(default_steam_path, APPID_DAYZ_EXP)
default_steam_path, APPID_DAYZ_EXP
)
vers = PeFile.get_dayz_version(exp_file_path) vers = PeFile.get_dayz_version(exp_file_path)
dayz_exp_version = PeFile.dayz_version_to_str(vers) dayz_exp_version = PeFile.dayz_version_to_str(vers)
except Exception: except Exception:
@@ -4270,9 +4182,7 @@ class KeybindingsDialog(Gtk.Box):
if not sep: if not sep:
grid.attach(desc, col, row, w, h) grid.attach(desc, col, row, w, h)
else: else:
grid.attach_next_to( grid.attach_next_to(desc, sep, Gtk.PositionType.BOTTOM, w, h)
desc, sep, Gtk.PositionType.BOTTOM, w, h
)
row += 1 row += 1
sep = None sep = None
grid.attach_next_to(frame, desc, Gtk.PositionType.RIGHT, w, h) grid.attach_next_to(frame, desc, Gtk.PositionType.RIGHT, w, h)
@@ -4280,9 +4190,7 @@ class KeybindingsDialog(Gtk.Box):
l_spacer = Gtk.Label(label="") l_spacer = Gtk.Label(label="")
r_spacer = Gtk.Label(label="") r_spacer = Gtk.Label(label="")
grid.attach(l_spacer, col, row + 1, w, h) grid.attach(l_spacer, col, row + 1, w, h)
grid.attach_next_to( grid.attach_next_to(r_spacer, l_spacer, Gtk.PositionType.RIGHT, w, h)
r_spacer, l_spacer, Gtk.PositionType.RIGHT, w, h
)
row += 1 row += 1
return grid return grid
@@ -4291,9 +4199,7 @@ class KeybindingsDialog(Gtk.Box):
col = 0 col = 0
w = 1 w = 1
h = 1 h = 1
sidebar = Gtk.Grid( sidebar = Gtk.Grid(row_homogeneous=True, orientation=Gtk.Orientation.VERTICAL)
row_homogeneous=True, orientation=Gtk.Orientation.VERTICAL
)
for cat in categories: for cat in categories:
label = Gtk.Label(label=cat) label = Gtk.Label(label=cat)
add_class(label, "left-label") add_class(label, "left-label")
@@ -4313,9 +4219,7 @@ class KeybindingsDialog(Gtk.Box):
column = 1 column = 1
w = 1 w = 1
h = 1 h = 1
sidebar = self.build_sidebar( sidebar = self.build_sidebar(["Servers", "Navigation", "Vim-style keys"])
["Servers", "Navigation", "Vim-style keys"]
)
separator = Gtk.Separator() separator = Gtk.Separator()
keys_box = self.build_keys(items) keys_box = self.build_keys(items)
@@ -4533,9 +4437,7 @@ class Statusbar(Gtk.Statusbar):
players_pretty = pluralize("players", players) players_pretty = pluralize("players", players)
hits_pretty = pluralize("matches", hits) hits_pretty = pluralize("matches", hits)
formatted = ( formatted = f"Found {hits:n} {hits_pretty} with {players:n} {players_pretty}"
f"Found {hits:n} {hits_pretty} with {players:n} {players_pretty}"
)
suffix = "| Distance: calculating..." suffix = "| Distance: calculating..."
if players == 0: if players == 0:
@@ -4587,9 +4489,7 @@ class Grid(Gtk.Grid):
self.breadcrumbs = Gtk.Label(label="Main menu", halign=Gtk.Align.START) self.breadcrumbs = Gtk.Label(label="Main menu", halign=Gtk.Align.START)
self.attach(self.notebook, 0, 0, 3, 1) self.attach(self.notebook, 0, 0, 3, 1)
self.attach_next_to( self.attach_next_to(self.breadcrumbs, self.notebook, Gtk.PositionType.TOP, 3, 1)
self.breadcrumbs, self.notebook, Gtk.PositionType.TOP, 3, 1
)
self.attach_next_to( self.attach_next_to(
self.statusbar, self.notebook, Gtk.PositionType.BOTTOM, 3, 1 self.statusbar, self.notebook, Gtk.PositionType.BOTTOM, 3, 1
) )
@@ -4651,11 +4551,23 @@ class App(Gtk.Application):
) )
self.win.add_accel_group(accel) self.win.add_accel_group(accel)
GLib.unix_signal_add( self.setup_signals()
GLib.PRIORITY_DEFAULT, signal.SIGINT, self._catch_sigint
)
Gtk.main() Gtk.main()
def setup_signals(self) -> None:
SIGNAL_ADD = "signal_add"
SIGNAL_ADD_FULL = "signal_add_full"
try:
if SIGNAL_ADD in dir(GLibUnix):
func = GLibUnix.signal_add
elif SIGNAL_ADD_FULL in dir(GLibUnix):
func = GLibUnix.signal_add_full
else:
func = GLib.unix_signal_add
func(GLib.PRIORITY_DEFAULT, signal.SIGINT, self._catch_sigint)
except Exception as e:
logger.critical(e)
def _catch_sigint(self) -> None: def _catch_sigint(self) -> None:
self.win.halt_proc_and_quit() self.win.halt_proc_and_quit()
@@ -4740,9 +4652,7 @@ class ModSelectionPanel(Gtk.Box):
def toggle_select_stale_button(self, state: bool) -> None: def toggle_select_stale_button(self, state: bool) -> None:
if state: if state:
button = Gtk.Button( button = Gtk.Button(label="Select stale", margin_start=10, margin_end=10)
label="Select stale", margin_start=10, margin_end=10
)
text = "Bulk selects all currently highlighted mods" text = "Bulk selects all currently highlighted mods"
button.set_tooltip_text(text) button.set_tooltip_text(text)
button.connect("clicked", self._on_button_clicked) button.connect("clicked", self._on_button_clicked)
@@ -4862,9 +4772,7 @@ class FilterPanel(Gtk.Box):
self.keyword_entry = Gtk.Entry() self.keyword_entry = Gtk.Entry()
self.keyword_entry.set_placeholder_text("Filter by keyword") self.keyword_entry.set_placeholder_text("Filter by keyword")
self.keyword_entry.connect("activate", self._on_keyword_enter) self.keyword_entry.connect("activate", self._on_keyword_enter)
self.keyword_entry.connect( self.keyword_entry.connect("key-press-event", self._on_keyword_keypress)
"key-press-event", self._on_keyword_keypress
)
completion = Gtk.EntryCompletion(inline_completion=True) completion = Gtk.EntryCompletion(inline_completion=True)
completion.set_text_column(0) completion.set_text_column(0)
@@ -4932,9 +4840,7 @@ class FilterPanel(Gtk.Box):
state = self.default_filters[label] state = self.default_filters[label]
check.set_active(state) check.set_active(state)
def _on_map_entry_keypress( def _on_map_entry_keypress(self, entry: Gtk.Entry, event: Gdk.EventKey) -> None:
self, entry: Gtk.Entry, event: Gdk.EventKey
) -> None:
match event.keyval: match event.keyval:
case Gdk.KEY_Return: case Gdk.KEY_Return:
text = entry.get_text() text = entry.get_text()
@@ -4977,9 +4883,7 @@ class FilterPanel(Gtk.Box):
App.treeview.grab_focus() App.treeview.grab_focus()
return False return False
def _on_keyword_keypress( def _on_keyword_keypress(self, entry: Gtk.Entry, event: Gdk.EventKey) -> bool:
self, entry: Gtk.Entry, event: Gdk.EventKey
) -> bool:
match event.keyval: match event.keyval:
case Gdk.KEY_Up: case Gdk.KEY_Up:
return True return True
@@ -4990,9 +4894,7 @@ class FilterPanel(Gtk.Box):
return True return True
return False return False
def _on_combo_keypress( def _on_combo_keypress(self, combo: Gtk.ComboBox, event: Gdk.EventKey) -> bool:
self, combo: Gtk.ComboBox, event: Gdk.EventKey
) -> bool:
match event.keyval: match event.keyval:
case Gdk.KEY_Down: case Gdk.KEY_Down:
self.maps_combo.popup() self.maps_combo.popup()
Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB