mirror of
https://github.com/aclist/dztui.git
synced 2026-08-31 12:16:55 +02:00
Compare commits
128 Commits
abf692e876
...
f4cc35343e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4cc35343e | ||
|
|
5654b327d4 | ||
|
|
0ad6366f0a | ||
|
|
a7c647aa6b | ||
|
|
3d32721683 | ||
|
|
881b0b3008 | ||
|
|
2a59441dbc | ||
|
|
acdb34d36a | ||
|
|
d6d0b241e0 | ||
|
|
2ba07bbba8 | ||
|
|
65eda05d9f | ||
|
|
f3b3f79bfb | ||
|
|
28c7d7f949 | ||
|
|
d072c8107e | ||
|
|
0629c8a6ba | ||
|
|
8aea4af687 | ||
|
|
0562d8230d | ||
|
|
759327e932 | ||
|
|
36753e02f1 | ||
|
|
2ab2e23736 | ||
|
|
34e7b2e480 | ||
|
|
540d0ad7e4 | ||
|
|
47bea45111 | ||
|
|
d03346b56a | ||
|
|
54e0762882 | ||
|
|
0834a86d33 | ||
|
|
c6a8841614 | ||
|
|
df92acb7dc | ||
|
|
a333571f94 | ||
|
|
05f88b6019 | ||
|
|
1f31c43fe4 | ||
|
|
0763416903 | ||
|
|
3b384c5a4d | ||
|
|
3ab666a807 | ||
|
|
c8f7a32284 | ||
|
|
a070e9fc65 | ||
|
|
5f55e3c664 | ||
|
|
07606d8852 | ||
|
|
afffc90abc | ||
|
|
70db7b45b3 | ||
|
|
aac8558079 | ||
|
|
ac8ff2febf | ||
|
|
6369d895fd | ||
|
|
7413f7c029 | ||
|
|
3f1d8cb24f | ||
|
|
4df12605fc | ||
|
|
2c5da88e06 | ||
|
|
3cb6175436 | ||
|
|
3739f19ab2 | ||
|
|
2da4d001d6 | ||
|
|
5d61a1f513 | ||
|
|
56c80d187b | ||
|
|
e08f66cdcb | ||
|
|
48fee301bb | ||
|
|
20511f5661 | ||
|
|
d2e8333ba9 | ||
|
|
5d37039064 | ||
|
|
ce44cdbc54 | ||
|
|
5b2738420e | ||
|
|
1e29288e2a | ||
|
|
745cc06091 | ||
|
|
0311cc8a95 | ||
|
|
7c3521cf97 | ||
|
|
bb0eefadd2 | ||
|
|
7eb5a4fac5 | ||
|
|
ebce329c3d | ||
|
|
23464d3b51 | ||
|
|
602a7b8edc | ||
|
|
59c58a1df5 | ||
|
|
830318d98e | ||
|
|
271c3e6197 | ||
|
|
1ab9634069 | ||
|
|
8666b0055a | ||
|
|
406554e011 | ||
|
|
5334d07f7f | ||
|
|
6cab96d1f8 | ||
|
|
bc94d989d4 | ||
|
|
e4fff5f242 | ||
|
|
53f2fe922a | ||
|
|
800f68a1b7 | ||
|
|
c6c377e8b9 | ||
|
|
7a68d7e097 | ||
|
|
f60a6edf2b | ||
|
|
8c33a1b371 | ||
|
|
e5f7391b89 | ||
|
|
a8f51280e8 | ||
|
|
c19346529b | ||
|
|
85451f765b | ||
|
|
5a144f7815 | ||
|
|
2370e81403 | ||
|
|
c11511abff | ||
|
|
bda7223ac8 | ||
|
|
6b48ee127d | ||
|
|
aa8a52bfc0 | ||
|
|
8ea62434ee | ||
|
|
baeaeb1b64 | ||
|
|
32b424da5c | ||
|
|
d71110d0a1 | ||
|
|
bc9178d2de | ||
|
|
1533febf9a | ||
|
|
c3c8872fad | ||
|
|
bae6a57e1e | ||
|
|
3bdc0057a2 | ||
|
|
5496f914f2 | ||
|
|
cb9db55bcb | ||
|
|
c0ab8111fd | ||
|
|
10c6a79b17 | ||
|
|
2b4b533a5b | ||
|
|
dcb8be9132 | ||
|
|
278aa8c28a | ||
|
|
32e5bafb33 | ||
|
|
59e19510e3 | ||
|
|
45ab112970 | ||
|
|
ed07fd0343 | ||
|
|
c2c7b37b90 | ||
|
|
23798c91d3 | ||
|
|
659cbb83a3 | ||
|
|
32c467fc6d | ||
|
|
d01add3573 | ||
|
|
0de89a3ef1 | ||
|
|
5686ea76c2 | ||
|
|
5fda598173 | ||
|
|
049fb47019 | ||
|
|
81f269b71b | ||
|
|
c97a7a260f | ||
|
|
b715eb5a3e | ||
|
|
8e6cf7e792 | ||
|
|
98868073a5 |
390
CHANGELOG.md
390
CHANGELOG.md
@ -1,363 +1,113 @@
|
||||
# Changelog
|
||||
|
||||
## [6.0.0-beta.7] 2025-09-20
|
||||
## Added
|
||||
- Users can save text notes to describe servers
|
||||
|
||||
## Changed
|
||||
- State file serialization methods
|
||||
|
||||
## [6.0.0-beta.6] 2025-09-12
|
||||
## Added
|
||||
- Support DayZ Experimental
|
||||
- Show additional client information in Options menu
|
||||
- Warn user of client version mismatches
|
||||
- Support clickable hyperlinks
|
||||
|
||||
## Fixed
|
||||
- Mods rarely not appearing in local mod list if download completed too quickly
|
||||
- Statusbar not updating when clicking a row after spamming keyboard input
|
||||
- Extraneous logs being generated when subscribing to mods
|
||||
- Narrow width of columns in modlist dialogs occluding text
|
||||
- Newline terminators in history file
|
||||
- Floating point number calculation
|
||||
- Window resizing too small if no prior resolution was set
|
||||
|
||||
## [6.0.0-beta.5] 2025-08-20
|
||||
## Fixed
|
||||
- Servers returning malformed A2S_INFO blocking server browser from loading
|
||||
|
||||
## [6.0.0-beta.4] 2025-08-13
|
||||
## Fixed
|
||||
- Branch toggle signal being emitted when entering Options menu from other contexts
|
||||
|
||||
## [6.0.0-beta.3] 2025-08-08
|
||||
## Added
|
||||
- Additional server metadata to details dialog
|
||||
- Prompt user when connecting if a server is locked
|
||||
- Dedicated settings page ("Options")
|
||||
|
||||
## Fixed
|
||||
- Statusbar contents not updating on certain pages
|
||||
- Breadcrumbs not updating when returning from keybindings page
|
||||
- Wrap long descriptions in server details
|
||||
- Prevent debug button from activating when in certain input fields
|
||||
- Window size not updating correctly when unmaximizing window after changing "fullscreen at boot" setting
|
||||
|
||||
## Dropped
|
||||
- Extraneous information from right statusbar
|
||||
|
||||
## [6.0.0-beta.2] 2025-07-29
|
||||
## Fixed
|
||||
- Missing parameters when closing application via window decorations
|
||||
|
||||
## [6.0.0-beta.1] 2025-07-29
|
||||
## Added
|
||||
- Ping column to server browsing contexts
|
||||
- Server details dialog to server browsing contexts
|
||||
- Filter by modded servers
|
||||
- Breadcrumbs showing current menu context to top of window
|
||||
- Speed up load time and navigation of server tables
|
||||
- More robust threading and cache system when filtering servers
|
||||
- Dedicated changelog page
|
||||
- Dedicated keybindings page
|
||||
- Vim-style navigation keybindings
|
||||
- Additional validation on entry dialogs to prevent submitting empty text
|
||||
- Added "Return to main menu" button to dialog windows when failing to load server table
|
||||
- Dynamic context menus for modded servers
|
||||
- Additional keybindings for filter toggles
|
||||
|
||||
## Fixed
|
||||
- Entry dialog sensitivity when validating API keys
|
||||
- Normalized buttons in dialogs and restored proper padding
|
||||
- GTK errors being emitted to stdout when inserting debug table
|
||||
- Suppress errors during pre-boot checks when mods are not installed
|
||||
- Centered filter checkboxes within panel
|
||||
- Do not pop unhighlight/select stale buttons if no stale mods exist
|
||||
- Improved keybinding interaction with side panels
|
||||
- Key stickiness when quickly navigating through entries in tables
|
||||
- Suppress typeahead search in mod dialogs
|
||||
- Do not trigger global API cooldown if no LAN/favorite servers are found
|
||||
- Fix table column expansion in server mod dialogs
|
||||
|
||||
## Changed
|
||||
- Reduced global API cooldown from 60s to 30s
|
||||
- Clarify dialog messages when DayZ path could not be found
|
||||
- Auto-focus first item when opening context menus
|
||||
- Opacity setting on side buttons when in a different context
|
||||
- Refactored BM API key validation to account for new key format
|
||||
|
||||
## Dropped
|
||||
- Ping readout in statusbar
|
||||
|
||||
## [5.8.0-beta.2] 2025-07-06
|
||||
## Added
|
||||
- Filter servers by official/unofficial status
|
||||
## Changed
|
||||
- Updated internal versioning of helper files
|
||||
|
||||
## [5.8.0-beta.1] 2025-06-05
|
||||
## Added
|
||||
- Automatically fetch geolocation records
|
||||
## Fixed
|
||||
- Corrected erroneous 2024 date in prior changelog entries
|
||||
|
||||
## [5.7.1-beta.1] 2025-04-17
|
||||
## Changed
|
||||
- Updated geolocation records
|
||||
|
||||
## [5.7.0-beta.13] 2025-04-17
|
||||
## Fixed
|
||||
- Updated checksum for UI helper file
|
||||
|
||||
## [5.7.0-beta.12] 2025-04-04
|
||||
## Dropped
|
||||
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
|
||||
|
||||
## [5.7.0-beta.11] 2025-03-20
|
||||
## Fixed
|
||||
- Reduce startup time when testing remote APIs
|
||||
|
||||
## [5.7.0-beta.10] 2025-03-15
|
||||
## Added
|
||||
- Restore application size on subsequent startup
|
||||
## Fixed
|
||||
- Grid scaling causes table to exceeds bounds of display viewport
|
||||
- Window exceeds taskbar on Steam Deck
|
||||
- Reduce possibility of timeouts when pinging servers
|
||||
## Changed
|
||||
- Packed filter checkbox buttons into a 3x3 grid
|
||||
- Updated IP database to 2025-03 records
|
||||
|
||||
## [5.7.0-beta.9] 2025-03-04
|
||||
## Fixed
|
||||
- Livonia server results being dropped from batch queries
|
||||
|
||||
## [5.7.0-beta.8] 2025-02-10
|
||||
## Changed
|
||||
- Drop launch flag and check for invocation through Steam automatically
|
||||
|
||||
## [5.7.0-beta.7] 2025-02-09
|
||||
## Changed
|
||||
## [5.6.6] 2024-02-09
|
||||
### Changed
|
||||
- Update IP database records for 2025-02
|
||||
|
||||
## [5.7.0-beta.6] 2025-01-10
|
||||
## Fixed
|
||||
- Resolve regression introduced with IP resolution feature in 13c6813
|
||||
|
||||
## [5.7.0-beta.5] 2025-01-10
|
||||
## Changed
|
||||
- Stricter redacting of usernames (again)
|
||||
|
||||
## [5.7.0-beta.4] 2025-01-10
|
||||
## Fixed
|
||||
- Clerical hotfix
|
||||
|
||||
## [5.7.0-beta.3] 2025-01-10
|
||||
## Changed
|
||||
- Support legacy jq syntax for Ubuntu variants
|
||||
|
||||
## [5.7.0-beta.2] 2025-01-07
|
||||
## Fixed
|
||||
- Stricter redacting of usernames
|
||||
- Omit nested directories when traversing symlinks
|
||||
|
||||
## [5.7.0-beta.1] 2025-01-07
|
||||
### Changed
|
||||
- Normalize version number
|
||||
|
||||
## [5.6.0-beta.21] 2025-01-06
|
||||
### Added
|
||||
- Add in-app documentation link to Codeberg mirror
|
||||
- Hover tooltips to most buttons
|
||||
## [5.6.2] 2024-01-22
|
||||
### Fixed
|
||||
- Prevent ArrowUp/ArrowDown input when inside keyword field
|
||||
- Resolve regression introduced with IP resolution feature in 5.6.0 (restores functionality of right-click action: Add to My Servers)
|
||||
|
||||
## [5.6.1] 2024-01-10
|
||||
### Fixed
|
||||
- Add fallback support for jq 1.6
|
||||
|
||||
## [5.6.0] 2024-01-06
|
||||
### Added
|
||||
- Application header bar and controls
|
||||
- Menu context subtitle in header bar
|
||||
- Bulk delete mods (via 'List installed mods' list). Not compatible with Manual Mod install mode
|
||||
- Highlight stale mods in mods list
|
||||
- Added --steam launch parameter
|
||||
- Open Steam workshop subscriptions dialog when switching from Manual to Auto mode
|
||||
- Hover tooltips to most buttons
|
||||
- Add in-app documentation link to Codeberg mirror
|
||||
- Additional logging
|
||||
### Fixed
|
||||
- Avoid sudo escalation if system map count is sufficient (jiriks74)
|
||||
- Duplicate dialog title on Steam Deck
|
||||
- Resolve remote IP when saving records for game servers with multiple hosts
|
||||
- More performant symlink traversal when checking for legacy links
|
||||
- Only iterate on missing symlinks at boot (400% performance uplift)
|
||||
- Fix for server list truncation causing some servers to not appear in results
|
||||
- Empty dialog popups if user manually deletes local mods while application is running
|
||||
- Update statusbar when removing servers from list/repopulating
|
||||
- Abort DayZ path discovery if Steam files are not synched
|
||||
- Race condition when checking for installed mods
|
||||
- Favorite server message not updating correctly
|
||||
- Improved handling for cases where there are no locally installed mods
|
||||
- Set up mod symlinks at boot, rather than only on server connect
|
||||
- Prevent context menus from opening when table is empty
|
||||
- When reloading table in-place, prevent duplicate panel elements from being added if already present
|
||||
- Clean up local mod signatures from versions file when deleting mods
|
||||
- Focus first row when opening mods list
|
||||
### Changed
|
||||
- Redact usernames in log files
|
||||
- Prevent ArrowUp/ArrowDown input when inside keyword field
|
||||
- Admonish user to restart Steam in error dialog if DayZ path could not be found
|
||||
- Refactor control flow for more robust contextual parsing
|
||||
- Stop sending modal dialog hints to outer window
|
||||
- Clean up signal emission
|
||||
- Clarify some error messages and normalize text formatting
|
||||
- Update forum URL
|
||||
- Reword Help section links to include destination
|
||||
- Update README.md
|
||||
- Update IP database to 2025-01
|
||||
- Reduce IP database size by 50%
|
||||
- Update documentation to 5.6.x standard
|
||||
### Dropped
|
||||
- Removed temporary mod ID output in debug logs
|
||||
- Removed Hall of Fame section from button links, moved inside documentation
|
||||
- Remove unused imports
|
||||
- Hall of Fame section from button links, to be moved inside documentation
|
||||
- Unused imports
|
||||
|
||||
## [5.6.0-beta.20] 2024-12-23
|
||||
### Added
|
||||
- Output real and resolved mod ids to logs (temporary)
|
||||
- Added -steam launch parameter
|
||||
### Fixed
|
||||
- Only iterate on missing symlinks
|
||||
- Move logging up
|
||||
|
||||
## [5.6.0-beta.19] 2024-12-18
|
||||
### Added
|
||||
- Redact usernames in log files
|
||||
### Fixed
|
||||
- More performant symlink traversal when checking for legacy links
|
||||
|
||||
## [5.6.0-beta.18] 2024-12-14
|
||||
### Added
|
||||
- Open Steam workshop subscriptions dialog
|
||||
- Additional logging
|
||||
### Fixed
|
||||
- Empty dialog popups if user manually deletes local mods while application is running
|
||||
- Abort DayZ path discovery if VDF if Steam files are not synched
|
||||
- Avoid sudo escalation if system map count is sufficient (jiriks74)
|
||||
### Changed
|
||||
- Admonish user to restart Steam in error dialog if DayZ path could not be found
|
||||
|
||||
## [5.6.0-beta.17] 2024-12-14
|
||||
### Added
|
||||
- Additional logging
|
||||
|
||||
## [5.6.0-beta.16] 2024-12-13
|
||||
## [5.5.3] 2024-12-13
|
||||
### Fixed
|
||||
- Add remote resource health checks before downloading updates
|
||||
### Added
|
||||
- Add fallback repository
|
||||
|
||||
## [5.6.0-beta.11] 2024-12-07
|
||||
## [5.5.1] 2024-12-03
|
||||
### Fixed
|
||||
- Add missing function definition
|
||||
- Support localized decimal separators when parsing installed mod sizes
|
||||
|
||||
## [5.6.0-beta.10] 2024-12-04
|
||||
### Fixed
|
||||
- Untoggle highlight button when repopulating mod list
|
||||
- Resolve remote IP when saving records for game servers with multiple hosts
|
||||
- Update statusbar when removing servers from list/repopulating
|
||||
### Added:
|
||||
- "Select stale" button to bulk select mods marked as obsolete
|
||||
|
||||
## [5.6.0-beta.9] 2024-12-03
|
||||
### Fixed
|
||||
- Normalize user locale when parsing floats
|
||||
|
||||
## [5.6.0-beta.8] 2024-11-28
|
||||
### Fixed
|
||||
- Normalize user locale when parsing floats
|
||||
|
||||
## [5.6.0-beta.7] 2024-11-28
|
||||
### Changed
|
||||
- Add additional logging when fetching installed mods
|
||||
|
||||
## [5.6.0-beta.6] 2024-11-28
|
||||
### Fixed
|
||||
- Race condition when checking for installed mods
|
||||
|
||||
## [5.6.0-beta.5] 2024-11-21
|
||||
### Added
|
||||
- Highlight stale mods in mods list
|
||||
### Fixed
|
||||
- Duplicate dialog title on Steam Deck
|
||||
|
||||
## [5.6.0-beta.4] 2024-11-20
|
||||
### Added
|
||||
- Application header bar and controls
|
||||
- Menu context subtitle in header bar
|
||||
### Changed
|
||||
- Refactor control flow for more robust contextual parsing
|
||||
- Stop sending modal dialog hints to outer window
|
||||
### Fixed
|
||||
- Favorite server message not updating correctly
|
||||
|
||||
## [5.6.0-beta.3] 2024-11-18
|
||||
### Fixed
|
||||
- Improved handling for cases where there are no locally installed mods
|
||||
- Set up mod symlinks at boot, rather than only on server connect
|
||||
- Prevent context menus from opening when table is empty
|
||||
- When reloading table in-place, prevent duplicate panel elements from being added if already present
|
||||
- Clean up signal emission
|
||||
|
||||
## [5.6.0-beta.2] 2024-11-15
|
||||
### Fixed
|
||||
- Clean up local mod signatures from versions file when deleting mods
|
||||
|
||||
## [5.6.0-beta.1] 2024-11-12
|
||||
### Added
|
||||
- Bulk delete mods (via 'List installed mods' list). Not compatible with Manual Mod install mode
|
||||
### Fixed
|
||||
- Fix for server list truncation causing some servers to not appear in results
|
||||
- Suppress signal emission when switching menu contexts
|
||||
- Focus first row when opening mods list
|
||||
### Changed
|
||||
- Clarify some error messages and normalize text formatting
|
||||
|
||||
## [5.5.0-beta.5] 2024-11-03
|
||||
### Changed
|
||||
- Use updated A2S_RULES logic
|
||||
### Fixed
|
||||
- Servers in saved servers list would populate context menu with same option when right-clicking in server browser
|
||||
|
||||
## [5.5.0-beta.4] 2024-10-31
|
||||
### Added
|
||||
- Expose a toggle setting for whether to launch the application in fullscreen
|
||||
### Fixed
|
||||
- Enable adding/removing servers to/from My Saved Servers when in Recent Servers context
|
||||
|
||||
## [5.5.0-beta.3] 2024-10-31
|
||||
### Fixed
|
||||
- Prevent maps combobox from duplicating contents
|
||||
|
||||
## [5.5.0-beta.2] 2024-10-31
|
||||
### Fixed
|
||||
- Restore keyboard input to keyword entry field
|
||||
|
||||
## [5.5.0-beta.1] 2024-10-30
|
||||
## [5.5.0] 2024-11-10
|
||||
### Added
|
||||
- Support servers running DLC content (fixes Frostline servers)
|
||||
- Text autocompletion in maps search field
|
||||
- Add disk space warning to popup dialog
|
||||
- Expose a toggle setting for whether to launch the application in fullscreen
|
||||
- Text autocompletion in maps search field (partial search)
|
||||
- Add disk space warning to popup dialog when downloading mods
|
||||
### Fixed
|
||||
- Servers in saved servers list would populate context menu with same option when right-clicking in server browser
|
||||
- Enable adding/removing servers to/from My Saved Servers when in Recent Servers context
|
||||
- Prevent maps combobox from duplicating contents
|
||||
- Restore keyboard input to keyword entry field
|
||||
### Changed
|
||||
- Abort fallback query method if DLC is required
|
||||
|
||||
## [5.4.2-beta.1] 2024-10-05
|
||||
## [5.4.2] 2024-10-05
|
||||
### Fixed
|
||||
- Sanitize third-party API IDs to remove UGC collisions
|
||||
|
||||
## [5.4.1-beta.2] 2024-09-12
|
||||
### Fixed
|
||||
- Use fallback logic for modlist queries when user traverses networks
|
||||
|
||||
## [5.4.1-beta.2] 2024-09-10
|
||||
### Fixed
|
||||
- Fix signal handling control flow for checkbox toggles
|
||||
|
||||
## [5.4.1-beta.1] 2024-09-10
|
||||
## [5.4.1] 2024-09-25
|
||||
### Added
|
||||
- Pre-boot validation check for users with self-compiled version of jq
|
||||
### Fixed
|
||||
- Use fallback logic for modlist queries when user traverses networks
|
||||
- Fix signal handling control flow for checkbox toggles
|
||||
- When reloading the server browser, the map combobox selection would revert to the last selected map instead of All Maps
|
||||
- Server filter toggle signals were accessible from the main menu when switching between menu contexts
|
||||
- Global cooldown dialog could sometimes block filter toggles after cooldown reset
|
||||
- Normalized minor version number due to a previous clerical error
|
||||
|
||||
## [5.4.0-beta.5] 2024-08-27
|
||||
### Added
|
||||
- Freedesktop application icons for system taskbar, tray, and other dialogs
|
||||
### Fixed
|
||||
- Errors being printed to the console when Exit button was explicitly clicked
|
||||
|
||||
## [5.4.0-beta.4] 2024-08-21
|
||||
### Added
|
||||
- Emit CPU model name when exporting system debug log
|
||||
### Fixed
|
||||
- Detect Steam Deck OLED APU variant during initial setup
|
||||
|
||||
## [5.4.0-beta.3] 2024-08-04
|
||||
## [5.4.0] 2024-08-27
|
||||
### Added
|
||||
- Scan local area network for DayZ servers
|
||||
|
||||
## [5.4.0-beta.2] 2024-08-03
|
||||
### Fixed
|
||||
- Clerical hotfix for previous player names fix
|
||||
- Test if DayZ directory is empty at startup, implying that the game was moved to a new library collection
|
||||
|
||||
## [5.4.0-beta.1] 2024-07-16
|
||||
- Freedesktop application icons for system taskbar, tray, and other dialogs
|
||||
- Emit CPU model name when exporting system debug log
|
||||
### Fixed
|
||||
- Errors being printed to the console when Exit button was explicitly clicked
|
||||
- Detect Steam Deck OLED APU variant during initial setup
|
||||
- Encapsulate player names correctly so that names with whitespace in them are supported
|
||||
- Test if DayZ directory is empty at startup, implying that the game was moved to a new library collection
|
||||
- Report WM_CLASS name to the window manager
|
||||
|
||||
## [5.3.2] 2024-07-02
|
||||
### Fixed
|
||||
|
||||
163
dzgui.sh
163
dzgui.sh
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -o pipefail
|
||||
|
||||
version=6.0.0.beta-7
|
||||
version=5.6.3
|
||||
|
||||
#CONSTANTS
|
||||
aid=221100
|
||||
@ -49,9 +49,10 @@ freedesktop_path="$HOME/.local/share/applications"
|
||||
|
||||
#HELPERS
|
||||
ui_helper="$helpers_path/ui.py"
|
||||
geo_file="$helpers_path/ips.csv"
|
||||
km_helper="$helpers_path/latlon"
|
||||
sums_path="$helpers_path/sums.md5"
|
||||
func_helper="$helpers_path/funcs"
|
||||
geo_helper="$helpers_path/ips.csv"
|
||||
|
||||
#REMOTE
|
||||
remote_host=gh
|
||||
@ -62,6 +63,7 @@ stable_url="$url_prefix/dzgui"
|
||||
testing_url="$url_prefix/testing"
|
||||
releases_url="https://github.com/$author/$repo/releases/download/browser"
|
||||
km_helper_url="$releases_url/latlon"
|
||||
geo_file_url="$releases_url/ips.csv.gz"
|
||||
|
||||
|
||||
set_im_module(){
|
||||
@ -540,7 +542,7 @@ lock(){
|
||||
ps -p $pid -o pid= >/dev/null 2>&1
|
||||
res=$?
|
||||
if [[ $res -eq 0 ]]; then
|
||||
local msg="DZGUI is already running ($pid)"
|
||||
local msg="DZGUI already running ($pid)"
|
||||
raise_error_and_quit "$msg"
|
||||
elif [[ $pid == $$ ]]; then
|
||||
:
|
||||
@ -569,13 +571,13 @@ fetch_a2s(){
|
||||
logger INFO "Updated A2S helper to sha '$sha'"
|
||||
}
|
||||
fetch_dzq(){
|
||||
local sum="0a334e1e144e76e560419d155435c91e"
|
||||
local sum="9caed1445c45832f4af87736ba3f9637"
|
||||
local file="$helpers_path/a2s/dayzquery.py"
|
||||
if [[ -f $file ]] && [[ $(get_hash "$file") == $sum ]]; then
|
||||
logger INFO "DZQ is current"
|
||||
return 0
|
||||
fi
|
||||
local sha=a22a9f428cbe075d7dda62f78000296955eea92a
|
||||
local sha=3088bbfb147b77bc7b6a9425581b439889ff3f7f
|
||||
local author="yepoleb"
|
||||
local repo="dayzquery"
|
||||
local url="https://raw.githubusercontent.com/$author/$repo/$sha/dayzquery.py"
|
||||
@ -612,12 +614,11 @@ fetch_helpers_by_sum(){
|
||||
[[ -f "$config_file" ]] && source "$config_file"
|
||||
declare -A sums
|
||||
sums=(
|
||||
["funcs"]="c27a81577c818b4f4d786ada52ad5042"
|
||||
["ui.py"]="5a876efacf208d12b5fe761996425412"
|
||||
["query_v2.py"]="55d339ba02512ac69de288eb3be41067"
|
||||
["servers.py"]="7f83d5c1ca54acb12f1bd6657feb2ecf"
|
||||
["ui.py"]="8d6e52b3e9032acbb84ba6cf137f455a"
|
||||
["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397"
|
||||
["pefile.py"]="21531f2c0d9dfa5f110cf6779f9d22c0"
|
||||
["funcs"]="417bd5eaffbefc905a843985c691dc64"
|
||||
["lan"]="c62e84ddd1457b71a85ad21da662b9af"
|
||||
)
|
||||
local author="aclist"
|
||||
local repo="dztui"
|
||||
@ -659,11 +660,22 @@ fetch_helpers_by_sum(){
|
||||
logger INFO "Updated '$full_path' to sum '$sum'"
|
||||
fi
|
||||
[[ $file == "funcs" ]] && chmod +x "$full_path"
|
||||
[[ $file == "lan" ]] && chmod +x "$full_path"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
fetch_km_helper(){
|
||||
fetch_geo_file(){
|
||||
# for binary releases
|
||||
local geo_sum="9824e9b9a75a4830a2423932cc188b06"
|
||||
local km_sum="b038fdb8f655798207bd28de3a004706"
|
||||
local gzip="$helpers_path/ips.csv.gz"
|
||||
if [[ ! -f $geo_file ]] || [[ $(get_hash $geo_file) != $geo_sum ]]; then
|
||||
local res=$(get_response_code "$geo_file_url")
|
||||
[[ $res -ne 200 ]] && raise_error_and_quit "Remote resource unavailable: '$geo_file_url'"
|
||||
curl -Ls "$geo_file_url" > "$gzip"
|
||||
#force overwrite
|
||||
gunzip -f "$gzip"
|
||||
fi
|
||||
if [[ ! -f $km_helper ]] || [[ $(get_hash $km_helper) != $km_sum ]]; then
|
||||
local res=$(get_response_code "$km_helper_url")
|
||||
[[ $res -ne 200 ]] && raise_error_and_quit "Remote resource unavailable: '$km_helper_url'"
|
||||
@ -671,119 +683,10 @@ fetch_km_helper(){
|
||||
chmod +x "$km_helper"
|
||||
fi
|
||||
}
|
||||
get_response_code(){
|
||||
local url="$1"
|
||||
curl -Ls -I -o /dev/null -w "%{http_code}" "$url"
|
||||
}
|
||||
raise_error_and_quit(){
|
||||
echo "$1"
|
||||
exit 1
|
||||
}
|
||||
fetch_ip_db(){
|
||||
parse_dl_url(){
|
||||
curl -Ls "$url" \
|
||||
| grep "csv.gz" \
|
||||
| awk -F"['']" '{print $2}'
|
||||
}
|
||||
|
||||
parse_dl_url_date(){
|
||||
local url="$1"
|
||||
<<< "$url" \
|
||||
awk -F/ '{print $5}' \
|
||||
| awk -F"dbip-city-lite-" '{print $2}' \
|
||||
| awk -F'.csv.gz' '{print $1}'
|
||||
}
|
||||
|
||||
fetch(){
|
||||
logger INFO "Triggering fetch routine"
|
||||
local url="$1"
|
||||
|
||||
curl -Ls "$url" > "$base_file"
|
||||
if [[ $? -ne 0 ]]; then
|
||||
logger WARN "Abnormal exit while accessing '$url'"
|
||||
return
|
||||
fi
|
||||
|
||||
gunzip -f "$base_file"
|
||||
if [[ $? -ne 0 ]]; then
|
||||
rm "$base_file"
|
||||
logger WARN "Abnormal exit while unpacking gzip '$base_file'"
|
||||
return
|
||||
fi
|
||||
|
||||
< "$extracted_file" grep -vE "^[a-z0-9]{4}:" | grep -v "::" > "$ip_file"
|
||||
if [[ $? -ne 0 ]]; then
|
||||
logger WARN "Abnormal exit while parsing IPs"
|
||||
return
|
||||
fi
|
||||
rm "${state_path}/${month}.csv"
|
||||
|
||||
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
|
||||
logger WARN "Anchor records missing in '$ip_file', possibly malformed"
|
||||
rm "$ip_file"
|
||||
return
|
||||
fi
|
||||
|
||||
mv "$ip_file" "$geo_helper"
|
||||
if [[ $? -ne 0 ]]; then
|
||||
logger WARN "Abnormal exit while moving '$ip_file' to '$helpers_path'"
|
||||
rm "$ip_file"
|
||||
fi
|
||||
|
||||
echo "$month" > "$month_file"
|
||||
logger INFO "Wrote '$month' to stub '$month_file'"
|
||||
logger INFO "Updated '$ip_file'"
|
||||
}
|
||||
|
||||
local url="https://db-ip.com/db/download/ip-to-city-lite"
|
||||
local month_file="${state_path}/.month"
|
||||
local ip_file="${state_path}/ips.csv"
|
||||
|
||||
# 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
|
||||
if [[ ! -f $month_file ]]; then
|
||||
logger WARN "No stub file '$month_file' present"
|
||||
fetch "$dl_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
|
||||
fi
|
||||
|
||||
# if stub is same date, abort
|
||||
logger INFO "Local stub '$last_month' is identical to remote, skipping"
|
||||
}
|
||||
fetch_helpers(){
|
||||
fetch_a2s
|
||||
fetch_dzq
|
||||
fetch_km_helper
|
||||
fetch_ip_db
|
||||
fetch_geo_file
|
||||
fetch_helpers_by_sum
|
||||
[[ ! -f $share_path/icon.png ]] && freedesktop_dirs
|
||||
fetch_icons
|
||||
@ -953,7 +856,7 @@ create_config(){
|
||||
}
|
||||
varcheck(){
|
||||
local msg="Config file '$config_file' missing. Start first-time setup now?"
|
||||
local msg2="The Steam paths set in your config file appear to be invalid (DayZ was moved or uninstalled). Restart first-time setup now?"
|
||||
local msg2="The Steam paths set in your config file appear to be invalid. Restart first-time setup now?"
|
||||
if [[ ! -f $config_file ]]; then
|
||||
qdialog "$msg" "Yes" "Exit"
|
||||
if [[ $? -eq 1 ]]; then
|
||||
@ -1000,14 +903,21 @@ get_response_code(){
|
||||
curl -Ls -I -o /dev/null -w "%{http_code}" "$url"
|
||||
}
|
||||
test_connection(){
|
||||
source "$config_file"
|
||||
declare -A hr
|
||||
local res1
|
||||
local res2
|
||||
local str="No connection could be established to the remote server"
|
||||
hr=(
|
||||
["steampowered.com"]="https://api.steampowered.com/IGameServersService/GetServerList/v1/?key=$steam_api"
|
||||
["github.com"]="https://github.com/$author"
|
||||
["codeberg.org"]="https://codeberg.org/$author"
|
||||
)
|
||||
# steam API is mandatory, except on initial setup
|
||||
if [[ -n $steam_api ]]; then
|
||||
res=$(get_response_code "${hr["steampowered.com"]}")
|
||||
[[ $res -ne 200 ]] && raise_error_and_quit "$str ("steampowered.com")"
|
||||
fi
|
||||
|
||||
res=$(get_response_code "${hr["github.com"]}")
|
||||
if [[ $res -ne 200 ]]; then
|
||||
@ -1023,6 +933,7 @@ test_connection(){
|
||||
stable_url="$url_prefix/dzgui"
|
||||
testing_url="$url_prefix/testing"
|
||||
km_helper_url="$releases_url/latlon"
|
||||
geo_file_url="$releases_url/ips.csv.gz"
|
||||
fi
|
||||
}
|
||||
legacy_cols(){
|
||||
@ -1033,20 +944,19 @@ legacy_cols(){
|
||||
mv $cols_file.new $cols_file
|
||||
}
|
||||
stale_mod_signatures(){
|
||||
[[ ! -f "$versions_file" ]] && return
|
||||
local workshop_dir="$steam_path/steamapps/workshop/content/$aid"
|
||||
if [[ -d $workshop_dir ]]; then
|
||||
readarray -t old_mod_ids < <(awk -F, '{print $1}' $versions_file)
|
||||
for ((i=0; i<${#old_mod_ids[@]}; ++i)); do
|
||||
if [[ ! -d $workshop_dir/${old_mod_ids[$i]} ]]; then
|
||||
"$func_helper" "align_local" "${old_mod_ids[$i]}"
|
||||
"$HOME/.local/share/$app_name/helpers/funcs" "align_local" "${old_mod_ids[$i]}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
}
|
||||
create_new_links(){
|
||||
"$func_helper" "update_symlinks"
|
||||
"$HOME/.local/share/$app_name/helpers/funcs" "update_symlinks"
|
||||
}
|
||||
initial_setup(){
|
||||
setup_dirs
|
||||
@ -1124,7 +1034,10 @@ main(){
|
||||
uninstall &&
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ $1 == "--steam" ]] || [[ $1 == "-s" ]]; then
|
||||
export STEAM_LAUNCH=1
|
||||
fi
|
||||
|
||||
set_im_module
|
||||
|
||||
printf "Initializing setup...\n"
|
||||
|
||||
515
helpers/funcs
515
helpers/funcs
@ -1,10 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -o pipefail
|
||||
version="6.0.0-beta.7"
|
||||
version=5.6.0
|
||||
|
||||
#CONSTANTS
|
||||
aid=221100
|
||||
exp=1024020
|
||||
game="dayz"
|
||||
app_name="dzgui"
|
||||
app_name_upper="DZGUI"
|
||||
@ -47,7 +46,6 @@ _cache_my_servers="$cache_dir/$prefix.my_servers"
|
||||
_cache_history="$cache_dir/$prefix.history"
|
||||
_cache_launch="$cache_dir/$prefix.launch_mods"
|
||||
_cache_address="$cache_dir/$prefix.launch_address"
|
||||
_cache_binary="$cache_dir/$prefix.binary"
|
||||
_cache_coords="$cache_path/$prefix.coords"
|
||||
_cache_cooldown="$cache_path/$prefix.cooldown"
|
||||
_cache_lan="$cache_path/$prefix.lan"
|
||||
@ -61,6 +59,7 @@ geo_file="$helpers_path/ips.csv"
|
||||
km_helper="$helpers_path/latlon"
|
||||
sums_path="$helpers_path/sums.md5"
|
||||
query_helper="$helpers_path/query_v2.py"
|
||||
func_helper="$helpers_path/funcs"
|
||||
lan_helper="$helpers_path/lan"
|
||||
|
||||
#STEAM PATHS
|
||||
@ -84,6 +83,7 @@ forum_url="https://old.reddit.com/r/dzgui"
|
||||
sponsor_url="$gh_prefix/sponsors/$author"
|
||||
battlemetrics_server_url="https://www.battlemetrics.com/servers/dayz"
|
||||
steam_api_url="https://steamcommunity.com/dev/apikey"
|
||||
#TODO: update link in docs
|
||||
battlemetrics_api_url="https://www.battlemetrics.com/developers"
|
||||
bm_api="https://api.battlemetrics.com/servers"
|
||||
|
||||
@ -95,20 +95,26 @@ fi
|
||||
|
||||
declare -A funcs=(
|
||||
["Highlight stale"]="find_stale_mods"
|
||||
["My servers"]="dump_servers"
|
||||
["Change player name"]="update_config_val"
|
||||
["Change Steam API key"]="update_config_val"
|
||||
["Change Battlemetrics API key"]="update_config_val"
|
||||
["Change favorite server"]="add_record"
|
||||
["Quick-connect to favorite server"]="quick_connect"
|
||||
["Add server by IP"]="add_record"
|
||||
["Add server by ID"]="add_record"
|
||||
["try_connect"]="try_connect"
|
||||
["Connect by IP"]="validate_and_connect"
|
||||
["Connect by ID"]="validate_and_connect"
|
||||
["Connect from table"]="connect_from_table"
|
||||
["find_id"]="find_id"
|
||||
["toggle"]="toggle"
|
||||
["Open link"]="open_link"
|
||||
["filter"]="dump_servers"
|
||||
["dump_servers"]="dump_servers"
|
||||
["get_unique_maps"]="get_unique_maps"
|
||||
["get_dist"]="get_dist"
|
||||
["test_cooldown"]="test_cooldown"
|
||||
["query_config"]="query_config"
|
||||
["query_favorites"]="query_favorites"
|
||||
["start_cooldown"]="start_cooldown"
|
||||
["List installed mods"]="list_mods"
|
||||
["Delete selected mods"]="delete_local_mod"
|
||||
@ -125,31 +131,21 @@ declare -A funcs=(
|
||||
["Remove from history"]="remove_from_history"
|
||||
["Force update local mods"]="force_update"
|
||||
["Resolve IP"]="resolve_ip"
|
||||
["Handshake"]="handshake"
|
||||
["Handshake_EXP"]="handshake_exp"
|
||||
["Handshake"]="final_handshake"
|
||||
["get_player_count"]="get_player_count"
|
||||
["lan_scan"]="lan_scan"
|
||||
["update_symlinks"]="update_symlinks"
|
||||
)
|
||||
|
||||
clone_symlinks(){
|
||||
local path="$(< $_cache_binary)"
|
||||
path=$(dirname "$path")
|
||||
for dir in $(find $game_dir -type l); do
|
||||
local link=$(basename $dir)
|
||||
ln -sf "${dir}" "${path}/${link}"
|
||||
done
|
||||
}
|
||||
handshake(){
|
||||
final_handshake "$aid"
|
||||
}
|
||||
handshake_exp(){
|
||||
final_handshake "$exp"
|
||||
}
|
||||
query_favorites(){
|
||||
if [[ -z "${ip_list[@]}" ]]; then
|
||||
return 1
|
||||
lan_scan(){
|
||||
local port="$1"
|
||||
local res
|
||||
res=$("$lan_helper" "$port")
|
||||
if [[ -z $res ]]; then
|
||||
printf "\n"
|
||||
else
|
||||
printf "%s\n" "$res"
|
||||
fi
|
||||
printf "%s\n" "${ip_list[@]}"
|
||||
}
|
||||
find_stale_mods(){
|
||||
local res
|
||||
@ -205,6 +201,44 @@ get_player_count(){
|
||||
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(){
|
||||
local id="$1"
|
||||
local res=$(curl -s "$bm_api" -H "Authorization: Bearer "$api_key"" \
|
||||
@ -270,10 +304,32 @@ add_record(){
|
||||
;;
|
||||
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(){
|
||||
logger WARN "API response empty. Started 60s cooldown at $(date +%s)"
|
||||
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(){
|
||||
local dlc
|
||||
local ip="$1"
|
||||
@ -395,10 +451,55 @@ get_dist(){
|
||||
else
|
||||
logger INFO "Resolved remote server geolocation to '$remote_lat, $remote_lon'"
|
||||
local dist=$($km_helper "$local_lat" "$local_lon" "$remote_lat" "$remote_lon")
|
||||
LC_NUMERIC=C printf "%.0f" "$dist"
|
||||
LC_NUMERIC=C printf "%d" "$dist"
|
||||
logger INFO "Distance: $dist km"
|
||||
fi
|
||||
}
|
||||
get_remote_servers(){
|
||||
params=(
|
||||
"\\nor\1\map\chernarusplus\\nor\1\map\sakhal"
|
||||
"\map\chernarusplus\empty\1"
|
||||
"\map\chernarusplus\noplayers\1"
|
||||
"\map\\sakhal"
|
||||
)
|
||||
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(){
|
||||
[[ -n $2 ]] && local key=$2
|
||||
keys=(
|
||||
@ -409,7 +510,6 @@ query_config(){
|
||||
"fav_label"
|
||||
"preferred_client"
|
||||
"fullscreen"
|
||||
"default_steam_path"
|
||||
)
|
||||
if [[ -n $key ]]; then
|
||||
if [[ -n ${!key} ]]; then
|
||||
@ -424,6 +524,137 @@ query_config(){
|
||||
echo "${!i}"
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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])"
|
||||
' | sort -k1
|
||||
}
|
||||
align_versions_file(){
|
||||
shift
|
||||
local mod="$1"
|
||||
@ -475,13 +706,69 @@ test_cooldown(){
|
||||
local old_time=$(< $_cache_cooldown)
|
||||
local cur_time=$(date +%s)
|
||||
local delta=$(($cur_time - $old_time))
|
||||
if [[ $delta -lt 30 ]]; then
|
||||
local remains=$((30 - $delta))
|
||||
if [[ $delta -lt 60 ]]; then
|
||||
local remains=$((60 - $delta))
|
||||
local suffix=$(pluralize "seconds" $remains)
|
||||
printf "Global API cooldown in effect. Please wait %s %s." "$remains" "$suffix"
|
||||
exit 1
|
||||
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(){
|
||||
sed 's@\(/home/\)[^/]*@\1REDACTED@g'
|
||||
}
|
||||
@ -497,17 +784,12 @@ logger(){
|
||||
}
|
||||
test_ping(){
|
||||
shift
|
||||
local time
|
||||
local res
|
||||
local ip="$1"
|
||||
local qport="$2"
|
||||
res=$(a2s $ip $qport info)
|
||||
if [[ -z $res ]]; then
|
||||
time="Timed out"
|
||||
else
|
||||
time=$(<<< "$res" jq -r '.[].ping')
|
||||
fi
|
||||
printf "%s" "$time"
|
||||
local res
|
||||
res=$(ping -c1 -4 -W0.5 $1 | grep time= | awk -F= '{print $4}')
|
||||
[[ ! $? -eq 0 ]] && res="Timed out"
|
||||
printf "%s" "$res"
|
||||
}
|
||||
show_server_modlist(){
|
||||
shift
|
||||
@ -791,10 +1073,10 @@ test_steam_api(){
|
||||
}
|
||||
test_bm_api(){
|
||||
local key="$1"
|
||||
if [[ -z $key ]]; then
|
||||
echo 1
|
||||
if [[ ! $key =~ ^[0-9]+$ ]]; then
|
||||
return
|
||||
fi
|
||||
[[ -z $key ]] && return 1
|
||||
local code=$(curl -ILs "$bm_api" \
|
||||
-H "Authorization: Bearer "$key"" -G \
|
||||
-d "filter[game]=$game" \
|
||||
@ -808,23 +1090,19 @@ update_config_val(){
|
||||
case $1 in
|
||||
"Change player name")
|
||||
key="name"
|
||||
if [[ -z "${value// }" ]]; then
|
||||
printf "Invalid name"
|
||||
return 2
|
||||
fi
|
||||
;;
|
||||
"Change Steam API key")
|
||||
key="steam_api"
|
||||
if [[ ${#value} -lt 32 ]] || [[ $(test_steam_api "$value") -eq 1 ]]; then
|
||||
printf "Invalid API key"
|
||||
return 78
|
||||
return 2
|
||||
fi
|
||||
;;
|
||||
"Change Battlemetrics API key")
|
||||
key="api_key"
|
||||
if [[ $(test_bm_api "$value") -eq 1 ]]; then
|
||||
printf "Invalid API key"
|
||||
return 79
|
||||
return 2
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
@ -832,7 +1110,7 @@ update_config_val(){
|
||||
nr="$value"
|
||||
update_config
|
||||
echo "Updated the key '$key' to '$value'"
|
||||
return 80
|
||||
return 90
|
||||
}
|
||||
show_log(){
|
||||
< "$debug_log" sed 's/Keyword␞/Keyword/'
|
||||
@ -890,6 +1168,24 @@ open_link(){
|
||||
xdg-open "$url"
|
||||
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(){
|
||||
source $config_file
|
||||
cat <<-DOC > $system_log
|
||||
@ -979,7 +1275,7 @@ symlinks(){
|
||||
link="@$encoded_id"
|
||||
mod="${arr[$i]}"
|
||||
logger INFO "Creating link '$game_dir/$link' for '$workshop_dir/$mod'"
|
||||
[[ -n $STEAMSCRIPT ]] && echo "# Creating mod link $((i+1))/${#arr[@]}"
|
||||
[[ $STEAM_LAUNCH -eq 1 ]] && echo "# Creating mod link $((i+1))/${#arr[@]}"
|
||||
ln -s "$workshop_dir/$mod" "$game_dir/$link"
|
||||
done
|
||||
}
|
||||
@ -994,7 +1290,7 @@ symlinks(){
|
||||
|
||||
if [[ ${#links[@]} -eq 0 ]]; then
|
||||
logger INFO "No symlinks present in '$game_dir', creating them"
|
||||
if [[ -n $STEAMSCRIPT ]]; then
|
||||
if [[ $STEAM_LAUNCH -eq 1 ]]; then
|
||||
_create_links "${mods[@]}" > >(_pulse)
|
||||
else
|
||||
_create_links "${mods[@]}"
|
||||
@ -1012,7 +1308,7 @@ symlinks(){
|
||||
|
||||
# update missing targets
|
||||
logger INFO "Found ${#hits[@]} unlinked mods"
|
||||
if [[ -n $STEAMSCRIPT ]]; then
|
||||
if [[ $STEAM_LAUNCH -eq 1 ]]; then
|
||||
_create_links "${hits[@]}" > >(_pulse)
|
||||
else
|
||||
_create_links "${hits[@]}"
|
||||
@ -1030,9 +1326,6 @@ update_history(){
|
||||
echo "$record" >> "$history_file"
|
||||
}
|
||||
update_symlinks(){
|
||||
if [[ ! -d "$workshop_dir" ]]; then
|
||||
return
|
||||
fi
|
||||
legacy_symlinks
|
||||
symlinks
|
||||
}
|
||||
@ -1079,17 +1372,10 @@ try_fallback(){
|
||||
esac
|
||||
}
|
||||
try_connect(){
|
||||
shift
|
||||
local record="$1"
|
||||
local appid="$2"
|
||||
local binary="$3"
|
||||
|
||||
local ip=$(<<< $record awk -F: '{print $1}')
|
||||
local gameport=$(<<< $record awk -F: '{print $2}')
|
||||
local qport=$(<<< $record awk -F: '{print $3}')
|
||||
|
||||
[[ $appid -eq $exp ]] && echo "$binary" > $_cache_binary
|
||||
|
||||
local remote_mods
|
||||
remote_mods=$(a2s $ip $qport rules)
|
||||
if [[ $? -eq 1 ]]; then
|
||||
@ -1114,11 +1400,11 @@ try_connect(){
|
||||
return 1
|
||||
fi
|
||||
case $auto_install in
|
||||
"") manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods" "" "$appid";;
|
||||
1|2) manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods" "auto" "$appid" ;;
|
||||
"") manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods";;
|
||||
1|2) manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods" "auto" ;;
|
||||
esac
|
||||
else
|
||||
launch "$ip" "$gameport" "$sanitized_mods" "$appid"
|
||||
launch "$ip" "$gameport" "$sanitized_mods"
|
||||
fi
|
||||
}
|
||||
check_architecture(){
|
||||
@ -1129,6 +1415,68 @@ check_architecture(){
|
||||
echo 0
|
||||
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(){
|
||||
if [[ ! $auto_install -eq 1 ]]; then
|
||||
printf "Only available when mod auto-install is ON"
|
||||
@ -1136,10 +1484,21 @@ force_update(){
|
||||
fi
|
||||
rm "$versions_file"
|
||||
local update=$(check_timestamps)
|
||||
manual_mod_install "null" "null" "$update" "null" "force" "null"
|
||||
manual_mod_install "null" "null" "$update" "null" "force"
|
||||
echo "Finished requesting mod updates."
|
||||
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(){
|
||||
readarray -t modlist < <(printf "%s\n" "$@")
|
||||
local max="${#modlist[@]}"
|
||||
@ -1178,6 +1537,8 @@ check_timestamps(){
|
||||
local aligned=$(<<< "$local_stamps" jq -r '.response.publishedfiledetails[]|"\(.publishedfileid),\(.time_updated)"')
|
||||
readarray -t remote_ids < <(<<< "$aligned" awk -F, '{print $1}')
|
||||
readarray -t remote_times < <(<<< "$aligned" awk -F, '{print $2}')
|
||||
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
|
||||
logger INFO "No prior versions file found, creating"
|
||||
@ -1187,9 +1548,6 @@ check_timestamps(){
|
||||
return 0
|
||||
fi
|
||||
|
||||
readarray -t old_ids < <(< $versions_file awk -F, '{print $1}')
|
||||
readarray -t old_times < <(< $versions_file awk -F, '{print $2}')
|
||||
|
||||
declare -A remote_version
|
||||
declare -A local_version
|
||||
|
||||
@ -1232,9 +1590,6 @@ concat_mods(){
|
||||
local encoded_id
|
||||
local link
|
||||
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}')
|
||||
encoded_id=$(encode $id)
|
||||
link="@$encoded_id;"
|
||||
@ -1253,8 +1608,6 @@ launch(){
|
||||
local ip="$1"
|
||||
local gameport="$2"
|
||||
local mods="$3"
|
||||
local appid="$4"
|
||||
|
||||
local concat
|
||||
if [[ -n $mods ]]; then
|
||||
concat=$(concat_mods "$mods")
|
||||
@ -1263,22 +1616,18 @@ launch(){
|
||||
fi
|
||||
|
||||
update_symlinks
|
||||
[[ $appid -eq $exp ]] && clone_symlinks
|
||||
|
||||
if [[ $debug -eq 1 ]]; then
|
||||
local launch_options="$steam_cmd -applaunch $appid -connect=$ip:$gameport -nolauncher -nosplash -name=$name -skipintro -mod=$concat"
|
||||
local launch_options="$steam_cmd -applaunch $aid -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"
|
||||
return 0
|
||||
fi
|
||||
echo "$concat" > "$_cache_launch"
|
||||
echo "$ip:$gameport" > "$_cache_address"
|
||||
logger INFO "Saved launch params: '$concat'"
|
||||
printf "Launch conditions satisfied. DayZ will launch after you confirm this dialog."
|
||||
[[ $appid == "$aid" ]] && return 100
|
||||
[[ $appid == "$exp" ]] && return 101
|
||||
printf "Launch conditions satisfied. DayZ will now launch after you confirm this dialog."
|
||||
return 100
|
||||
}
|
||||
final_handshake(){
|
||||
local appid="$1"
|
||||
local saved_mods=$(< "$_cache_launch")
|
||||
local saved_address=$(< "$_cache_address")
|
||||
local res=$(is_dayz_running)
|
||||
@ -1295,7 +1644,7 @@ final_handshake(){
|
||||
params+=("-skipintro")
|
||||
params+=("-name=$name")
|
||||
params+=("-mod=$saved_mods")
|
||||
$steam_cmd -applaunch $appid "${params[@]}" &
|
||||
$steam_cmd -applaunch $aid "${params[@]}" &
|
||||
until [[ $(is_dayz_running) -eq 1 ]]; do
|
||||
sleep 0.1s
|
||||
done
|
||||
@ -1309,7 +1658,6 @@ manual_mod_install(){
|
||||
local diff="$3"
|
||||
local sanitized_mods="$4"
|
||||
local mode="$5"
|
||||
local appid="$6"
|
||||
local ex="$state_path/dzg.watcher"
|
||||
|
||||
readarray -t stage_mods <<< "$diff"
|
||||
@ -1318,6 +1666,7 @@ manual_mod_install(){
|
||||
_watcher(){
|
||||
for((i=0;i<${#stage_mods[@]};i++)); do
|
||||
[[ -f $ex ]] && return 1
|
||||
log ${stage_mods[$i]}
|
||||
|
||||
if [[ $mode == "auto" ]] || [[ $mode == "force" ]]; then
|
||||
$steam_cmd "steam://url/CommunityFilePage/${stage_mods[$i]}+workshop_download_item $aid ${stage_mods[$i]}"
|
||||
@ -1367,7 +1716,7 @@ manual_mod_install(){
|
||||
rm "$versions_file"
|
||||
check_timestamps
|
||||
fi
|
||||
launch "$ip" "$gameport" "$sanitized_mods" "$appid"
|
||||
launch "$ip" "$gameport" "$sanitized_mods"
|
||||
else
|
||||
printf "User aborted download process, or some mods may have failed to download. Try connecting again to resync."
|
||||
exit 1
|
||||
@ -1391,7 +1740,7 @@ foreground(){
|
||||
}
|
||||
main(){
|
||||
local params="$(printf '"%s", ' "$@")"
|
||||
logger INFO "Received request from UI with params [${params::-2}]"
|
||||
logger INFO "Received request from UI constructor with params [${params::-2}]"
|
||||
func=${funcs["$1"]}
|
||||
[[ -z $func ]] && return 1
|
||||
if [[ -z $2 ]]; then
|
||||
|
||||
@ -1,492 +0,0 @@
|
||||
import json
|
||||
import struct
|
||||
import typing # noqa
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from shlex import shlex
|
||||
from typing import BinaryIO, Union
|
||||
|
||||
# https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
|
||||
endian = "<"
|
||||
IMAGE_DIRECTORY_ENTRY = 2
|
||||
VERSION_RESOURCE = 16
|
||||
RESOURCE_NODE = ".rsrc"
|
||||
PE32_x86 = "0x10b"
|
||||
PE32_x64 = "0x20b"
|
||||
VS_VERSION_INFO_MAGIC = "0xfeef04bd0000"
|
||||
VS_VERSION_INFO_ID = "VS_VERSION_INFO"
|
||||
|
||||
|
||||
class VersionMatch(Enum):
|
||||
LOCAL_OLDER = 1
|
||||
LOCAL_NEWER = 2
|
||||
SAME_VERSION = 3
|
||||
|
||||
|
||||
class u8:
|
||||
fmt = "B"
|
||||
|
||||
|
||||
class u16:
|
||||
fmt = "H"
|
||||
|
||||
|
||||
class u32:
|
||||
fmt = "L"
|
||||
|
||||
|
||||
class u64:
|
||||
fmt = "Q"
|
||||
|
||||
|
||||
class i8:
|
||||
fmt = "b"
|
||||
|
||||
|
||||
class i16:
|
||||
fmt = "h"
|
||||
|
||||
|
||||
class i32:
|
||||
fmt = "l"
|
||||
|
||||
|
||||
class i64:
|
||||
fmt = "q"
|
||||
|
||||
|
||||
class PackedData:
|
||||
@classmethod
|
||||
def unpack(cls, data: BinaryIO):
|
||||
r = []
|
||||
for key, value in cls.__annotations__.items():
|
||||
if value == str:
|
||||
f = data.read(8).rstrip(b"\x00\x00").decode()
|
||||
else:
|
||||
fmt = endian + (value.fmt)
|
||||
size = struct.calcsize(fmt)
|
||||
f = struct.unpack(fmt, data.read(size))[0]
|
||||
r.append(f)
|
||||
return cls(*r)
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class COFF_FILE_HDR(PackedData):
|
||||
machine_type: u16
|
||||
number_of_sections: u16
|
||||
timestamp: u32
|
||||
pointer_to_symbol_table: u32
|
||||
number_of_symbols: u32
|
||||
size_of_optional_header: u16
|
||||
characteristics: u16
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class OPTIONAL_HDR_X86(PackedData):
|
||||
magic: u16
|
||||
major_linker_ver: u8
|
||||
minor_linker_ver: u8
|
||||
size_of_code: u32
|
||||
size_of_initialized_data: u32
|
||||
size_of_uninitialized_data: u32
|
||||
address_of_entry_point: u32
|
||||
base_of_code: u32
|
||||
base_of_data: u32
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class OPTIONAL_HDR_X64(PackedData):
|
||||
magic: u16
|
||||
major_linker_ver: u8
|
||||
minor_linker_ver: u8
|
||||
size_of_code: u32
|
||||
size_of_initialized_data: u32
|
||||
size_of_uninitialized_data: u32
|
||||
address_of_entry_point: u32
|
||||
base_of_code: u32
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class OPTIONAL_HDR_WIN_X86(PackedData):
|
||||
image_base: u32
|
||||
section_alignment: u32
|
||||
file_alignment: u32
|
||||
major_operating_system_version: u16
|
||||
minor_operating_system_version: u16
|
||||
major_image_version: u16
|
||||
minor_image_version: u16
|
||||
major_subsystem_version: u16
|
||||
minor_subsystem_version: u16
|
||||
win32_version_value: u32
|
||||
size_of_image: u32
|
||||
size_of_headers: u32
|
||||
checksum: u32
|
||||
subsystem: u16
|
||||
dll_characteristics: u16
|
||||
size_of_stack_reserve: u32
|
||||
size_of_stack_commit: u32
|
||||
size_of_heap_reserve: u32
|
||||
size_of_heap_commit: u32
|
||||
loader_flags: u32
|
||||
number_of_rva_and_sizes: u32
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class OPTIONAL_HDR_WIN_X64(PackedData):
|
||||
image_base: u64
|
||||
section_alignment: u32
|
||||
file_alignment: u32
|
||||
major_operating_system_version: u16
|
||||
minor_operating_system_version: u16
|
||||
major_image_version: u16
|
||||
minor_image_version: u16
|
||||
major_subsystem_version: u16
|
||||
minor_subsystem_version: u16
|
||||
win32_version_value: u32
|
||||
size_of_image: u32
|
||||
size_of_headers: u32
|
||||
checksum: u32
|
||||
subsystem: u16
|
||||
dll_characteristics: u16
|
||||
size_of_stack_reserve: u64
|
||||
size_of_stack_commit: u64
|
||||
size_of_heap_reserve: u64
|
||||
size_of_heap_commit: u64
|
||||
loader_flags: u32
|
||||
number_of_rva_and_sizes: u32
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class DATA_DIR(PackedData):
|
||||
virtual_address: u32
|
||||
size: u32
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class SECTION_HDR(PackedData):
|
||||
name: str
|
||||
virtual_size: u32
|
||||
virtual_address: u32
|
||||
size_of_raw_data: u32
|
||||
pointer_to_raw_data: u32
|
||||
pointer_to_relocations: u32
|
||||
pointer_to_line_numbers: u32
|
||||
number_of_relocations: u16
|
||||
number_of_line_numbers: u16
|
||||
characteristics: u32
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class RESOURCE_DIRECTORY_TABLE(PackedData):
|
||||
characteristics: u32
|
||||
timestamp: u32
|
||||
major_version: u16
|
||||
minor_version: u16
|
||||
number_of_name_entries: u16
|
||||
number_of_id_entries: u16
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class RESOURCE_DIRECTORY_ENTRY(PackedData):
|
||||
"""
|
||||
This field is either a string identifying a data leaf
|
||||
(if the high bit is set) or an ID to another nested directory
|
||||
(if the high bit is clear). The outermost level is always a
|
||||
directory. If it is a name, the lower 31 bits are the offset from the
|
||||
beginning of the resource section's raw data to the name
|
||||
(the name consists of 16 bits length and trailing wide characters,
|
||||
in Unicode, not 0-terminated).
|
||||
"""
|
||||
|
||||
name_or_id: u32
|
||||
data_or_subdir: u32
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class RESOURCE_DATA_ENTRY(PackedData):
|
||||
data_rva: u32
|
||||
size: u32
|
||||
codepage: u32
|
||||
reserved: u32
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class VS_VERSION_INFO_HDR(PackedData):
|
||||
size: u16
|
||||
value_length: u16
|
||||
value_type: u16
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class DayZVersion:
|
||||
major: int
|
||||
minor: int
|
||||
patch: int
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class FileVersion:
|
||||
major: int
|
||||
minor: int
|
||||
build: int
|
||||
revision: int
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class Result:
|
||||
local: Union[str, None]
|
||||
remote: str
|
||||
build: str
|
||||
path: Union[Path, None]
|
||||
match: VersionMatch
|
||||
error: Union[Exception, None]
|
||||
|
||||
|
||||
class PeFileError(Exception):
|
||||
"""Expected contents missing from headers or resource nodes"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AppNotInstalledError(Exception):
|
||||
"""App not present in user's libraryfolders"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AppMovedError(Exception):
|
||||
"""VDF points to a nonexistent location on disk"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class VDFLoadError(Exception):
|
||||
"""Malformed VDF or JSON conversion"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def parse_version_number(data: BinaryIO):
|
||||
# https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
|
||||
minor = struct.unpack("<L", data.read(4))[0] >> 16 & 0xFFFF
|
||||
major = struct.unpack("<L", data.read(4))[0] >> 0 & 0xFFFF
|
||||
build = struct.unpack("<L", data.read(4))[0] >> 0 & 0xFFFF
|
||||
revision = struct.unpack("<L", data.read(4))[0] >> 16 & 0xFFFF
|
||||
return FileVersion(major, minor, build, revision)
|
||||
|
||||
|
||||
def seek_to_hex(address: str, data: BinaryIO) -> None:
|
||||
data.seek(int(address, base=16))
|
||||
|
||||
|
||||
def seek_to_pe_stub(data: BinaryIO) -> None:
|
||||
MAGIC = "0x3c"
|
||||
seek_to_hex(MAGIC, data)
|
||||
e_lfanew = hex(struct.unpack("<h", data.read(2))[0])
|
||||
seek_to_hex(e_lfanew, data)
|
||||
pe_stub = data.read(4).rstrip(b"\x00\x00\x00\x00").decode()
|
||||
if pe_stub != "PE":
|
||||
raise PeFileError("missing PE header data")
|
||||
|
||||
|
||||
def get_dayz_version(file: Path) -> DayZVersion | Exception:
|
||||
try:
|
||||
version = get_version(file)
|
||||
except Exception as e:
|
||||
return e
|
||||
patch = str(version.build) + str(version.revision)
|
||||
dz_vers = DayZVersion(version.major, version.minor, int(patch))
|
||||
return dz_vers
|
||||
|
||||
|
||||
def dayz_version_to_str(v: DayZVersion) -> str:
|
||||
return ".".join(str(el) for el in [v.major, v.minor, v.patch])
|
||||
|
||||
|
||||
def dayz_version_from_str(v: str) -> DayZVersion:
|
||||
vers = v.split(".")
|
||||
assert len(vers) == 3
|
||||
return DayZVersion(*[int(el) for el in vers])
|
||||
|
||||
|
||||
def get_version(file):
|
||||
with open(file, "rb") as f:
|
||||
seek_to_pe_stub(f)
|
||||
COFF = COFF_FILE_HDR.unpack(f)
|
||||
|
||||
pos = f.tell()
|
||||
blob = f.read()
|
||||
magic = hex(struct.unpack("<H", (blob[0:2]))[0])
|
||||
f.seek(pos)
|
||||
|
||||
if magic == PE32_x86:
|
||||
OPTIONAL_HDR_X86.unpack(f)
|
||||
OBJW = OPTIONAL_HDR_WIN_X86.unpack(f)
|
||||
else:
|
||||
OPTIONAL_HDR_X64.unpack(f)
|
||||
OBJW = OPTIONAL_HDR_WIN_X64.unpack(f)
|
||||
|
||||
if OBJW.number_of_rva_and_sizes < 1:
|
||||
raise PeFileError("no data resource directory")
|
||||
|
||||
data_dirs = []
|
||||
for rva in range(OBJW.number_of_rva_and_sizes):
|
||||
data_dir = DATA_DIR.unpack(f)
|
||||
data_dirs.append(data_dir)
|
||||
|
||||
res_dir = data_dirs[IMAGE_DIRECTORY_ENTRY]
|
||||
dir_va = res_dir.virtual_address
|
||||
|
||||
for section in range(COFF.number_of_sections):
|
||||
hdr = SECTION_HDR.unpack(f)
|
||||
if hdr.name == RESOURCE_NODE:
|
||||
va = hdr.virtual_address
|
||||
ptr = hdr.pointer_to_raw_data
|
||||
offset = dir_va - va + ptr
|
||||
seek_to_hex(hex(offset), f)
|
||||
break
|
||||
|
||||
if hdr.name != RESOURCE_NODE:
|
||||
raise PeFileError("no root resource node found")
|
||||
|
||||
table = RESOURCE_DIRECTORY_TABLE.unpack(f)
|
||||
total = table.number_of_name_entries + table.number_of_id_entries
|
||||
|
||||
for entry in range(total):
|
||||
entry = RESOURCE_DIRECTORY_ENTRY.unpack(f)
|
||||
if entry.name_or_id == VERSION_RESOURCE:
|
||||
while entry.data_or_subdir & (1 << 31):
|
||||
shift = entry.data_or_subdir & ~(1 << 31)
|
||||
seek_to_hex(hex(offset + shift), f)
|
||||
table = RESOURCE_DIRECTORY_TABLE.unpack(f)
|
||||
total = (
|
||||
table.number_of_name_entries
|
||||
+ table.number_of_id_entries
|
||||
)
|
||||
for entry in range(total):
|
||||
entry = RESOURCE_DIRECTORY_ENTRY.unpack(f)
|
||||
break
|
||||
if entry.name_or_id > VERSION_RESOURCE:
|
||||
raise PeFileError("no version info node found")
|
||||
seek_to_hex(hex(offset + entry.data_or_subdir), f)
|
||||
|
||||
data = RESOURCE_DATA_ENTRY.unpack(f)
|
||||
# https://stackoverflow.com/questions/2170843/va-virtual-address-rva-relative-virtual-address
|
||||
offset = data.data_rva - hdr.virtual_address + hdr.pointer_to_raw_data
|
||||
seek_to_hex(hex(offset), f)
|
||||
|
||||
hdr = VS_VERSION_INFO_HDR.unpack(f)
|
||||
# https://learn.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo
|
||||
byte_len = len(VS_VERSION_INFO_ID.encode("utf-16le"))
|
||||
label = f.read(byte_len).decode("utf-16le")
|
||||
if label != VS_VERSION_INFO_ID:
|
||||
raise PeFileError(f"header identifier != '{VS_VERSION_INFO_ID}'")
|
||||
f.read(32 - byte_len)
|
||||
|
||||
identifier = hex(struct.unpack("<Q", f.read(8))[0])
|
||||
if identifier != VS_VERSION_INFO_MAGIC:
|
||||
raise PeFileError(
|
||||
f"{VS_VERSION_INFO_ID} address != '{VS_VERSION_INFO_MAGIC}'"
|
||||
)
|
||||
|
||||
try:
|
||||
version = parse_version_number(f)
|
||||
except Exception as e:
|
||||
return PeFileError(e)
|
||||
|
||||
return version
|
||||
|
||||
|
||||
def get_pefile_path(path: str, appid: int) -> Path:
|
||||
binary = "DayZ_x64.exe"
|
||||
identifier = {221100: "DayZ", 1024020: "DayZ Exp"}
|
||||
name = identifier[appid]
|
||||
|
||||
pe_path = None
|
||||
path = path + "/steamapps/libraryfolders.vdf"
|
||||
|
||||
with open(path, "r") as f:
|
||||
try:
|
||||
j = json.loads(vdf_to_json(f))
|
||||
except Exception:
|
||||
raise VDFLoadError("Failed to parse libraryfolders")
|
||||
|
||||
for obj in j["libraryfolders"]:
|
||||
if str(appid) in j["libraryfolders"][obj]["apps"]:
|
||||
pe_path = j["libraryfolders"][obj]["path"]
|
||||
pe_path += f"/steamapps/common/{name}/{binary}"
|
||||
break
|
||||
|
||||
if pe_path is None:
|
||||
raise AppNotInstalledError(
|
||||
f"Failed to find a libraryfolder for the appid '{appid}'"
|
||||
)
|
||||
|
||||
pe_path = Path(pe_path)
|
||||
if pe_path.exists() is False:
|
||||
raise AppMovedError(
|
||||
f"Path '{pe_path}' specified in libraryfolders does not exist"
|
||||
)
|
||||
|
||||
return pe_path
|
||||
|
||||
|
||||
def compare_versions(local: DayZVersion, remote: DayZVersion):
|
||||
"""
|
||||
packaging.version module is not available OOTB on some distributions
|
||||
"""
|
||||
if dayz_version_to_str(local) == dayz_version_to_str(remote):
|
||||
return VersionMatch.SAME_VERSION
|
||||
|
||||
if local.major < remote.major:
|
||||
return VersionMatch.LOCAL_OLDER
|
||||
if local.major > remote.major:
|
||||
return VersionMatch.LOCAL_NEWER
|
||||
if local.major == remote.major:
|
||||
if local.minor < remote.minor:
|
||||
return VersionMatch.LOCAL_OLDER
|
||||
if local.minor > remote.minor:
|
||||
return VersionMatch.LOCAL_NEWER
|
||||
if local.minor == remote.minor:
|
||||
if local.patch < remote.patch:
|
||||
return VersionMatch.LOCAL_OLDER
|
||||
if local.patch > remote.patch:
|
||||
return VersionMatch.LOCAL_NEWER
|
||||
if local.patch == remote.patch:
|
||||
return VersionMatch.SAME_VERSION
|
||||
|
||||
|
||||
def vdf_to_json(stream):
|
||||
def _istr(indent, string):
|
||||
return (indent * " ") + string
|
||||
|
||||
jbuf = "{\n"
|
||||
lex = shlex(stream)
|
||||
indent = 1
|
||||
|
||||
while True:
|
||||
tok = lex.get_token()
|
||||
if not tok:
|
||||
return jbuf + "}\n"
|
||||
if tok == "}":
|
||||
indent -= 1
|
||||
jbuf += _istr(indent, "}")
|
||||
ntok = lex.get_token()
|
||||
lex.push_token(ntok)
|
||||
if ntok and ntok != "}":
|
||||
jbuf += ","
|
||||
jbuf += "\n"
|
||||
else:
|
||||
ntok = lex.get_token()
|
||||
if ntok == "{":
|
||||
jbuf += _istr(indent, tok + ": {\n")
|
||||
indent += 1
|
||||
else:
|
||||
jbuf += _istr(indent, tok + ": " + ntok)
|
||||
ntok = lex.get_token()
|
||||
lex.push_token(ntok)
|
||||
if ntok != "}":
|
||||
jbuf += ","
|
||||
jbuf += "\n"
|
||||
@ -1,467 +0,0 @@
|
||||
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
|
||||
|
||||
for key in [
|
||||
"map",
|
||||
"gametype",
|
||||
"players",
|
||||
"max_players",
|
||||
"addr",
|
||||
"gameport",
|
||||
]:
|
||||
try:
|
||||
row[key]
|
||||
except KeyError:
|
||||
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=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):
|
||||
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
|
||||
6288
helpers/ui.py
6288
helpers/ui.py
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user