Compare commits
263 Commits
2228cc26dd
...
b2f6abd667
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2f6abd667 | ||
|
|
f76985c79d | ||
|
|
3d3cd19892 | ||
|
|
c14fb10d23 | ||
|
|
a8417d27d7 | ||
|
|
2d58bb491e | ||
|
|
2f123da63a | ||
|
|
7016edf7cd | ||
|
|
b630cf4080 | ||
|
|
1034a5ac04 | ||
|
|
ebc62b0b1b | ||
|
|
4b47ff1fe0 | ||
|
|
e3155ad7ee | ||
|
|
c45d9fb234 | ||
|
|
22e92e67a7 | ||
|
|
cb45ce7b9a | ||
|
|
9678d41b4c | ||
|
|
0daf567111 | ||
|
|
b75e99ffd7 | ||
|
|
5562c44320 | ||
|
|
236a5dd0a3 | ||
|
|
4c10958d10 | ||
|
|
23f7140c66 | ||
|
|
fbcfa1fb34 | ||
|
|
52f36e0d0a | ||
|
|
d90c069a73 | ||
|
|
d2f3eebd42 | ||
|
|
af64ebb12a | ||
|
|
929f497b9f | ||
|
|
60aebe5eeb | ||
|
|
aef9a10c42 | ||
|
|
c6034bc6b9 | ||
|
|
005288371b | ||
|
|
53e50a5716 | ||
|
|
a50b11633f | ||
|
|
9c4541fe76 | ||
|
|
ac3e7ee128 | ||
|
|
1495ad386d | ||
|
|
4edf71aa0a | ||
|
|
113e5f885b | ||
|
|
a3092b1bf3 | ||
|
|
e28929bb70 | ||
|
|
4a820e3083 | ||
|
|
d866b4992b | ||
|
|
376c5294d9 | ||
|
|
71651bab6c | ||
|
|
1dba57af4b | ||
|
|
dabf5d4ef0 | ||
|
|
75f8c841df | ||
|
|
5bce8709af | ||
|
|
a418bcfb90 | ||
|
|
90d59cec97 | ||
|
|
a8ae07931d | ||
|
|
a4ce65fe57 | ||
|
|
d7e94eab90 | ||
|
|
a08c588df3 | ||
|
|
385b1a5af2 | ||
|
|
be02f086c7 | ||
|
|
cc35b2cd85 | ||
|
|
a419aad160 | ||
|
|
21d4601e41 | ||
|
|
a1d14669c7 | ||
|
|
a94f8f0e32 | ||
|
|
0da098afb6 | ||
|
|
c8e9f8f1fc | ||
|
|
506c82e86a | ||
|
|
7e45cb0fa2 | ||
|
|
3729d3c656 | ||
|
|
773d24a00c | ||
|
|
661329877f | ||
|
|
08f1f9f4d1 | ||
|
|
dccdffe761 | ||
|
|
957ad47b4d | ||
|
|
16d8f567a4 | ||
|
|
c883fc586c | ||
|
|
cc73f23c1c | ||
|
|
ec7daf7f6b | ||
|
|
d0de2974d8 | ||
|
|
5a54d43601 | ||
|
|
cbc131028f | ||
|
|
239a82876e | ||
|
|
1dfe9cd9df | ||
|
|
f2124218e4 | ||
|
|
cf087d96c7 | ||
|
|
6bb59f4060 | ||
|
|
d53476399d | ||
|
|
d34293e2c8 | ||
|
|
f82df9eacf | ||
|
|
e73617c2f4 | ||
|
|
67b0559058 | ||
|
|
dfe65aacd4 | ||
|
|
6aa1ec4201 | ||
|
|
d6223e54b0 | ||
|
|
83552f833c | ||
|
|
f0657be289 | ||
|
|
23fb10f48e | ||
|
|
eeca71fc25 | ||
|
|
89f37ab70d | ||
|
|
9d1ee7e130 | ||
|
|
9a37cebe47 | ||
|
|
239f806f7e | ||
|
|
54e2d2feaa | ||
|
|
796dcd5588 | ||
|
|
35f1f180d2 | ||
|
|
fe40a5e54f | ||
|
|
936dfe8d02 | ||
|
|
9db40fe664 | ||
|
|
5bcc4ec76d | ||
|
|
155306b1ab | ||
|
|
cff7b1c2e9 | ||
|
|
38203a9530 | ||
|
|
01c8a5276b | ||
|
|
17229c1cba | ||
|
|
adc617730b | ||
|
|
8acd359028 | ||
|
|
a03971ce70 | ||
|
|
054486dfef | ||
|
|
3043005977 | ||
|
|
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 | ||
|
|
d56bc5a57d | ||
|
|
0b433af230 | ||
|
|
61f3fd9821 | ||
|
|
c7bd8d3809 | ||
|
|
0d3a3debf9 | ||
|
|
8e492095d9 | ||
|
|
6a554b4e88 | ||
|
|
e2848b3fed | ||
|
|
a5be4d9d31 | ||
|
|
da5a5999f0 | ||
|
|
f4f3e61b83 | ||
|
|
3d91e07d25 | ||
|
|
cff4a2e1e7 | ||
|
|
4f3f17450c | ||
|
|
5d8741edfa | ||
|
|
94cabc9d70 | ||
|
|
9508bfd416 | ||
|
|
3ace15d78a | ||
|
|
2bb575cd97 | ||
|
|
5dfa51dc15 | ||
|
|
5dd0cd416b | ||
|
|
6f5f9ecb7b | ||
|
|
5b9309a6bd | ||
|
|
9c6847ba39 | ||
|
|
8f3238e4de | ||
|
|
8c7ae8acee | ||
|
|
9d94b4ff09 | ||
|
|
4795aeb3f9 | ||
|
|
12d971a5cf | ||
|
|
ff0c5caf30 | ||
|
|
63beb61878 | ||
|
|
6c192741ec | ||
|
|
437ea2a218 | ||
|
|
ee0fd7b78c | ||
|
|
562f6453dc | ||
|
|
51308e788e | ||
|
|
acc356d3a0 | ||
|
|
eaedde7ee5 | ||
|
|
43f32638f8 | ||
|
|
12b82cd1dd | ||
|
|
6081e6d2a7 | ||
|
|
f7af8ff35a | ||
|
|
51a5f4b8ef | ||
|
|
f9ba990142 | ||
|
|
09fdbf3a6f | ||
|
|
c57af3086c | ||
|
|
4e5f988ef7 | ||
|
|
e8c51cbadf | ||
|
|
495e7c14be | ||
|
|
b4ae6a9e8b | ||
|
|
99e13be3e8 | ||
|
|
760634d3d8 | ||
|
|
ae67124c10 | ||
|
|
235f4bca37 | ||
|
|
8ae6d67cca | ||
|
|
50b4ad8e3c | ||
|
|
bc712e926d | ||
|
|
6d7b2a1d19 | ||
|
|
4c504a7bd9 | ||
|
|
133966ef36 | ||
|
|
ec23a4eb2e | ||
|
|
169f4c051d | ||
|
|
6219fb6a78 | ||
|
|
02105807fb | ||
|
|
74bfdf42ae | ||
|
|
ede7f345da | ||
|
|
e18d203449 | ||
|
|
99fbc7a2cc | ||
|
|
317212ed6a | ||
|
|
7ec8d6c3bf | ||
|
|
cf053299f7 | ||
|
|
5ba23249bf | ||
|
|
406943bc12 | ||
|
|
4eb3171891 | ||
|
|
38de9a8e9d | ||
|
|
10c8555ddc | ||
|
|
ec0e276100 | ||
|
|
3c235e22ed | ||
|
|
05e897dd31 | ||
|
|
1b7254003a | ||
|
|
acf43b1591 | ||
|
|
4cbffaf3ff | ||
|
|
2e2b738ca6 | ||
|
|
22758938bd | ||
|
|
52961755ff | ||
|
|
df872fa4e7 | ||
|
|
35804df4df | ||
|
|
117814ad61 | ||
|
|
afe2882f3b | ||
|
|
bba793cc52 | ||
|
|
1b360f84ae | ||
|
|
bb976644cb | ||
|
|
aaaf653c57 | ||
|
|
76e8640476 | ||
|
|
eff6450d49 | ||
|
|
eee65314e8 | ||
|
|
fc6e8ef001 | ||
|
|
08c8862106 | ||
|
|
63307abd52 | ||
|
|
2ab0b5bbca | ||
|
|
5f6679cf34 | ||
|
|
5bc503f209 | ||
|
|
3a1223be9d | ||
|
|
853f353562 | ||
|
|
25ec6ec08d | ||
|
|
be8bf74401 | ||
|
|
63671e2478 | ||
|
|
9e09bd3bb3 | ||
|
|
5a292b37c4 | ||
|
|
0dc339893b | ||
|
|
923bac8f76 | ||
|
|
79b0bae05d | ||
|
|
9738de8fd1 |
3
.gitignore
vendored
@ -1 +1,4 @@
|
|||||||
tags
|
tags
|
||||||
|
__pycache__
|
||||||
|
pre-commit
|
||||||
|
tests/
|
||||||
|
|||||||
113
CHANGELOG.md
@ -1,6 +1,111 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [5.8.0] 2025-12-27
|
## [6.0.2] 2026-01-26
|
||||||
|
## Fixed
|
||||||
|
- Explicitly use Python 3.13 when calling subprocesses
|
||||||
|
- Raise error correctly from module
|
||||||
|
- Do not spawn dialogs from inside of thread when fetching prereqs
|
||||||
|
## Changed
|
||||||
|
- Optimize time complexity of startup checks
|
||||||
|
- Optimize coordinate calculation by using local records first
|
||||||
|
- Rename some internal functions
|
||||||
|
- Wrap entire startup process in dialog
|
||||||
|
|
||||||
|
## [6.0.1] 2026-01-16
|
||||||
|
## Fixed
|
||||||
|
- Explicitly use Python 3.13 when calling subprocesses
|
||||||
|
|
||||||
|
## [6.0.0] 2026-01-14
|
||||||
|
## Added
|
||||||
|
- Support DayZ Experimental
|
||||||
|
- Ping display on server tables
|
||||||
|
- Save descriptive text notes on a per-server basis
|
||||||
|
- Speed up load time and navigation of server tables
|
||||||
|
- More robust threading and cache system when filtering servers
|
||||||
|
- Show additional DayZ client information in Options menu
|
||||||
|
- Warn user of DayZ client version mismatches
|
||||||
|
- Support clickable hyperlinks in dialogs
|
||||||
|
- Rich server metadata details dialog (right-click on server table)
|
||||||
|
- Filter by modded servers
|
||||||
|
- Breadcrumbs showing current menu context
|
||||||
|
- Dedicated changelog page
|
||||||
|
- Dedicated keybindings page
|
||||||
|
- Dedicated settings page ("Options")
|
||||||
|
- Support sandboxed version of Flatpak
|
||||||
|
- Vim-style navigation keybindings
|
||||||
|
- Added "Return to main menu" button to dialog windows when failing to load server table
|
||||||
|
- Dynamic context menus for modded servers
|
||||||
|
- Additional keybindings for new filter toggles
|
||||||
|
- If a server is locked, prompt user when connecting
|
||||||
|
- Internal flag to allow distro-packaged releases to disable in-app updates
|
||||||
|
- Pre-boot checks to test whether script was invoked directly
|
||||||
|
- Commandline usage help text (GaryBlackbourne)
|
||||||
|
- More descriptive help dialogs when initial dependencies are missing
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
|
||||||
|
- Key stickiness when quickly navigating through entries in tables
|
||||||
|
- Servers returning malformed A2S_INFO blocking server browser from loading
|
||||||
|
- Normalized buttons in dialogs and restored proper padding
|
||||||
|
- GTK errors being emitted to stdout when inserting debug table
|
||||||
|
- Entry dialog sensitivity when validating API keys
|
||||||
|
- Additional validation on entry dialogs to prevent submitting empty text
|
||||||
|
- Suppress errors during pre-boot checks when mods are not installed
|
||||||
|
- Centered filter checkboxes within panel
|
||||||
|
- Do not pop unhighlight/select stale buttons if no stale mods exist
|
||||||
|
- Improved keybinding interaction with side panels
|
||||||
|
- Suppress typeahead search in mod dialogs
|
||||||
|
- Do not trigger global API cooldown if no LAN/favorite servers are found
|
||||||
|
- Fix table column expansion in server mod dialogs
|
||||||
|
- Missing parameters when closing application via window decorations
|
||||||
|
- Statusbar contents not updating on certain pages
|
||||||
|
- Prevent debug button from activating when in certain input fields
|
||||||
|
- Window size not updating correctly when unmaximizing window after changing "fullscreen at boot" setting
|
||||||
|
- Branch toggle signal being emitted when entering Options menu from other contexts
|
||||||
|
- 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
|
||||||
|
- ESC key destroying wait dialogs while a thread is pending
|
||||||
|
- Tooltip signals being erroneously emitted on main menu
|
||||||
|
- UI not being constructed correctly if CHANGELOG.md was missing
|
||||||
|
- Config file erroneously getting updated when populating settings menu
|
||||||
|
- Leaky variable name in dialog titles
|
||||||
|
- Prevent extraneous signals from propagating when column width is adjusted
|
||||||
|
- Server filter panel not being hidden when entering other page contexts
|
||||||
|
- Path to remote changelog being constructed incorrectly
|
||||||
|
- Dialogs with newlines breaking output in log table
|
||||||
|
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
||||||
|
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
- Require Python 3.13
|
||||||
|
- Reduced global API cooldown from 60s to 30s
|
||||||
|
- Clarify dialog messages when DayZ path could not be found
|
||||||
|
- Auto-focus first item when opening context menus
|
||||||
|
- Opacity setting on side buttons when in a different context
|
||||||
|
- Refactored BM API key validation to account for new key format
|
||||||
|
- State file serialization methods
|
||||||
|
- Optimize time complexity of pre-boot checks (GaryBlackbourne)
|
||||||
|
- Rewrote distance calculation module (GaryBlackbourne)
|
||||||
|
- Changed preferred client setting from radio toggle to combobox
|
||||||
|
|
||||||
|
## Dropped
|
||||||
|
- Ping readout in statusbar
|
||||||
|
- Extraneous information from right statusbar
|
||||||
|
|
||||||
|
## [5.8.3] 2026-01-04
|
||||||
|
## Fixed
|
||||||
|
- Normalize checksum numbers and dates
|
||||||
|
|
||||||
|
## [5.8.2] 2026-01-04
|
||||||
|
## Fixed
|
||||||
|
- Fix regression in symlink routine introduced by previous hotfix
|
||||||
|
|
||||||
|
## [5.8.1] 2025-12-27
|
||||||
## Fixed
|
## Fixed
|
||||||
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
||||||
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
||||||
@ -283,7 +388,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
|
||||||
@ -308,7 +413,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
|
||||||
@ -327,7 +432,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
|
||||||
|
|||||||
10
README.md
@ -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)
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|||||||
404
dzgui.sh
@ -1,7 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|
||||||
version=5.8.1
|
src_path="$(readlink -e "$0")"
|
||||||
|
version=6.0.2
|
||||||
|
|
||||||
#CONSTANTS
|
#CONSTANTS
|
||||||
aid=221100
|
aid=221100
|
||||||
@ -39,6 +40,7 @@ cols_file="$state_path/$prefix.cols.json"
|
|||||||
|
|
||||||
#CACHE FILES
|
#CACHE FILES
|
||||||
coords_file="$cache_path/$prefix.coords"
|
coords_file="$cache_path/$prefix.coords"
|
||||||
|
src_path_file="$cache_path/$prefix.src"
|
||||||
|
|
||||||
#legacy paths
|
#legacy paths
|
||||||
hist_file="$config_path/history"
|
hist_file="$config_path/history"
|
||||||
@ -63,17 +65,14 @@ testing_url="$url_prefix/testing"
|
|||||||
releases_url="https://github.com/$author/$repo/releases/download/browser"
|
releases_url="https://github.com/$author/$repo/releases/download/browser"
|
||||||
km_helper_url="$releases_url/latlon"
|
km_helper_url="$releases_url/latlon"
|
||||||
|
|
||||||
|
|
||||||
set_im_module(){
|
set_im_module(){
|
||||||
#TODO: drop pending SteamOS changes
|
#TODO: drop pending SteamOS changes
|
||||||
pgrep -a gamescope | grep -q "generate-drm-mode"
|
if pgrep -a gamescope | grep -q "generate-drm-mode"; then
|
||||||
if [[ $? -eq 0 ]]; then
|
unset GTK_IM_MODULE
|
||||||
GTK_IM_MODULE=""
|
|
||||||
logger INFO "Detected Steam Deck (Game Mode), unsetting GTK_IM_MODULE"
|
logger INFO "Detected Steam Deck (Game Mode), unsetting GTK_IM_MODULE"
|
||||||
else
|
|
||||||
return
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
redact(){
|
redact(){
|
||||||
sed 's@\(/home/\)[^/]*@\1REDACTED@g'
|
sed 's@\(/home/\)[^/]*@\1REDACTED@g'
|
||||||
}
|
}
|
||||||
@ -87,13 +86,21 @@ logger(){
|
|||||||
printf "%s␞%s␞%s::%s()::%s␞%s\n" "$date" "$tag" "$self" "$caller" "$line" "$string" \
|
printf "%s␞%s␞%s::%s()::%s␞%s\n" "$date" "$tag" "$self" "$caller" "$line" "$string" \
|
||||||
| redact >> "$debug_log"
|
| redact >> "$debug_log"
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_dirs(){
|
setup_dirs(){
|
||||||
for dir in "$state_path" "$cache_path" "$share_path" "$helpers_path" "$freedesktop_path" "$config_path" "$log_path"; do
|
directories=()
|
||||||
if [[ ! -d $dir ]]; then
|
directories+=("$state_path")
|
||||||
mkdir -p "$dir"
|
directories+=("$cache_path")
|
||||||
fi
|
directories+=("$share_path")
|
||||||
|
directories+=("$helpers_path")
|
||||||
|
directories+=("$freedesktop_path")
|
||||||
|
directories+=("$config_path")
|
||||||
|
directories+=("$log_path")
|
||||||
|
for dir in "${directories[@]}"; do
|
||||||
|
mkdir -p "$dir"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_state_files(){
|
setup_state_files(){
|
||||||
if [[ -f "$debug_log" ]]; then
|
if [[ -f "$debug_log" ]]; then
|
||||||
rm "$debug_log" && touch $debug_log
|
rm "$debug_log" && touch $debug_log
|
||||||
@ -104,13 +111,13 @@ setup_state_files(){
|
|||||||
logger INFO "Migrating legacy version file"
|
logger INFO "Migrating legacy version file"
|
||||||
fi
|
fi
|
||||||
# wipe cache files
|
# wipe cache files
|
||||||
local path="$cache_path"
|
if [[ $(ls -A "$cache_path") ]]; then
|
||||||
if find "$path" -mindepth 1 -maxdepth 1 | read; then
|
for file in "$cache_path"/*; do
|
||||||
for file in $path/*; do
|
|
||||||
rm "$file"
|
rm "$file"
|
||||||
done
|
done
|
||||||
logger INFO "Wiped cache files"
|
logger INFO "Wiped cache files"
|
||||||
fi
|
fi
|
||||||
|
echo "$src_path" > "$src_path_file"
|
||||||
}
|
}
|
||||||
print_config_vals(){
|
print_config_vals(){
|
||||||
local keys=(
|
local keys=(
|
||||||
@ -132,10 +139,10 @@ print_config_vals(){
|
|||||||
|
|
||||||
}
|
}
|
||||||
test_gobject(){
|
test_gobject(){
|
||||||
python3 -c "import gi"
|
python3.13 -c "import gi"
|
||||||
if [[ ! $? -eq 0 ]]; then
|
if [[ ! $? -eq 0 ]]; then
|
||||||
logger CRITICAL "Missing PyGObject"
|
logger CRITICAL "Missing PyGObject"
|
||||||
fdialog "Requires PyGObject (python-gobject)"
|
quit_with_help_dialog "Requires PyGObject (python-gobject)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
logger INFO "Found PyGObject in Python env"
|
logger INFO "Found PyGObject in Python env"
|
||||||
@ -214,17 +221,14 @@ default_steam_path="$default_steam_path"
|
|||||||
|
|
||||||
#Preferred Steam launch command (for Flatpak support)
|
#Preferred Steam launch command (for Flatpak support)
|
||||||
preferred_client="$preferred_client"
|
preferred_client="$preferred_client"
|
||||||
|
|
||||||
#DZGUI source path
|
|
||||||
src_path="$src_path"
|
|
||||||
END
|
END
|
||||||
}
|
}
|
||||||
depcheck(){
|
depcheck(){
|
||||||
for dep in "${!deps[@]}"; do
|
for dep in "${!deps[@]}"; do
|
||||||
command -v "$dep" 2>&1>/dev/null
|
if ! command -v "$dep" &> /dev/null; then
|
||||||
if [[ $? -eq 1 ]]; then
|
|
||||||
local msg="Requires $dep >= ${deps[$dep]}"
|
local msg="Requires $dep >= ${deps[$dep]}"
|
||||||
raise_error_and_quit "$msg"
|
echo "$msg"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
local jqmsg="jq must be compiled with support for oniguruma"
|
local jqmsg="jq must be compiled with support for oniguruma"
|
||||||
@ -233,14 +237,31 @@ depcheck(){
|
|||||||
[[ $? -ne 0 ]] && raise_error_and_quit "$jqmsg"
|
[[ $? -ne 0 ]] && raise_error_and_quit "$jqmsg"
|
||||||
logger INFO "Initial dependencies satisfied"
|
logger INFO "Initial dependencies satisfied"
|
||||||
}
|
}
|
||||||
check_pyver(){
|
open_url(){
|
||||||
local pyver=$(python3 --version | awk '{print $2}')
|
url="$1"
|
||||||
local minor=$(<<< $pyver awk -F. '{print $2}')
|
if [[ -n "$BROWSER" ]]; then
|
||||||
if [[ -z $pyver ]] || [[ ${pyver:0:1} -lt 3 ]] || [[ $minor -lt 10 ]]; then
|
logger INFO "Opening '$url' in '$BROWSER'"
|
||||||
local msg="Requires Python >=3.10"
|
"$BROWSER" "$url"
|
||||||
raise_error_and_quit "$msg"
|
else
|
||||||
|
logger INFO "Opening '$url' with xdg-open"
|
||||||
|
xdg-open "$url"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
quit_with_help_dialog(){
|
||||||
|
local sel
|
||||||
|
msg="$1"
|
||||||
|
help_button="Open help page"
|
||||||
|
url="https://aclist.github.io/dzgui/installation.html"
|
||||||
|
logger CRITICAL "$msg"
|
||||||
|
sel=$(zenity --info --extra-button="$help_button" --text="$msg")
|
||||||
|
[[ $sel == "$help_button" ]] && open_url "$url" &
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
check_pyver(){
|
||||||
|
if [[ ! $(python3.13 --version) ]]; then
|
||||||
|
local msg="Requires Python 3.13"
|
||||||
|
quit_with_help_dialog "$msg"
|
||||||
fi
|
fi
|
||||||
logger INFO "Found Python version: $pyver"
|
|
||||||
}
|
}
|
||||||
watcher_deps(){
|
watcher_deps(){
|
||||||
if [[ ! $(command -v wmctrl) ]] && [[ ! $(command -v xdotool) ]]; then
|
if [[ ! $(command -v wmctrl) ]] && [[ ! $(command -v xdotool) ]]; then
|
||||||
@ -314,6 +335,8 @@ check_unmerged(){
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
check_version(){
|
check_version(){
|
||||||
|
echo "# Checking version"
|
||||||
|
[[ -n $reference_branch ]] && return
|
||||||
local version_url=$(format_version_url)
|
local version_url=$(format_version_url)
|
||||||
local upstream=$(curl -Ls "$version_url" | awk -F= '/^version=/ {print $2}')
|
local upstream=$(curl -Ls "$version_url" | awk -F= '/^version=/ {print $2}')
|
||||||
local res=$(get_response_code "$version_url")
|
local res=$(get_response_code "$version_url")
|
||||||
@ -363,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
|
||||||
}
|
}
|
||||||
@ -377,10 +400,11 @@ prompt_dl(){
|
|||||||
dl_changelog(){
|
dl_changelog(){
|
||||||
local mdbranch
|
local mdbranch
|
||||||
local md
|
local md
|
||||||
|
source "$config_file"
|
||||||
[[ $branch == "stable" ]] && mdbranch="dzgui"
|
[[ $branch == "stable" ]] && mdbranch="dzgui"
|
||||||
[[ $branch == "testing" ]] && mdbranch="testing"
|
[[ $branch == "testing" ]] && mdbranch="testing"
|
||||||
local md="$url_prefix/${mdbranch}/$file"
|
local changelog="$url_prefix/${mdbranch}/CHANGELOG.md"
|
||||||
curl -Ls "$md" > "$state_path/CHANGELOG.md"
|
curl -Ls "$changelog" > "$state_path/CHANGELOG.md"
|
||||||
}
|
}
|
||||||
test_display_mode(){
|
test_display_mode(){
|
||||||
pgrep -a gamescope | grep -q "generate-drm-mode"
|
pgrep -a gamescope | grep -q "generate-drm-mode"
|
||||||
@ -391,6 +415,7 @@ test_display_mode(){
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
check_architecture(){
|
check_architecture(){
|
||||||
|
echo "# Checking system architecture"
|
||||||
local cpu=$(< /proc/cpuinfo awk -F": " '/AMD Custom APU [0-9]{4}$/ {print $2; exit}')
|
local cpu=$(< /proc/cpuinfo awk -F": " '/AMD Custom APU [0-9]{4}$/ {print $2; exit}')
|
||||||
read -a APU_MODEL <<< "$cpu"
|
read -a APU_MODEL <<< "$cpu"
|
||||||
if [[ ${APU_MODEL[3]} != "0932" ]] && [[ ${APU_MODEL[3]} != "0405" ]]; then
|
if [[ ${APU_MODEL[3]} != "0932" ]] && [[ ${APU_MODEL[3]} != "0405" ]]; then
|
||||||
@ -407,6 +432,7 @@ check_architecture(){
|
|||||||
logger INFO "Setting architecture to 'Steam Deck'"
|
logger INFO "Setting architecture to 'Steam Deck'"
|
||||||
}
|
}
|
||||||
check_map_count(){
|
check_map_count(){
|
||||||
|
echo "# Checking map count"
|
||||||
[[ $is_steam_deck -gt 0 ]] && return 0
|
[[ $is_steam_deck -gt 0 ]] && return 0
|
||||||
local map_count_file="/proc/sys/vm/max_map_count"
|
local map_count_file="/proc/sys/vm/max_map_count"
|
||||||
local min_count=1048576
|
local min_count=1048576
|
||||||
@ -456,6 +482,7 @@ tdialog(){
|
|||||||
$steamsafe_zenity --info --text="$1" "${zenity_flags[@]}"
|
$steamsafe_zenity --info --text="$1" "${zenity_flags[@]}"
|
||||||
}
|
}
|
||||||
steam_deps(){
|
steam_deps(){
|
||||||
|
echo "# Checking Steam"
|
||||||
local flatpak
|
local flatpak
|
||||||
local steam
|
local steam
|
||||||
[[ $(command -v flatpak) ]] && flatpak=$(flatpak list | grep valvesoftware.Steam)
|
[[ $(command -v flatpak) ]] && flatpak=$(flatpak list | grep valvesoftware.Steam)
|
||||||
@ -464,56 +491,70 @@ steam_deps(){
|
|||||||
local msg="Found neither Steam nor Flatpak Steam"
|
local msg="Found neither Steam nor Flatpak Steam"
|
||||||
raise_error_and_quit "$msg"
|
raise_error_and_quit "$msg"
|
||||||
exit 1
|
exit 1
|
||||||
elif [[ -n "$steam" ]] && [[ -n "$flatpak" ]]; then
|
|
||||||
[[ -n $preferred_client ]] && return 0
|
|
||||||
if [[ -z $preferred_client ]]; then
|
|
||||||
preferred_client="steam"
|
|
||||||
fi
|
|
||||||
elif [[ -n "$steam" ]]; then
|
|
||||||
preferred_client="steam"
|
|
||||||
else
|
|
||||||
preferred_client="flatpak"
|
|
||||||
fi
|
fi
|
||||||
update_config
|
|
||||||
logger INFO "Preferred client set to '$preferred_client'"
|
|
||||||
}
|
|
||||||
migrate_files(){
|
|
||||||
if [[ ! -f $config_path/dztuirc.oldapi ]]; then
|
|
||||||
cp $config_file $config_path/dztuirc.oldapi
|
|
||||||
logger INFO "Migrated old API file"
|
|
||||||
fi
|
|
||||||
[[ ! -f $hist_file ]] && return
|
|
||||||
rm $hist_file
|
|
||||||
logger INFO "Wiped old history file"
|
|
||||||
}
|
}
|
||||||
stale_symlinks(){
|
stale_symlinks(){
|
||||||
|
echo "# Cleaning stale symlinks"
|
||||||
local game_dir="$steam_path/steamapps/common/DayZ"
|
local game_dir="$steam_path/steamapps/common/DayZ"
|
||||||
for l in "$(find "$game_dir" -xtype l)"; do
|
readarray -t links < <(find "$game_dir" -xtype l)
|
||||||
logger DEBUG "Updating stale symlink '$l'"
|
for link in "${links[@]}"; do
|
||||||
unlink "$l"
|
logger DEBUG "Updating stale symlink '$link'"
|
||||||
|
unlink "$link"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_availability() {
|
||||||
|
if [[ -z $1 ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local url=$1
|
||||||
|
local timeout_sec="2"
|
||||||
|
if [[ $2 ]]; then
|
||||||
|
timeout_sec=$2
|
||||||
|
fi
|
||||||
|
if ! ping -w "$timeout_sec" "$url" > /dev/null 2>&1; then
|
||||||
|
logger WARN "Failed to reach $url, service may be down."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
local_latlon(){
|
local_latlon(){
|
||||||
|
echo "# Checking coordinates"
|
||||||
if [[ -z $(command -v dig) ]]; then
|
if [[ -z $(command -v dig) ]]; then
|
||||||
local local_ip=$(curl -Ls "https://ipecho.net/plain")
|
local url_ipecho="https://ipecho.net/plain"
|
||||||
|
if ! check_availability "ipecho.net"; then
|
||||||
|
logger WARN "Failed to get external ip address, ipecho.net service may be down."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local local_ip=$(curl -Ls "$url_ipecho")
|
||||||
else
|
else
|
||||||
|
# TODO : implement checking remote
|
||||||
local local_ip=$(dig -4 +short myip.opendns.com @resolver1.opendns.com)
|
local local_ip=$(dig -4 +short myip.opendns.com @resolver1.opendns.com)
|
||||||
fi
|
fi
|
||||||
local url="http://ip-api.com/json/$local_ip"
|
if calc_local_coords "$local_ip"; then
|
||||||
local res=$(curl -Ls "$url" | jq -r '"\(.lat)\n\(.lon)"')
|
return 0
|
||||||
|
fi
|
||||||
|
local url_ip_api="http://ip-api.com/json/$local_ip"
|
||||||
|
if ! check_availability "ip-api.com"; then
|
||||||
|
logger WARN "Failed to get local coordinates, ip-api.com service may be down."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local res=$(curl -Ls "$url_ip_api" | jq -r '"\(.lat)\n\(.lon)"')
|
||||||
if [[ -z "$res" ]]; then
|
if [[ -z "$res" ]]; then
|
||||||
logger WARN "Failed to get local coordinates"
|
logger WARN "Failed to get local coordinates"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
echo "$res" > "$coords_file"
|
echo "$res" > "$coords_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
lock(){
|
lock(){
|
||||||
|
echo "# Setting up lock file"
|
||||||
[[ ! -f $lock_file ]] && touch $lock_file
|
[[ ! -f $lock_file ]] && touch $lock_file
|
||||||
local pid=$(cat $lock_file)
|
local pid=$(cat $lock_file)
|
||||||
ps -p $pid -o pid= >/dev/null 2>&1
|
ps -p $pid -o pid= >/dev/null 2>&1
|
||||||
res=$?
|
res=$?
|
||||||
if [[ $res -eq 0 ]]; then
|
if [[ $res -eq 0 ]]; then
|
||||||
local msg="DZGUI already running ($pid)"
|
local msg="DZGUI is already running ($pid)"
|
||||||
raise_error_and_quit "$msg"
|
raise_error_and_quit "$msg"
|
||||||
elif [[ $pid == $$ ]]; then
|
elif [[ $pid == $$ ]]; then
|
||||||
:
|
:
|
||||||
@ -542,13 +583,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="9caed1445c45832f4af87736ba3f9637"
|
local sum="0a334e1e144e76e560419d155435c91e"
|
||||||
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=3088bbfb147b77bc7b6a9425581b439889ff3f7f
|
local sha=a22a9f428cbe075d7dda62f78000296955eea92a
|
||||||
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"
|
||||||
@ -585,11 +626,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=(
|
||||||
["ui.py"]="f128a97e744e9e11036d707198feb8a8"
|
["funcs"]="5523d933e9f0668fc14b3faf0d4aafd8"
|
||||||
["query_v2.py"]="55d339ba02512ac69de288eb3be41067"
|
["query_v2.py"]="55d339ba02512ac69de288eb3be41067"
|
||||||
|
["servers.py"]="ed442c3aecf33f777d59dcf53650d263"
|
||||||
|
["ui.py"]="d09b9d8bed3854efd51377289786c5ac"
|
||||||
["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397"
|
["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397"
|
||||||
["funcs"]="94287c75a5ae0a06a95dd439711e6fba"
|
["pefile.py"]="b452974a84bff1d821872fcebf59e380"
|
||||||
["lan"]="c62e84ddd1457b71a85ad21da662b9af"
|
|
||||||
)
|
)
|
||||||
local author="aclist"
|
local author="aclist"
|
||||||
local repo="dztui"
|
local repo="dztui"
|
||||||
@ -608,6 +650,10 @@ fetch_helpers_by_sum(){
|
|||||||
realbranch="dzgui"
|
realbranch="dzgui"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -n $reference_branch ]]; then
|
||||||
|
realbranch="$reference_branch"
|
||||||
|
fi
|
||||||
|
|
||||||
for i in "${!sums[@]}"; do
|
for i in "${!sums[@]}"; do
|
||||||
file="$i"
|
file="$i"
|
||||||
sum="${sums[$i]}"
|
sum="${sums[$i]}"
|
||||||
@ -631,7 +677,6 @@ fetch_helpers_by_sum(){
|
|||||||
logger INFO "Updated '$full_path' to sum '$sum'"
|
logger INFO "Updated '$full_path' to sum '$sum'"
|
||||||
fi
|
fi
|
||||||
[[ $file == "funcs" ]] && chmod +x "$full_path"
|
[[ $file == "funcs" ]] && chmod +x "$full_path"
|
||||||
[[ $file == "lan" ]] && chmod +x "$full_path"
|
|
||||||
done
|
done
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@ -646,15 +691,12 @@ fetch_km_helper(){
|
|||||||
}
|
}
|
||||||
get_response_code(){
|
get_response_code(){
|
||||||
local url="$1"
|
local url="$1"
|
||||||
curl -Ls -I -o /dev/null -w "%{http_code}" "$url"
|
curl --connect-timeout 3 -Ls -I -o /dev/null -w "%{http_code}" "$url"
|
||||||
}
|
|
||||||
raise_error_and_quit(){
|
|
||||||
echo "$1"
|
|
||||||
exit 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch_ip_db(){
|
fetch_ip_db(){
|
||||||
parse_dl_url(){
|
parse_dl_url(){
|
||||||
curl -Ls "$url" \
|
curl --connect-timeout 3 -Ls "$url" \
|
||||||
| grep "csv.gz" \
|
| grep "csv.gz" \
|
||||||
| awk -F"['']" '{print $2}'
|
| awk -F"['']" '{print $2}'
|
||||||
}
|
}
|
||||||
@ -670,6 +712,9 @@ fetch_ip_db(){
|
|||||||
fetch(){
|
fetch(){
|
||||||
logger INFO "Triggering fetch routine"
|
logger INFO "Triggering fetch routine"
|
||||||
local url="$1"
|
local url="$1"
|
||||||
|
local ip_file="${state_path}/ips.csv"
|
||||||
|
local base_file="${state_path}/${this_month}.csv.gz"
|
||||||
|
local extracted_file="${state_path}/${this_month}.csv"
|
||||||
|
|
||||||
curl -Ls "$url" > "$base_file"
|
curl -Ls "$url" > "$base_file"
|
||||||
if [[ $? -ne 0 ]]; then
|
if [[ $? -ne 0 ]]; then
|
||||||
@ -689,7 +734,7 @@ fetch_ip_db(){
|
|||||||
logger WARN "Abnormal exit while parsing IPs"
|
logger WARN "Abnormal exit while parsing IPs"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
rm "${state_path}/${month}.csv"
|
rm "${state_path}/${this_month}.csv"
|
||||||
|
|
||||||
readarray -t records < <(cat $ip_file | awk -F, 'NR==1 {print $1} END {print $1}')
|
readarray -t records < <(cat $ip_file | awk -F, 'NR==1 {print $1} END {print $1}')
|
||||||
if [[ ${records[0]} != "0.0.0.0" ]] && [[ ${records[1]} != "224.0.0.0" ]]; then
|
if [[ ${records[0]} != "0.0.0.0" ]] && [[ ${records[1]} != "224.0.0.0" ]]; then
|
||||||
@ -704,55 +749,57 @@ fetch_ip_db(){
|
|||||||
rm "$ip_file"
|
rm "$ip_file"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$month" > "$month_file"
|
echo "$this_month" > "$month_file"
|
||||||
logger INFO "Wrote '$month' to stub '$month_file'"
|
logger INFO "Wrote '$this_month' to stub '$month_file'"
|
||||||
logger INFO "Updated '$ip_file'"
|
logger INFO "Updated '$ip_file'"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_remote(){
|
||||||
|
local url="$1"
|
||||||
|
local res=$(get_response_code "$url")
|
||||||
|
if [[ $res -ne 200 ]]; then
|
||||||
|
logger WARN "Failed to retrieve remote resource: '$url' ($res)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
logger INFO "Resolved remote URL: '$url'"
|
||||||
|
|
||||||
|
# test dl url
|
||||||
|
local dl_url="$(parse_dl_url)"
|
||||||
|
local res=$(get_response_code "$dl_url")
|
||||||
|
if [[ $res -ne 200 ]]; then
|
||||||
|
logger WARN "Remote resource unavailable: '$dl_url' ($res)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
logger INFO "Resolved download URL: '$dl_url'"
|
||||||
|
fetch "$dl_url"
|
||||||
|
}
|
||||||
|
|
||||||
local url="https://db-ip.com/db/download/ip-to-city-lite"
|
local url="https://db-ip.com/db/download/ip-to-city-lite"
|
||||||
local month_file="${state_path}/.month"
|
month_file="${state_path}/.month"
|
||||||
local ip_file="${state_path}/ips.csv"
|
this_month=$(date +%Y-%m)
|
||||||
|
|
||||||
# test main url
|
|
||||||
local res=$(get_response_code "$url")
|
|
||||||
if [[ $res -ne 200 ]]; then
|
|
||||||
logger WARN "Failed to retrieve remote resource: '$url' ($res)"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
logger INFO "Resolved remote URL: '$url'"
|
|
||||||
|
|
||||||
# test dl url
|
|
||||||
local dl_url="$(parse_dl_url)"
|
|
||||||
local res=$(get_response_code "$dl_url")
|
|
||||||
if [[ $res -ne 200 ]]; then
|
|
||||||
logger WARN "Remote resource unavailable: '$dl_url' ($res)"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
logger INFO "Resolved download URL: '$dl_url'"
|
|
||||||
|
|
||||||
local month=$(parse_dl_url_date "$dl_url")
|
|
||||||
local base_file="${state_path}/${month}.csv.gz"
|
|
||||||
local extracted_file="${state_path}/${month}.csv"
|
|
||||||
|
|
||||||
# no stub file
|
# no stub file
|
||||||
if [[ ! -f $month_file ]]; then
|
if [[ ! -f $month_file ]]; then
|
||||||
logger WARN "No stub file '$month_file' present"
|
logger WARN "No stub file '$month_file' present"
|
||||||
fetch "$dl_url"
|
check_remote "$url"
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# local needs update
|
|
||||||
local last_month=$(< "$month_file")
|
|
||||||
if [[ $last_month != "$month" ]]; then
|
|
||||||
logger WARN "Local stub '$last_month' does not match remote stub '$month'"
|
|
||||||
fetch "$dl_url"
|
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# if stub is same date, abort
|
# if stub is same date, abort
|
||||||
logger INFO "Local stub '$last_month' is identical to remote, skipping"
|
local last_month=$(< "$month_file")
|
||||||
|
if [[ $last_month == "$this_month" ]]; then
|
||||||
|
logger INFO "Local stub '$last_month' is identical to remote, skipping"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# local needs update
|
||||||
|
logger WARN "Local stub '$last_month' does not match '$this_month'"
|
||||||
|
check_remote "$url"
|
||||||
|
return
|
||||||
|
|
||||||
}
|
}
|
||||||
fetch_helpers(){
|
fetch_helpers(){
|
||||||
|
echo "# Checking helper files"
|
||||||
fetch_a2s
|
fetch_a2s
|
||||||
fetch_dzq
|
fetch_dzq
|
||||||
fetch_km_helper
|
fetch_km_helper
|
||||||
@ -851,7 +898,7 @@ file_picker(){
|
|||||||
}
|
}
|
||||||
find_library_folder(){
|
find_library_folder(){
|
||||||
local search_path="$1"
|
local search_path="$1"
|
||||||
readarray -t paths < <(python3 "$helpers_path/vdf2json.py" -i "$1/steamapps/libraryfolders.vdf" \
|
readarray -t paths < <(python3.13 "$helpers_path/vdf2json.py" -i "$1/steamapps/libraryfolders.vdf" \
|
||||||
| jq -r '.libraryfolders[]|select(.apps|has("221100")).path')
|
| jq -r '.libraryfolders[]|select(.apps|has("221100")).path')
|
||||||
if [[ ! $? -eq 0 ]]; then
|
if [[ ! $? -eq 0 ]]; then
|
||||||
logger WARN "Failed to parse Steam path using '$search_path'"
|
logger WARN "Failed to parse Steam path using '$search_path'"
|
||||||
@ -879,6 +926,7 @@ create_config(){
|
|||||||
unset default_steam_path
|
unset default_steam_path
|
||||||
unset steam_path
|
unset steam_path
|
||||||
|
|
||||||
|
preferred_client="steam"
|
||||||
while true; do
|
while true; do
|
||||||
local player_input="$($steamsafe_zenity \
|
local player_input="$($steamsafe_zenity \
|
||||||
--forms \
|
--forms \
|
||||||
@ -939,8 +987,9 @@ create_config(){
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
varcheck(){
|
varcheck(){
|
||||||
|
echo "# Checking config file"
|
||||||
local msg="Config file '$config_file' missing. Start first-time setup now?"
|
local msg="Config file '$config_file' missing. Start first-time setup now?"
|
||||||
local msg2="The Steam paths set in your config file appear to be invalid. Restart first-time setup now?"
|
local msg2="The Steam paths set in your config file appear to be invalid (DayZ was moved or uninstalled). Restart first-time setup now?"
|
||||||
if [[ ! -f $config_file ]]; then
|
if [[ ! -f $config_file ]]; then
|
||||||
qdialog "$msg" "Yes" "Exit"
|
qdialog "$msg" "Yes" "Exit"
|
||||||
if [[ $? -eq 1 ]]; then
|
if [[ $? -eq 1 ]]; then
|
||||||
@ -963,18 +1012,16 @@ varcheck(){
|
|||||||
create_config
|
create_config
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
if [[ $src_path != $(realpath "$0") ]]; then
|
|
||||||
src_path=$(realpath "$0")
|
|
||||||
update_config
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
is_dzg_downloading(){
|
is_dzg_downloading(){
|
||||||
|
echo "# Checking DayZ"
|
||||||
if [[ -d $steam_path ]] && [[ -d $steam_path/downloading/$aid ]]; then
|
if [[ -d $steam_path ]] && [[ -d $steam_path/downloading/$aid ]]; then
|
||||||
logger WARN "DayZ may be scheduling updates"
|
logger WARN "DayZ may be scheduling updates"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
is_steam_running(){
|
is_steam_running(){
|
||||||
|
echo "# Checking Steam"
|
||||||
local res=$(ps aux | grep "steamwebhelper" | grep -v grep)
|
local res=$(ps aux | grep "steamwebhelper" | grep -v grep)
|
||||||
if [[ -z $res ]]; then
|
if [[ -z $res ]]; then
|
||||||
logger WARN "Steam may not be running"
|
logger WARN "Steam may not be running"
|
||||||
@ -987,6 +1034,7 @@ get_response_code(){
|
|||||||
curl -Ls -I -o /dev/null -w "%{http_code}" "$url"
|
curl -Ls -I -o /dev/null -w "%{http_code}" "$url"
|
||||||
}
|
}
|
||||||
test_connection(){
|
test_connection(){
|
||||||
|
echo "# Testing connection"
|
||||||
declare -A hr
|
declare -A hr
|
||||||
local res1
|
local res1
|
||||||
local res2
|
local res2
|
||||||
@ -1020,6 +1068,8 @@ legacy_cols(){
|
|||||||
mv $cols_file.new $cols_file
|
mv $cols_file.new $cols_file
|
||||||
}
|
}
|
||||||
stale_mod_signatures(){
|
stale_mod_signatures(){
|
||||||
|
echo "# Checking stale mods"
|
||||||
|
[[ ! -f "$versions_file" ]] && return
|
||||||
local workshop_dir="$steam_path/steamapps/workshop/content/$aid"
|
local workshop_dir="$steam_path/steamapps/workshop/content/$aid"
|
||||||
if [[ -d $workshop_dir ]]; then
|
if [[ -d $workshop_dir ]]; then
|
||||||
readarray -t old_mod_ids < <(awk -F, '{print $1}' $versions_file)
|
readarray -t old_mod_ids < <(awk -F, '{print $1}' $versions_file)
|
||||||
@ -1032,18 +1082,48 @@ stale_mod_signatures(){
|
|||||||
|
|
||||||
}
|
}
|
||||||
create_new_links(){
|
create_new_links(){
|
||||||
|
echo "# Setting up symlinks"
|
||||||
"$func_helper" "update_symlinks"
|
"$func_helper" "update_symlinks"
|
||||||
}
|
}
|
||||||
|
calc_local_coords(){
|
||||||
|
local ip="$1"
|
||||||
|
[[ ! -f "$geo_helper" ]] && return 1
|
||||||
|
|
||||||
|
IFS="." read -ra split <<< "$ip"
|
||||||
|
[[ ${#split[@]} -ne 4 ]] && return 1
|
||||||
|
|
||||||
|
prefix="^${split[0]}.${split[1]}."
|
||||||
|
readarray -t res < <(grep "$prefix" "$geo_helper")
|
||||||
|
|
||||||
|
for address in "${res[@]}"; do
|
||||||
|
IFS="," read -ra fields <<< "$address"
|
||||||
|
upper="${fields[1]}"
|
||||||
|
IFS="." read -ra upper_digits <<< "$upper"
|
||||||
|
if [[ ${split[2]} -gt ${upper_digits[2]} ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ ${split[2]} -eq ${upper_digits[2]} ]]; then
|
||||||
|
if [[ ${split[3]} -gt ${upper_digits[3]} ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ ${split[3]} -eq ${upper_digits[3]} ]]; then
|
||||||
|
echo "${fields[-2]}" >> "$coords_file"
|
||||||
|
echo "${fields[-1]}" >> "$coords_file"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ ${split[2]} -lt ${upper_digits[2]} ]]; then
|
||||||
|
echo "${fields[-2]}" >> "$coords_file"
|
||||||
|
echo "${fields[-1]}" >> "$coords_file"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
initial_setup(){
|
initial_setup(){
|
||||||
setup_dirs
|
|
||||||
setup_state_files
|
|
||||||
depcheck
|
|
||||||
check_pyver
|
|
||||||
test_gobject
|
|
||||||
watcher_deps
|
|
||||||
check_architecture
|
check_architecture
|
||||||
test_connection
|
test_connection
|
||||||
fetch_helpers > >(pdialog "Checking helper files")
|
fetch_helpers
|
||||||
varcheck
|
varcheck
|
||||||
source "$config_file"
|
source "$config_file"
|
||||||
lock
|
lock
|
||||||
@ -1052,14 +1132,12 @@ initial_setup(){
|
|||||||
check_version
|
check_version
|
||||||
check_map_count
|
check_map_count
|
||||||
steam_deps
|
steam_deps
|
||||||
migrate_files
|
|
||||||
stale_symlinks
|
stale_symlinks
|
||||||
stale_mod_signatures
|
stale_mod_signatures
|
||||||
create_new_links
|
create_new_links
|
||||||
local_latlon
|
local_latlon
|
||||||
is_steam_running
|
is_steam_running
|
||||||
is_dzg_downloading
|
is_dzg_downloading
|
||||||
print_config_vals
|
|
||||||
}
|
}
|
||||||
uninstall(){
|
uninstall(){
|
||||||
_full(){
|
_full(){
|
||||||
@ -1103,22 +1181,82 @@ uninstall(){
|
|||||||
rm "$self"
|
rm "$self"
|
||||||
echo "Uninstall routine complete"
|
echo "Uninstall routine complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
usage(){
|
||||||
|
cat <<- EOM
|
||||||
|
DZGUI - Free and Open Source DayZ launcher
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
dzgui.sh [options]
|
||||||
|
|
||||||
|
Description:
|
||||||
|
|
||||||
|
When no option is provided, the script launches DZGUI.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
-u, --uninstall:
|
||||||
|
Uninstalls the software
|
||||||
|
|
||||||
|
-v, --version:
|
||||||
|
Prints the version
|
||||||
|
|
||||||
|
-h, --help:
|
||||||
|
Prints this message
|
||||||
|
EOM
|
||||||
|
}
|
||||||
|
|
||||||
main(){
|
main(){
|
||||||
local zenv=$(zenity --version 2>/dev/null)
|
# setup zenity environment
|
||||||
|
local zenv=""
|
||||||
|
zenv=$(zenity --version 2>/dev/null)
|
||||||
[[ -z $zenv ]] && { echo "Requires zenity >= ${deps[$steamsafe_zenity]}"; exit 1; }
|
[[ -z $zenv ]] && { echo "Requires zenity >= ${deps[$steamsafe_zenity]}"; exit 1; }
|
||||||
if [[ $1 == "--uninstall" ]] || [[ $1 == "-u" ]]; then
|
|
||||||
uninstall &&
|
# parse arguments
|
||||||
exit 0
|
while [[ $# -gt 0 ]]; do
|
||||||
fi
|
case $1 in
|
||||||
|
"--uninstall" | "-u")
|
||||||
|
uninstall
|
||||||
|
exit 0
|
||||||
|
# shift
|
||||||
|
;;
|
||||||
|
"--version" | "-v")
|
||||||
|
echo $version
|
||||||
|
exit 0
|
||||||
|
# shift
|
||||||
|
;;
|
||||||
|
"--help" | "-h")
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
# shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unrecognized command!"
|
||||||
|
return 1
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
set_im_module
|
set_im_module
|
||||||
|
|
||||||
printf "Initializing setup...\n"
|
printf "Checking dependencies...\n"
|
||||||
initial_setup
|
setup_dirs
|
||||||
|
setup_state_files
|
||||||
|
depcheck
|
||||||
|
check_pyver
|
||||||
|
test_gobject
|
||||||
|
watcher_deps
|
||||||
|
|
||||||
|
initial_setup > >(pdialog "Initializing setup")
|
||||||
|
|
||||||
printf "All OK. Kicking off UI...\n"
|
printf "All OK. Kicking off UI...\n"
|
||||||
python3 "$ui_helper" "--init-ui" "$version" "$is_steam_deck"
|
python3.13 "$ui_helper" "--init-ui" "$version" "$is_steam_deck"
|
||||||
|
|
||||||
}
|
}
|
||||||
main "$@"
|
|
||||||
#TODO: tech debt: cruddy handling for steam forking
|
if [[ $(basename "$0") == "dzgui.sh" ]]; then
|
||||||
[[ $? -eq 1 ]] && pkill -f dzgui.sh
|
main "$@"
|
||||||
|
|
||||||
|
#TODO: tech debt: cruddy handling for steam forking
|
||||||
|
[[ $? -eq 1 ]] && pkill -f dzgui.sh
|
||||||
|
fi
|
||||||
|
|||||||
548
helpers/funcs
@ -1,9 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
version="5.8.0"
|
version="6.0.2"
|
||||||
|
|
||||||
#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,9 +47,11 @@ _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"
|
||||||
|
_cache_src_path="$cache_path/$prefix.src"
|
||||||
|
|
||||||
#XDG
|
#XDG
|
||||||
freedesktop_path="$HOME/.local/share/applications"
|
freedesktop_path="$HOME/.local/share/applications"
|
||||||
@ -82,38 +85,28 @@ 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"
|
||||||
|
|
||||||
if [[ $preferred_client == "steam" ]]; then
|
steam_cmd="$preferred_client"
|
||||||
steam_cmd="steam"
|
|
||||||
else
|
|
||||||
steam_cmd="flatpak run com.valvesoftware.Steam"
|
|
||||||
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 client"]="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"
|
||||||
|
["query_favorites"]="query_favorites"
|
||||||
["start_cooldown"]="start_cooldown"
|
["start_cooldown"]="start_cooldown"
|
||||||
["List installed mods"]="list_mods"
|
["List installed mods"]="list_mods"
|
||||||
["Delete selected mods"]="delete_local_mod"
|
["Delete selected mods"]="delete_local_mod"
|
||||||
@ -130,21 +123,31 @@ 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"
|
}
|
||||||
|
handshake(){
|
||||||
|
final_handshake "$aid"
|
||||||
|
}
|
||||||
|
handshake_exp(){
|
||||||
|
final_handshake "$exp"
|
||||||
|
}
|
||||||
|
query_favorites(){
|
||||||
|
if [[ -z "${ip_list[@]}" ]]; then
|
||||||
|
return 1
|
||||||
fi
|
fi
|
||||||
|
printf "%s\n" "${ip_list[@]}"
|
||||||
}
|
}
|
||||||
find_stale_mods(){
|
find_stale_mods(){
|
||||||
local res
|
local res
|
||||||
@ -200,44 +203,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"" \
|
||||||
@ -303,32 +268,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"
|
||||||
@ -343,7 +286,7 @@ a2s(){
|
|||||||
local mode="$3"
|
local mode="$3"
|
||||||
logger INFO "Querying '$ip:$qport' with mode '$mode'"
|
logger INFO "Querying '$ip:$qport' with mode '$mode'"
|
||||||
local res
|
local res
|
||||||
res=$(python3 "$query_helper" "$ip" "$qport" "$mode")
|
res=$(python3.13 "$query_helper" "$ip" "$qport" "$mode")
|
||||||
if [[ $? -eq 1 ]]; then
|
if [[ $? -eq 1 ]]; then
|
||||||
res=$(try_fallback "$ip" "$qport" "$mode")
|
res=$(try_fallback "$ip" "$qport" "$mode")
|
||||||
if [[ $? -eq 1 ]]; then
|
if [[ $? -eq 1 ]]; then
|
||||||
@ -366,7 +309,7 @@ is_in_favs(){
|
|||||||
find_id(){
|
find_id(){
|
||||||
local file="$default_steam_path/config/loginusers.vdf"
|
local file="$default_steam_path/config/loginusers.vdf"
|
||||||
[[ ! -f $file ]] && return 1
|
[[ ! -f $file ]] && return 1
|
||||||
local res=$(python3 "$helpers_path/vdf2json.py" \
|
local res=$(python3.13 "$helpers_path/vdf2json.py" \
|
||||||
-i "$file" \
|
-i "$file" \
|
||||||
| jq -r '.users
|
| jq -r '.users
|
||||||
|to_entries[]
|
|to_entries[]
|
||||||
@ -395,7 +338,7 @@ list_mods(){
|
|||||||
name=$(awk -F\" '/name/ {print $2}' "${dir}/meta.cpp")
|
name=$(awk -F\" '/name/ {print $2}' "${dir}/meta.cpp")
|
||||||
base_dir=$(basename $(readlink -f $game_dir/$symlink))
|
base_dir=$(basename $(readlink -f $game_dir/$symlink))
|
||||||
size=$(du -s "$(readlink -f "$game_dir/$symlink")" | awk '{print $1}')
|
size=$(du -s "$(readlink -f "$game_dir/$symlink")" | awk '{print $1}')
|
||||||
size=$(python3 -c "n=($size/1024) +.005; print(round(n,4))")
|
size=$(python3.13 -c "n=($size/1024) +.005; print(round(n,4))")
|
||||||
LC_NUMERIC=C printf "%s$sep%s$sep%s$sep%3.3f\n" "$name" "$symlink" "$base_dir" "$size"
|
LC_NUMERIC=C printf "%s$sep%s$sep%s$sep%3.3f\n" "$name" "$symlink" "$base_dir" "$size"
|
||||||
done | sort -k1)
|
done | sort -k1)
|
||||||
# user may have manually pruned mods out-of-band
|
# user may have manually pruned mods out-of-band
|
||||||
@ -424,6 +367,10 @@ local_latlon(){
|
|||||||
get_dist(){
|
get_dist(){
|
||||||
shift
|
shift
|
||||||
local given_ip="$1"
|
local given_ip="$1"
|
||||||
|
if [[ ! -f $_cache_coords ]]; then
|
||||||
|
printf "Unknown"
|
||||||
|
return
|
||||||
|
fi
|
||||||
readarray -t coords < "$_cache_coords"
|
readarray -t coords < "$_cache_coords"
|
||||||
readarray -t n < <(<<< "$given_ip" awk 'BEGIN{RS="."}{$1=$1}1')
|
readarray -t n < <(<<< "$given_ip" awk 'BEGIN{RS="."}{$1=$1}1')
|
||||||
|
|
||||||
@ -446,60 +393,11 @@ 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=(
|
||||||
@ -510,6 +408,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
|
||||||
@ -524,154 +423,7 @@ 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"
|
||||||
@ -723,69 +475,13 @@ test_cooldown(){
|
|||||||
local old_time=$(< $_cache_cooldown)
|
local old_time=$(< $_cache_cooldown)
|
||||||
local cur_time=$(date +%s)
|
local cur_time=$(date +%s)
|
||||||
local delta=$(($cur_time - $old_time))
|
local delta=$(($cur_time - $old_time))
|
||||||
if [[ $delta -lt 60 ]]; then
|
if [[ $delta -lt 30 ]]; then
|
||||||
local remains=$((60 - $delta))
|
local remains=$((30 - $delta))
|
||||||
local suffix=$(pluralize "seconds" $remains)
|
local suffix=$(pluralize "seconds" $remains)
|
||||||
printf "Global API cooldown in effect. Please wait %s %s." "$remains" "$suffix"
|
printf "Global API cooldown in effect. Please wait %s %s." "$remains" "$suffix"
|
||||||
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 [[ ! $subcontext =~ Name ]]; then
|
|
||||||
initialize_remote_servers
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*My[[:space:]]saved[[:space:]]servers*)
|
|
||||||
local file="$_cache_my_servers"
|
|
||||||
if [[ ! $subcontext =~ Name ]]; then
|
|
||||||
[[ -f $file ]] && rm $file
|
|
||||||
_iterate "$file" "${ip_list[@]}"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*Recent[[:space:]]servers*)
|
|
||||||
local file="$_cache_history"
|
|
||||||
if [[ ! $subcontext =~ Name ]]; 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 [[ ! $subcontext =~ Name ]]; 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'
|
||||||
}
|
}
|
||||||
@ -893,9 +589,6 @@ default_steam_path="$default_steam_path"
|
|||||||
|
|
||||||
#Preferred Steam launch command (for Flatpak support)
|
#Preferred Steam launch command (for Flatpak support)
|
||||||
preferred_client="$preferred_client"
|
preferred_client="$preferred_client"
|
||||||
|
|
||||||
#DZGUI source path
|
|
||||||
src_path="$src_path"
|
|
||||||
END
|
END
|
||||||
}
|
}
|
||||||
format_version_url(){
|
format_version_url(){
|
||||||
@ -949,6 +642,7 @@ download_new_version(){
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
local version_url="$(format_version_url)"
|
local version_url="$(format_version_url)"
|
||||||
|
local src_path="$(< "$_cache_src_path")"
|
||||||
mv "$src_path" "$src_path.old"
|
mv "$src_path" "$src_path.old"
|
||||||
curl -Ls "$version_url" > "$src_path"
|
curl -Ls "$version_url" > "$src_path"
|
||||||
rc=$?
|
rc=$?
|
||||||
@ -1005,13 +699,6 @@ toggle(){
|
|||||||
debug=""
|
debug=""
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
Toggle[[:space:]]Steam/Flatpak)
|
|
||||||
if [[ $preferred_client == "steam" ]]; then
|
|
||||||
preferred_client="flatpak"
|
|
||||||
else
|
|
||||||
preferred_client="steam"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
Toggle[[:space:]]DZGUI[[:space:]]fullscreen[[:space:]]boot)
|
Toggle[[:space:]]DZGUI[[:space:]]fullscreen[[:space:]]boot)
|
||||||
if [[ $fullscreen == "true" ]]; then
|
if [[ $fullscreen == "true" ]]; then
|
||||||
fullscreen="false"
|
fullscreen="false"
|
||||||
@ -1095,10 +782,10 @@ test_steam_api(){
|
|||||||
}
|
}
|
||||||
test_bm_api(){
|
test_bm_api(){
|
||||||
local key="$1"
|
local key="$1"
|
||||||
if [[ ! $key =~ ^[0-9]+$ ]]; then
|
if [[ -z $key ]]; then
|
||||||
|
echo 1
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
[[ -z $key ]] && return 1
|
|
||||||
local code=$(curl -ILs "$bm_api" \
|
local code=$(curl -ILs "$bm_api" \
|
||||||
-H "Authorization: Bearer "$key"" -G \
|
-H "Authorization: Bearer "$key"" -G \
|
||||||
-d "filter[game]=$game" \
|
-d "filter[game]=$game" \
|
||||||
@ -1110,29 +797,35 @@ update_config_val(){
|
|||||||
local context="$1"
|
local context="$1"
|
||||||
local value="$2"
|
local value="$2"
|
||||||
case $1 in
|
case $1 in
|
||||||
|
"Change client")
|
||||||
|
key="preferred_client"
|
||||||
|
;;
|
||||||
"Change player name")
|
"Change player name")
|
||||||
key="name"
|
key="name"
|
||||||
|
if [[ -z "${value// }" ]]; then
|
||||||
|
printf "Invalid name"
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
"Change Steam API key")
|
"Change Steam API key")
|
||||||
key="steam_api"
|
key="steam_api"
|
||||||
if [[ ${#value} -lt 32 ]] || [[ $(test_steam_api "$value") -eq 1 ]]; then
|
if [[ ${#value} -lt 32 ]] || [[ $(test_steam_api "$value") -eq 1 ]]; then
|
||||||
printf "Invalid API key"
|
printf "Invalid API key"
|
||||||
return 2
|
return 78
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
"Change Battlemetrics API key")
|
"Change Battlemetrics API key")
|
||||||
key="api_key"
|
key="api_key"
|
||||||
if [[ $(test_bm_api "$value") -eq 1 ]]; then
|
if [[ $(test_bm_api "$value") -eq 1 ]]; then
|
||||||
printf "Invalid API key"
|
printf "Invalid API key"
|
||||||
return 2
|
return 79
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
declare -n nr=$key
|
declare -n nr=$key
|
||||||
nr="$value"
|
nr="$value"
|
||||||
update_config
|
update_config
|
||||||
echo "Updated the key '$key' to '$value'"
|
return 80
|
||||||
return 90
|
|
||||||
}
|
}
|
||||||
show_log(){
|
show_log(){
|
||||||
< "$debug_log" sed 's/Keyword␞/Keyword/'
|
< "$debug_log" sed 's/Keyword␞/Keyword/'
|
||||||
@ -1190,24 +883,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
|
||||||
@ -1348,6 +1023,9 @@ update_history(){
|
|||||||
echo "$record" >> "$history_file"
|
echo "$record" >> "$history_file"
|
||||||
}
|
}
|
||||||
update_symlinks(){
|
update_symlinks(){
|
||||||
|
if [[ ! -d "$workshop_dir" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
legacy_symlinks
|
legacy_symlinks
|
||||||
symlinks
|
symlinks
|
||||||
}
|
}
|
||||||
@ -1394,10 +1072,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
|
||||||
@ -1422,11 +1107,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(){
|
||||||
@ -1437,68 +1122,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"
|
||||||
@ -1506,21 +1129,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[@]}"
|
||||||
@ -1559,8 +1171,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"
|
||||||
@ -1570,6 +1180,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
|
||||||
|
|
||||||
@ -1612,6 +1225,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;"
|
||||||
@ -1630,6 +1246,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")
|
||||||
@ -1638,18 +1256,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)
|
||||||
@ -1666,7 +1288,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
|
||||||
@ -1680,6 +1302,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"
|
||||||
@ -1688,7 +1311,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]}"
|
||||||
@ -1738,7 +1360,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
|
||||||
@ -1762,7 +1384,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
|
||||||
|
|||||||
@ -1,36 +0,0 @@
|
|||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <math.h>
|
|
||||||
|
|
||||||
#define R 6371
|
|
||||||
#define TO_RAD (3.1415926536 / 180)
|
|
||||||
double dist(double th1, double ph1, double th2, double ph2)
|
|
||||||
{
|
|
||||||
double dx, dy, dz;
|
|
||||||
ph1 -= ph2;
|
|
||||||
ph1 *= TO_RAD, th1 *= TO_RAD, th2 *= TO_RAD;
|
|
||||||
|
|
||||||
dz = sin(th1) - sin(th2);
|
|
||||||
dx = cos(ph1) * cos(th1) - cos(th2);
|
|
||||||
dy = sin(ph1) * cos(th1);
|
|
||||||
return asin(sqrt(dx * dx + dy * dy + dz * dz) / 2) * 2 * R;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, const char * argv[])
|
|
||||||
{
|
|
||||||
if(argc < 5 || argc > 5){
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
float coords[4];
|
|
||||||
for(int i=1;i<5;i++){
|
|
||||||
if(atof(argv[i]) == 0){
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
coords[i] = atof(argv[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
double d = dist(coords[1], coords[2], coords[3], coords[4]);
|
|
||||||
printf("%.1f\n", d);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
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:
|
||||||
|
raise 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"
|
||||||
472
helpers/servers.py
Normal file
@ -0,0 +1,472 @@
|
|||||||
|
import json
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import typing # noqa
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from urllib import request, parse
|
||||||
|
from urllib.error import HTTPError
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
sys.path.append("a2s")
|
||||||
|
import a2s # noqa
|
||||||
|
from a2s import dayzquery # noqa
|
||||||
|
|
||||||
|
params = [
|
||||||
|
r"\nor\1\map\chernarusplus\nor\1\map\sakhal\nor\1\map\enoch\empty\1\nor\1\map\namalsk", # noqa
|
||||||
|
r"\map\namalsk\empty\1",
|
||||||
|
r"\map\namalsk\noplayers\1",
|
||||||
|
r"\map\chernarusplus\empty\1",
|
||||||
|
r"\map\chernarusplus\noplayers\1",
|
||||||
|
r"\map\sakhal\empty\1",
|
||||||
|
r"\map\sakhal\noplayers\1",
|
||||||
|
r"\map\enoch\empty\1",
|
||||||
|
r"\map\enoch\noplayers\1",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BmAPIError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BmIdError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidIpError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_netmask() -> str:
|
||||||
|
hostname = os.uname()[1]
|
||||||
|
i = socket.gethostbyname(hostname)
|
||||||
|
netmask = i.rsplit(".", 1)[0]
|
||||||
|
return netmask
|
||||||
|
|
||||||
|
|
||||||
|
def test_ip(suffix: int, port: int) -> dict | None:
|
||||||
|
netmask = get_netmask()
|
||||||
|
hostname = f"{netmask}.{str(suffix)}"
|
||||||
|
ping = ["ping", "-c1", "-i", "0.1", "-w", "1"]
|
||||||
|
output = subprocess.run(ping + [hostname], capture_output=True)
|
||||||
|
if output.returncode == 0:
|
||||||
|
return query_direct(hostname, port)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize(name: str) -> str:
|
||||||
|
name = re.sub(r"\r", r"", name)
|
||||||
|
name = re.sub(r"\n", r"", name)
|
||||||
|
name = re.sub(r"\x01", r"", name)
|
||||||
|
name = re.sub(r"\ufeff", r"", name)
|
||||||
|
name = re.sub(r"(^!\s*)", r"-", name)
|
||||||
|
name = re.sub(r"(^-\s*)", r"", name)
|
||||||
|
name = re.sub(r"(^-)", r"", name)
|
||||||
|
name = re.sub(r"(^\s*)", r"", name)
|
||||||
|
name = re.sub(r"\t", "", name)
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def parse_json(json: list) -> list:
|
||||||
|
"""
|
||||||
|
Server metadata is underspecified and server operators
|
||||||
|
tend to insert random garbage in the headers. In case
|
||||||
|
sanitization failed, discard malformed rows rather than
|
||||||
|
aborting outright.
|
||||||
|
"""
|
||||||
|
rows = []
|
||||||
|
for row in json:
|
||||||
|
try:
|
||||||
|
name = sanitize(row["name"])
|
||||||
|
if name == "":
|
||||||
|
continue
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
malformed = False
|
||||||
|
for key in [
|
||||||
|
"map",
|
||||||
|
"gametype",
|
||||||
|
"players",
|
||||||
|
"max_players",
|
||||||
|
"addr",
|
||||||
|
"gameport",
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
row[key]
|
||||||
|
except KeyError:
|
||||||
|
malformed = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if malformed is True:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = row["gametype"].split(",")
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "no3rd" in r:
|
||||||
|
view = "1PP"
|
||||||
|
else:
|
||||||
|
view = "3PP"
|
||||||
|
|
||||||
|
if "external" in r:
|
||||||
|
provider = "Unoffic."
|
||||||
|
else:
|
||||||
|
provider = "Official"
|
||||||
|
|
||||||
|
if "mod" in r:
|
||||||
|
modded = True
|
||||||
|
else:
|
||||||
|
modded = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = row["gametype"].split("lqs")
|
||||||
|
queue = r[1].split(",")[0]
|
||||||
|
except IndexError:
|
||||||
|
queue = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
test_time = re.search(r"[0-9]{2}:[0-9]{2}", row["gametype"])
|
||||||
|
if test_time:
|
||||||
|
time = test_time.group(0)
|
||||||
|
else:
|
||||||
|
time = "Unknown"
|
||||||
|
except AttributeError:
|
||||||
|
time = "Unknown"
|
||||||
|
|
||||||
|
try:
|
||||||
|
ip = row["addr"].split(":")[0] + ":" + str(row["gameport"])
|
||||||
|
qport = row["addr"].split(":")[1]
|
||||||
|
except IndexError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
ping = row["ping"]
|
||||||
|
except KeyError:
|
||||||
|
ping = 9999
|
||||||
|
|
||||||
|
mapname = row["map"].lower()
|
||||||
|
players = row["players"]
|
||||||
|
max_players = row["max_players"]
|
||||||
|
raw = [
|
||||||
|
name,
|
||||||
|
mapname,
|
||||||
|
view,
|
||||||
|
time,
|
||||||
|
int(players),
|
||||||
|
int(max_players),
|
||||||
|
int(queue),
|
||||||
|
ip,
|
||||||
|
int(qport),
|
||||||
|
ping,
|
||||||
|
provider,
|
||||||
|
modded,
|
||||||
|
]
|
||||||
|
rows.append(raw)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def query_direct(ip: str, qport: int, TIMEOUT: float=3.0) -> dict | None:
|
||||||
|
try:
|
||||||
|
info = a2s.info((ip, qport), TIMEOUT)
|
||||||
|
|
||||||
|
name = info.server_name
|
||||||
|
mapname = info.map_name
|
||||||
|
address = ip + ":" + str(qport)
|
||||||
|
gameport = str(info.port)
|
||||||
|
players = info.player_count
|
||||||
|
max_players = info.max_players
|
||||||
|
keywords = info.keywords
|
||||||
|
|
||||||
|
try:
|
||||||
|
ping = info.ping
|
||||||
|
ping = math.floor(info.ping * 1000)
|
||||||
|
except AttributeError:
|
||||||
|
ping = 9999
|
||||||
|
|
||||||
|
res = {}
|
||||||
|
res["name"] = name
|
||||||
|
res["map"] = mapname
|
||||||
|
res["gametype"] = keywords
|
||||||
|
res["players"] = players
|
||||||
|
res["max_players"] = max_players
|
||||||
|
res["addr"] = address
|
||||||
|
res["gameport"] = gameport
|
||||||
|
res["ping"] = ping
|
||||||
|
return res
|
||||||
|
except TimeoutError:
|
||||||
|
return None
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class Res:
|
||||||
|
status: int
|
||||||
|
parsed: bool
|
||||||
|
json: Union[str, None]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class Ping:
|
||||||
|
addr: str
|
||||||
|
iteration: int
|
||||||
|
ping: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class Details:
|
||||||
|
data: Union[list, None]
|
||||||
|
description: str
|
||||||
|
success: 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:
|
||||||
|
info = a2s.info((ip, qport))
|
||||||
|
except TimeoutError:
|
||||||
|
return Prereqs(False, 0, None, None)
|
||||||
|
|
||||||
|
gameport = getattr(info, "port", 0)
|
||||||
|
is_password = getattr(info, "password_protected", False)
|
||||||
|
appid = getattr(info, "game_id", None)
|
||||||
|
version = getattr(info, "version", None)
|
||||||
|
|
||||||
|
return Prereqs(is_password, gameport, appid, version)
|
||||||
|
|
||||||
|
|
||||||
|
def details(ip: str, qport: int) -> Details:
|
||||||
|
default_str = "None provided"
|
||||||
|
|
||||||
|
try:
|
||||||
|
info = a2s.info((ip, qport))
|
||||||
|
except TimeoutError:
|
||||||
|
return Details(None, default_str, False)
|
||||||
|
try:
|
||||||
|
rules = dayzquery.dayz_rules((ip, int(qport)))
|
||||||
|
except TimeoutError:
|
||||||
|
return Details(None, default_str, False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
keywords = info.keywords.split(",")
|
||||||
|
except AttributeError:
|
||||||
|
return Details(None, default_str, False)
|
||||||
|
|
||||||
|
battleye = "Disabled"
|
||||||
|
if "battleye" in keywords:
|
||||||
|
battleye = "Enabled"
|
||||||
|
|
||||||
|
day_accel = 0.0
|
||||||
|
night_accel = 0.0
|
||||||
|
for keyword in keywords:
|
||||||
|
if "etm" in keyword:
|
||||||
|
day_accel = float(keyword.lstrip("etm"))
|
||||||
|
day_accel = float(f"{day_accel:g}")
|
||||||
|
if "entm" in keywords:
|
||||||
|
night_accel = float(keyword.lstrip("entm"))
|
||||||
|
night_accel = float(f"{night_accel:g}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
password = info.password_protected
|
||||||
|
if password is False:
|
||||||
|
password = "Disabled"
|
||||||
|
else:
|
||||||
|
password = "Enabled"
|
||||||
|
except AttributeError:
|
||||||
|
password = "-"
|
||||||
|
|
||||||
|
try:
|
||||||
|
vac = info.vac_enabled
|
||||||
|
if vac is False:
|
||||||
|
vac = "Disabled"
|
||||||
|
else:
|
||||||
|
vac = "Enabled"
|
||||||
|
except AttributeError:
|
||||||
|
vac = "-"
|
||||||
|
|
||||||
|
try:
|
||||||
|
version = info.version
|
||||||
|
except AttributeError:
|
||||||
|
version = "-"
|
||||||
|
|
||||||
|
try:
|
||||||
|
dlc = rules.dlc_flags
|
||||||
|
if dlc == 0:
|
||||||
|
dlc = "None"
|
||||||
|
if dlc == 2:
|
||||||
|
dlc = "Frostline"
|
||||||
|
except AttributeError:
|
||||||
|
dlc = "not specified"
|
||||||
|
|
||||||
|
try:
|
||||||
|
platform = rules.platform
|
||||||
|
if platform == "win":
|
||||||
|
platform = "Windows"
|
||||||
|
if platform == "?":
|
||||||
|
platform = "Linux"
|
||||||
|
except AttributeError:
|
||||||
|
platform = "not specified"
|
||||||
|
|
||||||
|
try:
|
||||||
|
description = rules.description.strip()
|
||||||
|
if description == "":
|
||||||
|
description = default_str
|
||||||
|
except AttributeError:
|
||||||
|
description = default_str
|
||||||
|
|
||||||
|
rows = [
|
||||||
|
["Battleye", battleye],
|
||||||
|
["Daytime acceleration", f"{day_accel}x"],
|
||||||
|
["DLC", dlc],
|
||||||
|
["Night-time acceleration", f"{night_accel}x"],
|
||||||
|
["Password", password],
|
||||||
|
["Platform", platform],
|
||||||
|
["Valve Anti-Cheat", vac],
|
||||||
|
["Version", version],
|
||||||
|
]
|
||||||
|
|
||||||
|
return Details(rows, description, True)
|
||||||
|
|
||||||
|
|
||||||
|
def ping(iteration: int, row: list) -> Ping:
|
||||||
|
addr = row[7]
|
||||||
|
qport = row[8]
|
||||||
|
|
||||||
|
res = None
|
||||||
|
|
||||||
|
if row[9] != 9999:
|
||||||
|
return Ping(addr, iteration, row[9])
|
||||||
|
|
||||||
|
try:
|
||||||
|
ip = addr.split(":")[0]
|
||||||
|
except IndexError:
|
||||||
|
ping = 9999
|
||||||
|
|
||||||
|
try:
|
||||||
|
res = query_direct(ip, qport, 0.5)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if res is None:
|
||||||
|
ping = 9999
|
||||||
|
else:
|
||||||
|
ping = res["ping"]
|
||||||
|
|
||||||
|
return Ping(addr, iteration, ping)
|
||||||
|
|
||||||
|
|
||||||
|
def query_api(key: str, appid: int, param: str) -> Res:
|
||||||
|
LIMIT = 10000
|
||||||
|
url = "https://api.steampowered.com/IGameServersService/GetServerList/v1/?"
|
||||||
|
|
||||||
|
payload: dict[str, Union[int, str]] = {
|
||||||
|
"filter": r"\appid" + fr"\{appid}" + param,
|
||||||
|
"limit": LIMIT,
|
||||||
|
"key": key,
|
||||||
|
}
|
||||||
|
par = parse.urlencode(payload)
|
||||||
|
url = f"{url}{par}"
|
||||||
|
|
||||||
|
status = 200
|
||||||
|
parsed = True
|
||||||
|
data = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with request.urlopen(url) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
status = response.status
|
||||||
|
try:
|
||||||
|
parsed = True
|
||||||
|
data = json.load(response)
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
parsed = False
|
||||||
|
data = None
|
||||||
|
except HTTPError:
|
||||||
|
status = 403
|
||||||
|
parsed = False
|
||||||
|
data = None
|
||||||
|
finally:
|
||||||
|
return Res(status, parsed, data)
|
||||||
|
|
||||||
|
|
||||||
|
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) -> Record:
|
||||||
|
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
|
||||||
1
helpers/tools/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
latlon
|
||||||
24
helpers/tools/Makefile
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
bin := latlon
|
||||||
|
|
||||||
|
CC = gcc
|
||||||
|
|
||||||
|
CFLAGS += -Wall
|
||||||
|
CFLAGS += -Wextra
|
||||||
|
CFLAGS += -Wpedantic
|
||||||
|
CFLAGS += -std=c23
|
||||||
|
CFLAGS += -O3
|
||||||
|
|
||||||
|
LDFLAGS += -lm
|
||||||
|
|
||||||
|
PREFIX ?= $(HOME)/.local/share/dzgui
|
||||||
|
|
||||||
|
all: $(bin)
|
||||||
|
|
||||||
|
install: $(bin)
|
||||||
|
install -Dm 755 $(bin) $(PREFIX)
|
||||||
|
|
||||||
|
$(bin): latlon.c
|
||||||
|
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(bin)
|
||||||
77
helpers/tools/latlon.c
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// This small program computes the distance between two points on the Earths
|
||||||
|
// surface. The algorithm used for computing is called haversine formula.
|
||||||
|
// Source for the algorithm can be found here:
|
||||||
|
// https://en.wikipedia.org/wiki/Haversine_formula
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
// Earth's radius in meters.
|
||||||
|
#define RADIUS ((12756L / 2.0L) * 1000L)
|
||||||
|
|
||||||
|
// PI natural constant
|
||||||
|
#define PI 3.1415926536L
|
||||||
|
|
||||||
|
// coordinates struct for storing points
|
||||||
|
struct Coordinate {
|
||||||
|
double latitude;
|
||||||
|
double longitude;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Calculates a radian value from an angle
|
||||||
|
*/
|
||||||
|
double radian_from(const double angle) {
|
||||||
|
return (angle * PI) / 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Calculate the haversine function from an angle in radian
|
||||||
|
*/
|
||||||
|
double haversine(const double angle) { return ((1.0L - cos(angle)) / 2); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Calculate the distance between two coordinates
|
||||||
|
*/
|
||||||
|
double great_circle_distance(const struct Coordinate a,
|
||||||
|
const struct Coordinate b) {
|
||||||
|
|
||||||
|
// calculate longitude and latitude differences
|
||||||
|
struct Coordinate diff_coord = {
|
||||||
|
.longitude = a.longitude - b.longitude,
|
||||||
|
.latitude = a.latitude - b.latitude,
|
||||||
|
};
|
||||||
|
|
||||||
|
// calculate haversine(theta) value, where theta is the angle between the
|
||||||
|
// two coordinates
|
||||||
|
double hav_theta = haversine(diff_coord.latitude)
|
||||||
|
+ cos(a.latitude) * cos(b.latitude) * haversine(diff_coord.longitude);
|
||||||
|
|
||||||
|
// calculate distance from radius, and haversine(theta) values
|
||||||
|
double distance = 2 * RADIUS * asin(sqrt(hav_theta));
|
||||||
|
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, const char* argv[]) {
|
||||||
|
if (argc < 5 || argc > 5) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const struct Coordinate a = {
|
||||||
|
.latitude = radian_from(atof(argv[1])),
|
||||||
|
.longitude = radian_from(atof(argv[2])),
|
||||||
|
};
|
||||||
|
const struct Coordinate b = {
|
||||||
|
.latitude = radian_from(atof(argv[3])),
|
||||||
|
.longitude = radian_from(atof(argv[4])),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns the distance in meters.
|
||||||
|
// To pretty-print the result in a consumer of this module, it is
|
||||||
|
// recommended to divide the result by 1,000 and round it.
|
||||||
|
printf("%.1f\n", great_circle_distance(a, b));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
6916
helpers/ui.py
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 608 KiB |
|
Before Width: | Height: | Size: 569 KiB |
|
Before Width: | Height: | Size: 417 KiB |
|
Before Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 31 KiB |