Compare commits

..

127 Commits

Author SHA1 Message Date
aclist
5654b327d4 fix: early abort if local coords are missing 2025-10-17 04:20:13 +09:00
Gergely Koloszar
0ad6366f0a Fix unintended deletion 2025-10-16 20:53:14 +02:00
Gergely Koloszar
a7c647aa6b Add availability checks for local_latlon function 2025-10-16 20:51:46 +02:00
Gergely Koloszar
3d32721683 Add availability checker for remote services based on ping and timeout 2025-10-16 20:50:42 +02:00
aclist
881b0b3008
Merge pull request #218 from aclist/feat/coc
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
feat: add code of conduct
2025-06-05 17:48:05 +09:00
aclist
2a59441dbc feat: add code of conduct 2025-06-05 17:47:07 +09:00
aclist
acdb34d36a
Merge pull request #206 from aclist/chore/index-url
chore: update to new index url
2025-03-15 17:01:19 +09:00
aclist
d6d0b241e0 chore: update to new index url 2025-03-15 17:00:08 +09:00
aclist
2ba07bbba8
Merge pull request #197 from aclist/dzgui
chore: merge stable onto archival branch
2025-02-09 19:55:27 +09:00
aclist
65eda05d9f
Merge pull request #200 from aclist/chore/templates-stable
chore: back up templates from stable
2025-02-09 19:54:17 +09:00
aclist
f3b3f79bfb chore: back up templates from main 2025-02-09 19:53:25 +09:00
aclist
28c7d7f949
Merge pull request #198 from aclist/fix/drop-files
chore: drop extraneous file
2025-02-09 19:02:02 +09:00
aclist
d072c8107e chore: drop extraneous file 2025-02-09 19:01:30 +09:00
aclist
0629c8a6ba
Merge pull request #196 from aclist/release/5.6.3
chore: update IP db
2025-02-09 18:56:13 +09:00
aclist
8aea4af687 chore: update IP db 2025-02-09 18:55:33 +09:00
aclist
0562d8230d
Merge pull request #195 from aclist/release/5.6.2
chore: prepare release
2025-01-24 16:40:07 +09:00
aclist
759327e932 chore: prepare release 2025-01-23 12:02:27 +09:00
aclist
36753e02f1
Merge pull request #193 from aclist/release/5.6.1
Release/5.6.1
2025-01-10 22:48:54 +09:00
aclist
2ab2e23736 docs: update readme 2025-01-10 22:45:56 +09:00
aclist
34e7b2e480 fix: jq 1.6 support 2025-01-10 22:45:25 +09:00
aclist
540d0ad7e4
Update bug_report.yml 2025-01-09 23:32:28 +09:00
aclist
47bea45111
Update feature.yml 2025-01-09 23:32:27 +09:00
aclist
d03346b56a
Update trouble.yml 2025-01-09 23:32:26 +09:00
aclist
54e0762882
Update feature.yml 2025-01-09 23:31:21 +09:00
aclist
0834a86d33
Update trouble.yml 2025-01-09 23:30:54 +09:00
aclist
c6a8841614
Update trouble.yml 2025-01-09 23:30:37 +09:00
aclist
df92acb7dc
Update bug_report.yml 2025-01-09 23:30:24 +09:00
aclist
a333571f94
Update bug_report.yml 2025-01-09 23:28:46 +09:00
aclist
05f88b6019
Update trouble.yml 2025-01-09 23:27:08 +09:00
aclist
1f31c43fe4
Update trouble.yml 2025-01-09 20:39:35 +09:00
aclist
0763416903
Update feature.yml 2025-01-09 20:39:25 +09:00
aclist
3b384c5a4d
Update bug_report.yml 2025-01-09 20:39:17 +09:00
aclist
3ab666a807
Merge pull request #190 from aclist/release/5.6.0
Release/5.6.0
2025-01-07 21:46:53 +09:00
aclist
c8f7a32284 chore: update db checksum 2025-01-07 21:39:16 +09:00
aclist
a070e9fc65 chore: drop unused log file 2025-01-07 20:54:03 +09:00
aclist
5f55e3c664 Merge branch 'release/5.6.0' of github.com:aclist/dztui into release/5.6.0 2025-01-07 20:52:58 +09:00
aclist
07606d8852 docs: update changelog 2025-01-07 20:51:57 +09:00
aclist
afffc90abc
Update trouble.yml 2025-01-07 19:55:31 +09:00
aclist
70db7b45b3
Update trouble.yml 2025-01-07 19:54:19 +09:00
aclist
aac8558079
Update trouble.yml 2025-01-07 19:53:15 +09:00
aclist
ac8ff2febf
Update trouble.yml 2025-01-07 19:52:29 +09:00
aclist
6369d895fd
Update feature.yml 2025-01-07 19:51:58 +09:00
aclist
7413f7c029
Update bug_report.yml 2025-01-07 19:51:46 +09:00
aclist
3f1d8cb24f
Merge branch 'dzgui' into release/5.6.0 2025-01-07 00:59:57 +09:00
aclist
4df12605fc chore: update changelog 2025-01-07 00:55:58 +09:00
aclist
2c5da88e06
Update bug_report.yml 2025-01-06 20:28:34 +09:00
aclist
3cb6175436
Update feature.yml 2025-01-06 20:28:23 +09:00
aclist
3739f19ab2
Update trouble.yml 2025-01-06 20:28:12 +09:00
aclist
2da4d001d6
Update trouble.yml 2025-01-06 20:27:30 +09:00
aclist
5d61a1f513
Update trouble.yml 2025-01-06 20:26:29 +09:00
aclist
56c80d187b
Update trouble.yml 2025-01-06 20:26:20 +09:00
aclist
e08f66cdcb
Update trouble.yml 2025-01-06 20:25:22 +09:00
aclist
48fee301bb
Update trouble.yml 2025-01-06 20:24:47 +09:00
aclist
20511f5661
Update trouble.yml 2025-01-06 20:24:14 +09:00
aclist
d2e8333ba9
Update trouble.yml 2025-01-06 20:21:10 +09:00
aclist
5d37039064
Update trouble.yml 2025-01-06 20:20:57 +09:00
aclist
ce44cdbc54
Update trouble.yml 2025-01-06 20:20:36 +09:00
aclist
5b2738420e
Create trouble.yml 2025-01-06 20:19:41 +09:00
aclist
1e29288e2a
Update feature.yml 2025-01-06 20:10:08 +09:00
aclist
745cc06091
Update feature.yml 2025-01-06 20:09:08 +09:00
aclist
0311cc8a95
Create feature.yml 2025-01-06 20:08:29 +09:00
aclist
7c3521cf97
Delete .github/ISSUE_TEMPLATE/feature.md 2025-01-06 20:08:16 +09:00
aclist
bb0eefadd2
Delete .github/ISSUE_TEMPLATE/test.yml 2025-01-06 20:03:41 +09:00
aclist
7eb5a4fac5
Update bug_report.yml 2025-01-06 20:02:52 +09:00
aclist
ebce329c3d
Delete .github/ISSUE_TEMPLATE/bug_report.md 2025-01-06 20:02:38 +09:00
aclist
23464d3b51
Update bug_report.yml 2025-01-06 20:01:30 +09:00
aclist
602a7b8edc
Update bug_report.yml 2025-01-06 19:59:20 +09:00
aclist
59c58a1df5
Update bug_report.yml 2025-01-06 19:57:56 +09:00
aclist
830318d98e
Update bug_report.yml 2025-01-06 19:57:00 +09:00
aclist
271c3e6197
Update bug_report.yml 2025-01-06 19:56:39 +09:00
aclist
1ab9634069
Update bug_report.yml 2025-01-06 19:54:29 +09:00
aclist
8666b0055a
Update bug_report.yml 2025-01-06 19:53:52 +09:00
aclist
406554e011
Create bug_report.yml 2025-01-06 19:50:39 +09:00
aclist
5334d07f7f
Create test.yml 2025-01-06 19:41:34 +09:00
aclist
6cab96d1f8
Create config.yml 2025-01-06 19:39:18 +09:00
aclist
bc94d989d4
Delete .github/feature.md 2025-01-06 19:37:57 +09:00
aclist
e4fff5f242
Create feature.md 2025-01-06 19:37:38 +09:00
aclist
53f2fe922a
Create feature.md 2025-01-06 19:37:00 +09:00
aclist
800f68a1b7
Update bug_report.md 2025-01-06 19:32:55 +09:00
aclist
c6c377e8b9
Update bug_report.md 2025-01-06 19:30:16 +09:00
aclist
7a68d7e097
Update issue templates 2025-01-06 19:27:44 +09:00
aclist
f60a6edf2b
Merge pull request #182 from aclist/patch/yaml
Patch/yaml
2024-12-25 14:21:49 +09:00
aclist
8c33a1b371 chore: drop branch logic 2024-12-25 14:21:24 +09:00
aclist
e5f7391b89 chore: add YAML file 2024-12-25 14:20:36 +09:00
aclist
a8f51280e8
Update mirror.yml 2024-12-25 14:19:04 +09:00
aclist
c19346529b
Update mirror.yml 2024-12-25 14:12:08 +09:00
aclist
85451f765b
Update mirror.yml 2024-12-25 14:09:54 +09:00
aclist
5a144f7815
Merge pull request #179 from jiriks74/add_mirror_action
feat(ci): Add workflow for pushing to Codeberg
2024-12-25 12:42:58 +09:00
jiriks74
2370e81403
feat(ci): Add workflow for pushing to Codeberg 2024-12-17 17:24:32 +01:00
aclist
c11511abff Merge pull request 'chore: backports into stable' (#5) from release/testing-backports into dzgui
Reviewed-on: https://codeberg.org/aclist/dztui/pulls/5
2024-12-13 01:50:01 +00:00
aclist
bda7223ac8
Merge pull request #176 from aclist/release/testing-backports
Release/testing backports
2024-12-13 10:47:54 +09:00
aclist
6b48ee127d chore: backports into stable 2024-12-13 10:45:39 +09:00
aclist
aa8a52bfc0 Merge pull request 'release/readme' (#4) from release/readme into dzgui
Reviewed-on: https://codeberg.org/aclist/dztui/pulls/4
2024-12-12 09:33:32 +00:00
aclist
8ea62434ee docs: update docs link 2024-12-12 18:32:44 +09:00
aclist
baeaeb1b64 docs: update README 2024-12-12 17:57:27 +09:00
aclist
32b424da5c fix: update installscript 2024-12-12 12:25:26 +09:00
aclist
d71110d0a1 fix: url syntax 2024-12-12 12:10:04 +09:00
aclist
bc9178d2de fix: triage hotfix from testing 2024-12-12 11:56:10 +09:00
aclist
1533febf9a
Merge pull request #172 from aclist/release/5.5.1
fix: backport hotfix (#168)
2024-12-03 20:10:29 +09:00
aclist
c3c8872fad fix: backport hotfix (#168) 2024-12-03 20:09:19 +09:00
aclist
bae6a57e1e
Merge pull request #164 from aclist/dzgui
5.5.0 backports
2024-11-11 09:35:17 +09:00
aclist
3bdc0057a2
Merge pull request #163 from aclist/release/5.5.0-stable
Release/5.5.0 stable
2024-11-11 09:22:06 +09:00
aclist
5496f914f2
fix: correct typo 2024-11-11 09:17:47 +09:00
aclist
cb9db55bcb
Merge branch 'dzgui' into release/5.5.0-stable 2024-11-11 06:08:47 +09:00
aclist
c0ab8111fd chore: bump version 2024-11-11 06:05:56 +09:00
aclist
10c6a79b17 chore: normalize changelog 2024-11-11 06:05:49 +09:00
aclist
2b4b533a5b
Merge pull request #156 from aclist/release/5.4.2
Release/5.4.2
2024-10-06 03:31:10 +09:00
aclist
dcb8be9132 chore: update checksum 2024-10-06 03:27:49 +09:00
aclist
278aa8c28a chore: update checksum 2024-10-06 03:26:24 +09:00
aclist
32e5bafb33
Merge branch 'dzgui' into release/5.4.2 2024-10-06 03:25:16 +09:00
aclist
59e19510e3
Merge pull request #152 from aclist/release/5.4.1
Release/5.4.1
2024-09-28 20:51:19 +09:00
aclist
45ab112970
Merge branch 'dzgui' into release/5.4.1 2024-09-25 23:11:28 +09:00
aclist
ed07fd0343 chore: normalize changelog 2024-09-25 23:08:29 +09:00
aclist
c2c7b37b90
Merge pull request #151 from aclist/release/5.3.0-backports
Release/5.3.0 backports
2024-09-09 17:16:31 +09:00
aclist
23798c91d3
Merge pull request #149 from aclist/release/5.3.0
chore: normalize version
2024-09-01 13:19:35 +09:00
aclist
659cbb83a3 chore: normalize version 2024-09-01 13:19:16 +09:00
aclist
32c467fc6d
Merge pull request #147 from aclist/release/5.3.3
Release/5.3.3 (stable)
2024-09-01 13:13:54 +09:00
aclist
d01add3573 docs: update changelog 2024-08-29 22:11:49 +09:00
aclist
0de89a3ef1 add: set WM_CLASS name 2024-08-28 08:44:13 +09:00
aclist
5686ea76c2 fix: boolean operator 2024-08-28 08:09:32 +09:00
aclist
5fda598173 chore: update url 2024-08-28 07:31:14 +09:00
aclist
049fb47019 chore: normalize version 2024-08-28 07:30:38 +09:00
aclist
81f269b71b chore: normalize changelog 2024-08-28 07:29:02 +09:00
aclist
c97a7a260f
Merge pull request #133 from aclist/docs/kb
docs: update API info
2024-07-14 22:26:03 +09:00
aclist
b715eb5a3e
Merge pull request #132 from aclist/docs/kb
docs: fix url
2024-07-06 20:45:12 +09:00
aclist
8e6cf7e792
Merge pull request #126 from aclist/dzgui
backports
2024-06-23 12:51:24 +09:00
aclist
98868073a5
Merge pull request #124 from aclist/dzgui
backports
2024-06-23 12:00:35 +09:00
6 changed files with 2545 additions and 5770 deletions

View File

@ -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
View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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

File diff suppressed because it is too large Load Diff