Compare commits

...

263 Commits

Author SHA1 Message Date
aclist
b2f6abd667
Merge pull request #276 from aclist/chore/update-readme
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
chore: update readme
2026-03-30 20:56:32 +09:00
aclist
f76985c79d chore: reword readme 2026-03-30 20:14:45 +09:00
aclist
3d3cd19892 chore: update readme 2026-03-30 20:13:20 +09:00
aclist
c14fb10d23
Merge pull request #270 from aclist/prerelease/6.0.2
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: dialogs in thread
2026-01-26 18:25:52 +09:00
aclist
a8417d27d7 fix: dialogs in thread 2026-01-26 17:00:55 +09:00
aclist
2d58bb491e
Merge pull request #264 from aclist/fix/explicit-version
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: explicitly use python 3.13
2026-01-17 01:19:40 +09:00
aclist
2f123da63a fix: explicitly use python 3.13 2026-01-17 01:18:40 +09:00
aclist
7016edf7cd
Merge pull request #259 from aclist/prerelease/6.0.0
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
Prerelease/6.0.0
2026-01-15 05:38:25 +09:00
aclist
b630cf4080 fix: write file after wiping cache
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-01-15 05:13:35 +09:00
aclist
1034a5ac04 fix: store source path 2026-01-15 05:04:10 +09:00
aclist
ebc62b0b1b fix: steam deck compatible source path expansion 2026-01-15 04:30:44 +09:00
aclist
4b47ff1fe0 fix: remove duplicate line 2026-01-15 03:41:38 +09:00
aclist
e3155ad7ee fix: patch changes from testing 2026-01-15 03:08:00 +09:00
aclist
c45d9fb234
Update funcs 2026-01-15 03:02:24 +09:00
aclist
22e92e67a7
Merge branch 'dzgui' into prerelease/6.0.0 2026-01-15 03:01:14 +09:00
aclist
cb45ce7b9a chore: prepare 6.0.0 release
Some checks are pending
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-01-15 02:48:05 +09:00
aclist
9678d41b4c
Merge pull request #258 from aclist/fix/first-dialog
fix/first dialog
2026-01-15 02:30:33 +09:00
aclist
0daf567111 feat: more descriptive help dialogs
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-01-15 02:29:00 +09:00
aclist
b75e99ffd7 feat: add link to help page 2026-01-09 10:47:56 +09:00
aclist
5562c44320 fix: regression introduced in previous hotfix
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-01-04 21:39:18 +09:00
aclist
236a5dd0a3
Merge pull request #256 from aclist/fix/unlink-loop
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
chore: normalize checksums
2026-01-04 21:34:41 +09:00
aclist
4c10958d10 chore: normalize checksums 2026-01-04 21:33:56 +09:00
aclist
23f7140c66
Merge pull request #255 from aclist/fix/unlink-loop
fix: regression introduced in 5.8.1
2026-01-04 21:25:38 +09:00
aclist
fbcfa1fb34 fix: regression introduced in 5.8.1. 2026-01-04 21:24:06 +09:00
aclist
52f36e0d0a
Merge pull request #252 from aclist/fix/setup-whitespaces
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: whitespaces during setup
2025-12-28 00:03:40 +09:00
aclist
d90c069a73 chore: correct version number in changelog 2025-12-28 00:01:30 +09:00
aclist
d2f3eebd42 chore: update changelog
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2025-12-27 23:57:11 +09:00
aclist
af64ebb12a fix: enclose whitespaces during setup 2025-12-27 23:56:18 +09:00
aclist
929f497b9f chore: reword changelog
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2025-12-10 13:05:46 +09:00
aclist
60aebe5eeb chore: update date 2025-12-10 13:04:54 +09:00
aclist
aef9a10c42
Merge pull request #246 from aclist/prerelease/6.0.0-beta.14
fix: newlines in tooltips
2025-12-10 13:03:22 +09:00
aclist
c6034bc6b9 fix: strip newlines 2025-12-09 13:03:52 +09:00
aclist
005288371b fix: newlines in tooltips 2025-12-05 15:58:28 +09:00
aclist
53e50a5716
Merge pull request #245 from aclist/fix/6.0.0-beta.13
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: hide filter panel when changing contexts
2025-11-30 04:55:19 +09:00
aclist
a50b11633f fix: update variable name 2025-11-30 04:54:50 +09:00
aclist
9c4541fe76 chore: update changelog 2025-11-30 04:53:43 +09:00
aclist
ac3e7ee128 fix: incorrect changelog path 2025-11-30 04:51:43 +09:00
aclist
1495ad386d fix: hide filter panel when changing contexts 2025-11-30 04:43:18 +09:00
aclist
4edf71aa0a docs: update changelog
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2025-11-22 01:04:59 +09:00
aclist
113e5f885b fix: normalize beta version 2025-11-22 01:04:19 +09:00
aclist
a3092b1bf3
Merge pull request #239 from aclist/prerelease/6.0.1-beta.1
fix: UI not starting if changelog was missing
2025-11-22 00:50:38 +09:00
aclist
e28929bb70 chore: remove reference branch
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2025-11-22 00:47:36 +09:00
aclist
4a820e3083 chore: uncomment line
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2025-11-15 20:24:01 +09:00
aclist
d866b4992b fix: update error message string
Some checks are pending
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2025-11-14 13:41:47 +09:00
aclist
376c5294d9 chore: require python 3.13 2025-11-14 13:16:14 +09:00
aclist
71651bab6c chore: update reference branch
Some checks are pending
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2025-11-13 12:54:31 +09:00
aclist
1dba57af4b chore: update reference branch 2025-11-13 12:53:59 +09:00
aclist
dabf5d4ef0 fix: uncomment lines 2025-11-13 12:53:41 +09:00
aclist
75f8c841df feat: support sandboxed flatpak 2025-11-13 12:50:27 +09:00
aclist
5bce8709af chore: enforce stricter python version
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2025-11-10 12:07:35 +09:00
aclist
a418bcfb90 fix: return from finally block 2025-11-10 11:42:26 +09:00
aclist
90d59cec97 chore: drop legacy src_path logic 2025-11-06 14:52:35 +09:00
aclist
a8ae07931d feat: disable version check on beta branch
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2025-11-06 14:45:04 +09:00
aclist
a4ce65fe57 feat: use reference branch for beta releases 2025-11-06 14:30:16 +09:00
aclist
d7e94eab90 fix: UI not starting if changelog was missing 2025-11-05 14:27:15 +09:00
aclist
a08c588df3 chore: update changelog date
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2025-11-04 16:00:31 +09:00
aclist
385b1a5af2
Merge pull request #230 from aclist/prerelease/6.0.0-beta.8
Prerelease/6.0.0-beta.8
2025-11-04 15:59:48 +09:00
aclist
be02f086c7 docs: update changelog
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2025-11-03 12:43:08 +09:00
aclist
cc35b2cd85
Merge pull request #233 from GaryBlackbourne/prerelease/latlon-update
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
Prerelease/latlon update
2025-10-31 10:06:13 +09:00
aclist
a419aad160 chore: move comments 2025-10-31 10:05:21 +09:00
aclist
21d4601e41 chore: add comments about pretty printing 2025-10-31 10:01:46 +09:00
aclist
a1d14669c7
Merge pull request #237 from aclist/doc/port-contributing
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
docs: add contributing file
2025-10-30 11:49:04 +09:00
aclist
a94f8f0e32
Merge pull request #236 from GaryBlackbourne/doc/add-contributing
Some checks are pending
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
Doc/add contributing
2025-10-30 11:36:43 +09:00
aclist
0da098afb6 chore: mention branch basing conventions 2025-10-30 11:36:10 +09:00
aclist
c8e9f8f1fc chore: move location of notes file in advance of 7.0 release
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2025-10-26 21:53:19 +09:00
aclist
506c82e86a chore: reword CONTRIBUTING.md
Corrected some typos and reworded passive voice/some sections for
brevity.
2025-10-25 11:51:19 +09:00
aclist
7e45cb0fa2 chore: backport funcs file changes from testing
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2025-10-25 11:37:48 +09:00
aclist
3729d3c656 chore: update changelog 2025-10-25 11:35:22 +09:00
aclist
773d24a00c chore: backport patch from testing 2025-10-25 11:33:42 +09:00
aclist
661329877f
Merge pull request #232 from GaryBlackbourne/fix-deadlock-on-ip-api-server
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
Fix deadlock on ip api server
2025-10-25 11:30:05 +09:00
aclist
08f1f9f4d1
Merge pull request #235 from GaryBlackbourne/dzgui-sh-cleanup
dzgui.sh argument handling and cleanup
2025-10-25 11:28:43 +09:00
Gergely Koloszar
dccdffe761 doc: change branch names to currently used ones 2025-10-24 14:04:37 +00:00
Gergely Koloszar
957ad47b4d doc: add troubleshooting help to contribute.md 2025-10-24 14:01:00 +00:00
Gergely Koloszar
16d8f567a4 doc: add code of conduct notice 2025-10-24 14:00:46 +00:00
Gergely Koloszar
c883fc586c doc: add contributing.md 2025-10-24 13:43:26 +00:00
Gergely Koloszar
cc73f23c1c fix: suppress ping stderr output 2025-10-24 12:35:56 +00:00
aclist
ec7daf7f6b fix: store filepaths in array
I opted to encapsulate these in an array because I was paranoid about
needing to separate each directory with a whitespace when storing them
on a stringwise basis. This should be safer and ensure the paths don't
get concatenated together if the whitespace is omitted.
2025-10-23 13:57:19 +09:00
aclist
d0de2974d8 fix: emit heredoc directly 2025-10-23 13:44:50 +09:00
aclist
5a54d43601 fix: encapsulate usage command 2025-10-23 13:38:56 +09:00
aclist
cbc131028f
chore: capitalize TODO for syntax highlighting 2025-10-23 08:22:20 +09:00
Gergely Koloszar
239a82876e fix: silence ping while checking remote urls 2025-10-22 17:20:56 +02:00
Gergely Koloszar
1dfe9cd9df Fix not checking dotfiles in cache path 2025-10-21 19:04:38 +00:00
Gergely Koloszar
f2124218e4 Add handling for multiple argument, version getting option and help 2025-10-18 23:12:59 +02:00
Gergely Koloszar
cf087d96c7 Add check for script sourcing 2025-10-18 23:12:31 +02:00
Gergely Koloszar
6bb59f4060 Clean up depcheck 2025-10-18 22:53:14 +02:00
Gergely Koloszar
d53476399d Clean up setup_state_files 2025-10-18 22:52:49 +02:00
Gergely Koloszar
d34293e2c8 Clean up setup_dirs 2025-10-18 22:52:13 +02:00
Gergely Koloszar
f82df9eacf Clean up set_im_module 2025-10-18 22:51:52 +02:00
Gergely Koloszar
e73617c2f4 Add gitignore for latlon 2025-10-18 21:14:39 +02:00
Gergely Koloszar
67b0559058 Add makefile, and move latlon to a 'tools' directory 2025-10-18 21:13:15 +02:00
Gergely Koloszar
dfe65aacd4 Rewrite and document latlon tool source
- incorrect argument count has been fixed
- The code has been split up into smaller units to be more
understandable
- sources were put in comments to help find resources for the
algorithm
- output is now in meters
2025-10-18 21:00:00 +02:00
aclist
6aa1ec4201 fix: early abort if local coords are missing 2025-10-17 19:12:59 +02:00
Gergely Koloszar
d6223e54b0 Fix unintended deletion 2025-10-17 19:12:59 +02:00
Gergely Koloszar
83552f833c Add availability checks for local_latlon function 2025-10-17 19:12:59 +02:00
Gergely Koloszar
f0657be289 Add availability checker for remote services based on ping and timeout 2025-10-17 19:12:59 +02:00
aclist
23fb10f48e fix: malformed servers
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2025-10-16 22:38:52 +09:00
aclist
eeca71fc25 drop: tutorial images
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2025-10-03 20:20:10 +09:00
aclist
89f37ab70d fix: use fixed-width property 2025-10-03 19:52:55 +09:00
aclist
9d1ee7e130 chore: update checksums
Some checks are pending
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2025-10-03 01:22:02 +09:00
aclist
9a37cebe47 fix: f-string in dialog title 2025-10-03 01:20:40 +09:00
aclist
239f806f7e chore: update changelog
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2025-09-30 19:09:39 +09:00
aclist
54e2d2feaa fix: block tooltips on main menu 2025-09-30 19:08:04 +09:00
aclist
796dcd5588 fix: missing param 2025-09-30 19:07:42 +09:00
aclist
35f1f180d2 chore: bump version 2025-09-23 20:56:00 +09:00
aclist
fe40a5e54f feat: self_update flag 2025-09-23 20:46:12 +09:00
aclist
936dfe8d02 fix: correct date
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2025-09-20 18:16:50 +09:00
aclist
9db40fe664
Merge pull request #229 from aclist/prerelease/6.0.0-beta.7
Prerelease/6.0.0-beta.7
2025-09-20 18:11:57 +09:00
aclist
5bcc4ec76d chore: bump version 2025-09-18 21:37:35 +09:00
aclist
155306b1ab fix: incorrect version number 2025-09-18 20:08:36 +09:00
aclist
cff7b1c2e9 feat: add note saving 2025-09-18 20:04:44 +09:00
aclist
38203a9530 feat: add read_json(), write_json() 2025-09-18 20:03:35 +09:00
aclist
01c8a5276b chore: abstract default size params 2025-09-18 20:02:12 +09:00
aclist
17229c1cba chore: rename get_record_dict() to get_record() 2025-09-18 20:01:51 +09:00
aclist
adc617730b fix: change f-string 2025-09-18 20:00:42 +09:00
aclist
8acd359028 chore: update comments 2025-09-18 20:00:15 +09:00
aclist
a03971ce70 feat: add get_entry() to EntryDialog 2025-09-18 19:59:51 +09:00
aclist
054486dfef chore: rename var 2025-09-18 19:59:28 +09:00
aclist
3043005977 chore: drop unused import 2025-09-18 19:58:49 +09:00
aclist
998b15d5e8
Merge pull request #228 from aclist/prerelease/6.0.0-beta.6
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
Prerelease/6.0.0-beta.6
2025-09-12 19:05:37 +09:00
aclist
9d8bbec03a chore: update changelog
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2025-09-12 18:03:38 +09:00
aclist
6425f3d8e3 chore: update checksums
Some checks are pending
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2025-09-11 18:35:12 +09:00
aclist
ad64ba75c9 chore: reword failure message 2025-09-11 18:34:12 +09:00
aclist
47f7622606 fix: return codes in manual mode 2025-09-11 18:33:59 +09:00
aclist
81c2d19a09 chore: update checksums
Some checks are pending
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2025-09-11 17:46:32 +09:00
aclist
b500806815 chore: drop dependency 2025-09-11 17:45:58 +09:00
aclist
60ce184d8f fix: whitespace 2025-09-11 17:10:16 +09:00
aclist
2edb81247b chore: update checksums 2025-09-11 17:09:59 +09:00
aclist
7b67222885 fix: move docstring 2025-09-11 17:09:28 +09:00
aclist
4ddb33cabb chore: update checksums 2025-09-11 17:08:06 +09:00
aclist
364f0b3482 chore: update dataclass params 2025-09-11 17:06:39 +09:00
aclist
5482f296a5 chore: formatting 2025-09-11 17:01:57 +09:00
aclist
0d52c83ed9 chore: refactor pefile methods 2025-09-11 16:58:58 +09:00
aclist
b92e337016 chore: bump version 2025-09-10 16:57:36 +09:00
aclist
ee73c2cfa3 feat: parse pefile 2025-09-10 16:45:49 +09:00
aclist
8ffb1c7ea6 fix: version file not available yet 2025-09-10 16:43:14 +09:00
aclist
9040977447 chore: drop defunct methods 2025-09-10 16:42:09 +09:00
aclist
da6cc9c5c3 chore: reword log message 2025-09-10 16:39:43 +09:00
aclist
c40d767dcc chore: drop extraneous logging 2025-09-10 16:39:25 +09:00
aclist
6f3b272bfe fix: floating point syntax with printf 2025-09-10 16:38:47 +09:00
aclist
ffe3dac3a8 fix: mods downloading too fast 2025-09-10 16:38:06 +09:00
aclist
5fa8498c56 fix: clear linting errors 2025-09-10 16:36:42 +09:00
aclist
c0b5c5bd89 feat: parse hyperlinks 2025-09-10 16:34:58 +09:00
aclist
6d428ebb0d fix: ease padding on mod table 2025-09-10 16:34:05 +09:00
aclist
479be05452 chore: clarify comments 2025-09-10 16:32:33 +09:00
aclist
95a775cd74 fix: newline in tooltip 2025-09-10 16:32:14 +09:00
aclist
1bfb054be2 fix: ease padding on mod table 2025-09-10 16:31:54 +09:00
aclist
6b31bb3ab3 fix: resolution shrinking by default 2025-09-10 16:30:02 +09:00
aclist
c5b13ff0f3 fix: strip newlines in history file 2025-09-10 16:29:22 +09:00
aclist
5a33591d6a chore: drop extraneous path expansion 2025-09-10 16:29:03 +09:00
aclist
62b00507e5 fix: unblock signals during continuous keypress 2025-09-10 16:28:33 +09:00
aclist
d56bc5a57d
Merge pull request #227 from aclist/prerelease/6.0.0-beta.5
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: discard malformed server data
2025-08-20 15:58:51 +09:00
aclist
0b433af230 fix: discard malformed server data 2025-08-20 15:58:09 +09:00
aclist
61f3fd9821
Merge pull request #226 from aclist/prerelease/6.0.0-beta.4
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: toggle branch signal propagation
2025-08-13 16:47:32 +09:00
aclist
c7bd8d3809 chore: reword changelog 2025-08-13 16:46:24 +09:00
aclist
0d3a3debf9 chore: uncomment lines 2025-08-13 16:45:32 +09:00
aclist
8e492095d9 chore: bump version 2025-08-13 16:44:48 +09:00
aclist
6a554b4e88 fix: toggle branch signal propagation 2025-08-13 16:40:55 +09:00
aclist
e2848b3fed
Merge pull request #225 from aclist/prerelease/6.0.0-beta.3
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
Prerelease/6.0.0 beta.3
2025-08-09 16:13:40 +09:00
aclist
a5be4d9d31 chore: bump version
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2025-08-09 16:06:07 +09:00
aclist
da5a5999f0 chore: uncomment 2025-08-09 16:01:30 +09:00
aclist
f4f3e61b83 fix: pass correct button to submit method 2025-08-09 16:01:00 +09:00
aclist
3d91e07d25 chore: uncomment todo 2025-08-09 15:47:22 +09:00
aclist
cff4a2e1e7 chore: update checksums 2025-08-09 11:32:43 +09:00
aclist
4f3f17450c chore: comments and clerical changes 2025-08-08 18:22:51 +09:00
aclist
5d8741edfa chore: drop unnecessary comment 2025-08-08 18:15:28 +09:00
aclist
94cabc9d70 chore: update checksums 2025-08-08 18:11:26 +09:00
aclist
9508bfd416 chore: update changelog 2025-08-08 18:09:53 +09:00
aclist
3ace15d78a chore: bump version 2025-08-08 18:04:49 +09:00
aclist
2bb575cd97 chore: update return codes 2025-08-08 18:04:32 +09:00
aclist
5dfa51dc15 feat: options page 2025-08-08 18:04:17 +09:00
aclist
5dd0cd416b chore: reword comment 2025-08-08 18:02:02 +09:00
aclist
6f5f9ecb7b fix: resolution not updating when returning from fullscreen 2025-08-08 18:01:37 +09:00
aclist
5b9309a6bd fix: discard key input in entry field 2025-08-08 18:01:05 +09:00
aclist
9c6847ba39 feat: toast notification 2025-08-08 18:00:33 +09:00
aclist
8f3238e4de feat: prompt on locked servers 2025-08-08 17:59:10 +09:00
aclist
8c7ae8acee fix: wrap description text 2025-08-08 17:58:33 +09:00
aclist
9d94b4ff09 chore: clear linting errors 2025-08-08 17:57:43 +09:00
aclist
4795aeb3f9 chore: rename function 2025-08-08 17:55:40 +09:00
aclist
12d971a5cf fix: no statusbar in special pages 2025-08-08 17:54:32 +09:00
aclist
ff0c5caf30 feat: add quit dialog 2025-08-08 17:51:29 +09:00
aclist
63beb61878 feat: make back button optional 2025-08-08 17:50:37 +09:00
aclist
6c192741ec chore: consolidate vars 2025-08-08 17:48:55 +09:00
aclist
437ea2a218 fix: breadcrums not updating correctly 2025-08-08 17:48:26 +09:00
aclist
ee0fd7b78c fix: handle unspecified args 2025-08-08 17:47:37 +09:00
aclist
562f6453dc fix: use correct path 2025-08-08 17:46:58 +09:00
aclist
51308e788e feat: parse server details 2025-08-08 17:41:38 +09:00
aclist
acc356d3a0 chore: resolve QA errors 2025-08-08 17:40:38 +09:00
aclist
eaedde7ee5 fix: clarify wording 2025-08-08 17:39:52 +09:00
aclist
43f32638f8 chore: bump version
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2025-07-30 02:25:29 +09:00
aclist
12b82cd1dd
Merge pull request #224 from aclist/prerelease/6.0.0
Prerelease/6.0.0
2025-07-30 02:11:53 +09:00
aclist
6081e6d2a7 chore: update funcs version 2025-07-30 00:59:49 +09:00
aclist
f7af8ff35a chore: update changelog 2025-07-30 00:58:48 +09:00
aclist
51a5f4b8ef chore: update checksums 2025-07-30 00:58:16 +09:00
aclist
f9ba990142 fix: set expand properties of columns 2025-07-30 00:57:42 +09:00
aclist
09fdbf3a6f chore: update changelog 2025-07-30 00:52:04 +09:00
aclist
c57af3086c chore: update checksums 2025-07-30 00:46:32 +09:00
aclist
4e5f988ef7 feat: parse description field 2025-07-30 00:45:22 +09:00
aclist
e8c51cbadf feat: details dialog 2025-07-30 00:44:46 +09:00
aclist
495e7c14be chore: drop ping from statusbar 2025-07-30 00:44:01 +09:00
aclist
b4ae6a9e8b feat: ping column 2025-07-29 03:01:37 +09:00
aclist
99e13be3e8 chore: update checksums 2025-07-26 02:47:15 +09:00
aclist
760634d3d8 change: normalize steam deck headers 2025-07-26 02:46:11 +09:00
aclist
ae67124c10 fix: change return type 2025-07-26 02:45:51 +09:00
aclist
235f4bca37 fix: add version file check 2025-07-26 02:45:23 +09:00
aclist
8ae6d67cca fix: support alternate response format 2025-07-26 02:10:55 +09:00
aclist
50b4ad8e3c fix: use urllib on steam deck 2025-07-26 02:07:48 +09:00
aclist
bc712e926d chore: 6.0.0 beta prerelease 2025-07-26 00:37:59 +09:00
aclist
6d7b2a1d19
Merge pull request #221 from aclist/feat/filter-official
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
feat: filter official
2025-07-06 12:25:16 +09:00
aclist
4c504a7bd9 chore: update versions 2025-07-06 12:22:12 +09:00
aclist
133966ef36 feat: filter official 2025-07-06 12:11:56 +09:00
aclist
ec23a4eb2e
Merge pull request #220 from aclist/feat/coc3
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
feat: add code of conduct
2025-06-05 17:50:37 +09:00
aclist
169f4c051d feat: add code of conduct 2025-06-05 17:50:11 +09:00
aclist
6219fb6a78
Merge pull request #217 from aclist/fix/version_no
fix: version number
2025-06-05 17:32:30 +09:00
aclist
02105807fb fix: version number 2025-06-05 17:31:59 +09:00
aclist
74bfdf42ae
Merge pull request #216 from aclist/feat/fetch_ipdb
feat: auto fetch geolocation records
2025-06-05 17:29:56 +09:00
aclist
ede7f345da fix: change date in changelog 2025-06-05 17:29:06 +09:00
aclist
e18d203449 feat: auto fetch geolocation records 2025-06-05 17:25:14 +09:00
aclist
99fbc7a2cc chore: update IP checksum 2025-04-17 22:14:52 +09:00
aclist
317212ed6a
Merge pull request #212 from aclist/chore/update-checksum
chore: update helper checksum
2025-04-17 21:39:45 +09:00
aclist
7ec8d6c3bf chore: update checksum 2025-04-17 21:39:09 +09:00
aclist
cf053299f7
Merge pull request #210 from aclist/fix/preboot
chore: backports from stable
2025-04-05 19:17:36 +09:00
aclist
5ba23249bf chore: backports from stable 2025-04-05 19:16:28 +09:00
aclist
406943bc12 fix: reduce startup time 2025-03-21 02:15:03 +09:00
aclist
4eb3171891
Merge pull request #205 from aclist/fix/flowed-grid
fix: flowed grid
2025-03-16 12:18:37 +09:00
aclist
38de9a8e9d fix: interpolate file name 2025-03-16 12:04:10 +09:00
aclist
10c8555ddc fix: local var 2025-03-16 12:02:15 +09:00
aclist
ec0e276100 chore: drop logging 2025-03-16 12:00:41 +09:00
aclist
3c235e22ed chore: update changelog 2025-03-15 17:43:28 +09:00
aclist
05e897dd31 chore: update changelog 2025-03-15 16:51:54 +09:00
aclist
1b7254003a chore: update ip db checksum 2025-03-15 16:50:25 +09:00
aclist
acf43b1591 chore: remove newline 2025-03-15 16:40:31 +09:00
aclist
4cbffaf3ff chore: update checksums 2025-03-15 16:31:44 +09:00
aclist
2e2b738ca6 fix: use same padding on all devices 2025-03-15 16:22:03 +09:00
aclist
22758938bd chore: bump ui version 2025-03-15 12:12:44 +09:00
aclist
52961755ff fix: quit signal being blocked in fullscreen 2025-03-15 12:09:51 +09:00
aclist
df872fa4e7 fix: send proper params to halt funcs 2025-03-14 23:47:46 +09:00
aclist
35804df4df fix: move call to other func def 2025-03-14 23:36:52 +09:00
aclist
117814ad61 feat: save window size on quit 2025-03-14 23:34:40 +09:00
aclist
afe2882f3b chore: add test ui 2025-03-12 15:14:39 +09:00
aclist
bba793cc52 fix: bump version 2025-03-05 11:11:00 +09:00
aclist
1b360f84ae fix: split up batch queries 2025-03-05 11:10:41 +09:00
aclist
bb976644cb
Merge pull request #201 from aclist/feat/steam
feat: steam
2025-02-10 17:52:10 +09:00
aclist
aaaf653c57 docs: update URL 2025-02-10 17:31:29 +09:00
aclist
76e8640476 docs: update changelog 2025-02-10 17:28:44 +09:00
aclist
eff6450d49 chore: update checksum 2025-02-10 17:27:52 +09:00
aclist
eee65314e8 fix: abstract funcs file path 2025-02-10 17:27:22 +09:00
aclist
fc6e8ef001 chore: drop unused env var 2025-02-10 17:26:56 +09:00
aclist
08c8862106 feat: check steam env vars 2025-02-10 17:26:18 +09:00
aclist
63307abd52 chore: drop unused var 2025-02-10 17:25:51 +09:00
aclist
2ab0b5bbca
Merge pull request #199 from aclist/chore/templates
chore: back up templates from main
2025-02-09 19:51:43 +09:00
aclist
5f6679cf34 chore: back up templates from main 2025-02-09 19:50:36 +09:00
aclist
5bc503f209 Merge branch 'testing' of github.com:aclist/dztui into testing 2025-02-09 18:53:43 +09:00
aclist
3a1223be9d chore: update IP db 2025-02-09 18:53:32 +09:00
aclist
853f353562 chore: test commit 2025-01-23 12:03:18 +09:00
aclist
25ec6ec08d
Merge pull request #194 from aclist/release/5.7.0-beta.6
fix: resolve regression introduced with 13c6813
2025-01-11 06:43:51 +09:00
aclist
be8bf74401 fix: resolve regression introduced with 13c6813 2025-01-11 06:41:22 +09:00
aclist
63671e2478 fix: use redacted function from helpers 2025-01-10 22:43:00 +09:00
aclist
9e09bd3bb3 fix: uncomment line 2025-01-10 22:41:02 +09:00
aclist
5a292b37c4 fix: jq 1.6 support (#192) 2025-01-10 22:38:20 +09:00
aclist
0dc339893b docs: update changelog 2025-01-09 23:22:42 +09:00
aclist
923bac8f76 fix: symlink traversal 2025-01-09 23:20:18 +09:00
aclist
79b0bae05d chore: bump version 2025-01-07 21:41:34 +09:00
aclist
9738de8fd1 chore: backports from stable 2025-01-07 21:40:39 +09:00
21 changed files with 6123 additions and 2973 deletions

3
.gitignore vendored
View File

@ -1 +1,4 @@
tags tags
__pycache__
pre-commit
tests/

View File

@ -1,6 +1,111 @@
# Changelog # Changelog
## [5.8.0] 2025-12-27 ## [6.0.2] 2026-01-26
## Fixed
- Explicitly use Python 3.13 when calling subprocesses
- Raise error correctly from module
- Do not spawn dialogs from inside of thread when fetching prereqs
## Changed
- Optimize time complexity of startup checks
- Optimize coordinate calculation by using local records first
- Rename some internal functions
- Wrap entire startup process in dialog
## [6.0.1] 2026-01-16
## Fixed
- Explicitly use Python 3.13 when calling subprocesses
## [6.0.0] 2026-01-14
## Added
- Support DayZ Experimental
- Ping display on server tables
- Save descriptive text notes on a per-server basis
- Speed up load time and navigation of server tables
- More robust threading and cache system when filtering servers
- Show additional DayZ client information in Options menu
- Warn user of DayZ client version mismatches
- Support clickable hyperlinks in dialogs
- Rich server metadata details dialog (right-click on server table)
- Filter by modded servers
- Breadcrumbs showing current menu context
- Dedicated changelog page
- Dedicated keybindings page
- Dedicated settings page ("Options")
- Support sandboxed version of Flatpak
- Vim-style navigation keybindings
- Added "Return to main menu" button to dialog windows when failing to load server table
- Dynamic context menus for modded servers
- Additional keybindings for new filter toggles
- If a server is locked, prompt user when connecting
- Internal flag to allow distro-packaged releases to disable in-app updates
- Pre-boot checks to test whether script was invoked directly
- Commandline usage help text (GaryBlackbourne)
- More descriptive help dialogs when initial dependencies are missing
## Fixed
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
- Key stickiness when quickly navigating through entries in tables
- Servers returning malformed A2S_INFO blocking server browser from loading
- Normalized buttons in dialogs and restored proper padding
- GTK errors being emitted to stdout when inserting debug table
- Entry dialog sensitivity when validating API keys
- Additional validation on entry dialogs to prevent submitting empty text
- Suppress errors during pre-boot checks when mods are not installed
- Centered filter checkboxes within panel
- Do not pop unhighlight/select stale buttons if no stale mods exist
- Improved keybinding interaction with side panels
- Suppress typeahead search in mod dialogs
- Do not trigger global API cooldown if no LAN/favorite servers are found
- Fix table column expansion in server mod dialogs
- Missing parameters when closing application via window decorations
- Statusbar contents not updating on certain pages
- Prevent debug button from activating when in certain input fields
- Window size not updating correctly when unmaximizing window after changing "fullscreen at boot" setting
- Branch toggle signal being emitted when entering Options menu from other contexts
- Mods rarely not appearing in local mod list if download completed too quickly
- Statusbar not updating when clicking a row after spamming keyboard input
- Extraneous logs being generated when subscribing to mods
- Narrow width of columns in modlist dialogs occluding text
- Newline terminators in history file
- Floating point number calculation
- Window resizing too small if no prior resolution was set
- ESC key destroying wait dialogs while a thread is pending
- Tooltip signals being erroneously emitted on main menu
- UI not being constructed correctly if CHANGELOG.md was missing
- Config file erroneously getting updated when populating settings menu
- Leaky variable name in dialog titles
- Prevent extraneous signals from propagating when column width is adjusted
- Server filter panel not being hidden when entering other page contexts
- Path to remote changelog being constructed incorrectly
- Dialogs with newlines breaking output in log table
- Path discovery during first-time setup when parsing filepaths with whitespaces
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
## Changed
- Require Python 3.13
- Reduced global API cooldown from 60s to 30s
- Clarify dialog messages when DayZ path could not be found
- Auto-focus first item when opening context menus
- Opacity setting on side buttons when in a different context
- Refactored BM API key validation to account for new key format
- State file serialization methods
- Optimize time complexity of pre-boot checks (GaryBlackbourne)
- Rewrote distance calculation module (GaryBlackbourne)
- Changed preferred client setting from radio toggle to combobox
## Dropped
- Ping readout in statusbar
- Extraneous information from right statusbar
## [5.8.3] 2026-01-04
## Fixed
- Normalize checksum numbers and dates
## [5.8.2] 2026-01-04
## Fixed
- Fix regression in symlink routine introduced by previous hotfix
## [5.8.1] 2025-12-27
## Fixed ## Fixed
- Path discovery during first-time setup when parsing filepaths with whitespaces - Path discovery during first-time setup when parsing filepaths with whitespaces
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it - First-time setup dialog continuously triggering when DayZ install path had whitespaces in it

View File

@ -11,6 +11,15 @@ This overcomes certain limitations in the Linux client and helps prepare the gam
- Concatenate launch options to pass to Steam - Concatenate launch options to pass to Steam
- Connect to mod-enabled LAN servers - Connect to mod-enabled LAN servers
## Design philosophy
DZGUI follows a specific set of guidelines, enumerated below:
- Provide a free and open-source tool under GPL
- Do not use undocumented, third-party servers to aggregate data; no central "DZGUI" server
- All connections go to Steam and DayZ servers directly; the tool should function in perpetuity without intermediary endpoints
- Target Linux desktop and Steam deck specifically; not a cross-platform tool
## Setup and usage ## Setup and usage
Refer to the documentation for installation and setup instructions: Refer to the documentation for installation and setup instructions:
@ -18,7 +27,6 @@ Refer to the documentation for installation and setup instructions:
- [GitHub](https://aclist.github.io/dzgui/index.html) - [GitHub](https://aclist.github.io/dzgui/index.html)
- [Mirror (Codeberg)](https://aclist.codeberg.page) - [Mirror (Codeberg)](https://aclist.codeberg.page)
![A screenshot of DZGUI](/images/example.png) ![A screenshot of DZGUI](/images/example.png)
## Attribution ## Attribution

360
dzgui.sh
View File

@ -1,7 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -o pipefail set -o pipefail
version=5.8.1 src_path="$(readlink -e "$0")"
version=6.0.2
#CONSTANTS #CONSTANTS
aid=221100 aid=221100
@ -39,6 +40,7 @@ cols_file="$state_path/$prefix.cols.json"
#CACHE FILES #CACHE FILES
coords_file="$cache_path/$prefix.coords" coords_file="$cache_path/$prefix.coords"
src_path_file="$cache_path/$prefix.src"
#legacy paths #legacy paths
hist_file="$config_path/history" hist_file="$config_path/history"
@ -63,17 +65,14 @@ testing_url="$url_prefix/testing"
releases_url="https://github.com/$author/$repo/releases/download/browser" releases_url="https://github.com/$author/$repo/releases/download/browser"
km_helper_url="$releases_url/latlon" km_helper_url="$releases_url/latlon"
set_im_module(){ set_im_module(){
#TODO: drop pending SteamOS changes #TODO: drop pending SteamOS changes
pgrep -a gamescope | grep -q "generate-drm-mode" if pgrep -a gamescope | grep -q "generate-drm-mode"; then
if [[ $? -eq 0 ]]; then unset GTK_IM_MODULE
GTK_IM_MODULE=""
logger INFO "Detected Steam Deck (Game Mode), unsetting GTK_IM_MODULE" logger INFO "Detected Steam Deck (Game Mode), unsetting GTK_IM_MODULE"
else
return
fi fi
} }
redact(){ redact(){
sed 's@\(/home/\)[^/]*@\1REDACTED@g' sed 's@\(/home/\)[^/]*@\1REDACTED@g'
} }
@ -87,13 +86,21 @@ logger(){
printf "%s␞%s␞%s::%s()::%s␞%s\n" "$date" "$tag" "$self" "$caller" "$line" "$string" \ printf "%s␞%s␞%s::%s()::%s␞%s\n" "$date" "$tag" "$self" "$caller" "$line" "$string" \
| redact >> "$debug_log" | redact >> "$debug_log"
} }
setup_dirs(){ setup_dirs(){
for dir in "$state_path" "$cache_path" "$share_path" "$helpers_path" "$freedesktop_path" "$config_path" "$log_path"; do directories=()
if [[ ! -d $dir ]]; then directories+=("$state_path")
directories+=("$cache_path")
directories+=("$share_path")
directories+=("$helpers_path")
directories+=("$freedesktop_path")
directories+=("$config_path")
directories+=("$log_path")
for dir in "${directories[@]}"; do
mkdir -p "$dir" mkdir -p "$dir"
fi
done done
} }
setup_state_files(){ setup_state_files(){
if [[ -f "$debug_log" ]]; then if [[ -f "$debug_log" ]]; then
rm "$debug_log" && touch $debug_log rm "$debug_log" && touch $debug_log
@ -104,13 +111,13 @@ setup_state_files(){
logger INFO "Migrating legacy version file" logger INFO "Migrating legacy version file"
fi fi
# wipe cache files # wipe cache files
local path="$cache_path" if [[ $(ls -A "$cache_path") ]]; then
if find "$path" -mindepth 1 -maxdepth 1 | read; then for file in "$cache_path"/*; do
for file in $path/*; do
rm "$file" rm "$file"
done done
logger INFO "Wiped cache files" logger INFO "Wiped cache files"
fi fi
echo "$src_path" > "$src_path_file"
} }
print_config_vals(){ print_config_vals(){
local keys=( local keys=(
@ -132,10 +139,10 @@ print_config_vals(){
} }
test_gobject(){ test_gobject(){
python3 -c "import gi" python3.13 -c "import gi"
if [[ ! $? -eq 0 ]]; then if [[ ! $? -eq 0 ]]; then
logger CRITICAL "Missing PyGObject" logger CRITICAL "Missing PyGObject"
fdialog "Requires PyGObject (python-gobject)" quit_with_help_dialog "Requires PyGObject (python-gobject)"
exit 1 exit 1
fi fi
logger INFO "Found PyGObject in Python env" logger INFO "Found PyGObject in Python env"
@ -214,17 +221,14 @@ default_steam_path="$default_steam_path"
#Preferred Steam launch command (for Flatpak support) #Preferred Steam launch command (for Flatpak support)
preferred_client="$preferred_client" preferred_client="$preferred_client"
#DZGUI source path
src_path="$src_path"
END END
} }
depcheck(){ depcheck(){
for dep in "${!deps[@]}"; do for dep in "${!deps[@]}"; do
command -v "$dep" 2>&1>/dev/null if ! command -v "$dep" &> /dev/null; then
if [[ $? -eq 1 ]]; then
local msg="Requires $dep >= ${deps[$dep]}" local msg="Requires $dep >= ${deps[$dep]}"
raise_error_and_quit "$msg" echo "$msg"
exit 1
fi fi
done done
local jqmsg="jq must be compiled with support for oniguruma" local jqmsg="jq must be compiled with support for oniguruma"
@ -233,14 +237,31 @@ depcheck(){
[[ $? -ne 0 ]] && raise_error_and_quit "$jqmsg" [[ $? -ne 0 ]] && raise_error_and_quit "$jqmsg"
logger INFO "Initial dependencies satisfied" logger INFO "Initial dependencies satisfied"
} }
check_pyver(){ open_url(){
local pyver=$(python3 --version | awk '{print $2}') url="$1"
local minor=$(<<< $pyver awk -F. '{print $2}') if [[ -n "$BROWSER" ]]; then
if [[ -z $pyver ]] || [[ ${pyver:0:1} -lt 3 ]] || [[ $minor -lt 10 ]]; then logger INFO "Opening '$url' in '$BROWSER'"
local msg="Requires Python >=3.10" "$BROWSER" "$url"
raise_error_and_quit "$msg" else
logger INFO "Opening '$url' with xdg-open"
xdg-open "$url"
fi
}
quit_with_help_dialog(){
local sel
msg="$1"
help_button="Open help page"
url="https://aclist.github.io/dzgui/installation.html"
logger CRITICAL "$msg"
sel=$(zenity --info --extra-button="$help_button" --text="$msg")
[[ $sel == "$help_button" ]] && open_url "$url" &
exit 1
}
check_pyver(){
if [[ ! $(python3.13 --version) ]]; then
local msg="Requires Python 3.13"
quit_with_help_dialog "$msg"
fi fi
logger INFO "Found Python version: $pyver"
} }
watcher_deps(){ watcher_deps(){
if [[ ! $(command -v wmctrl) ]] && [[ ! $(command -v xdotool) ]]; then if [[ ! $(command -v wmctrl) ]] && [[ ! $(command -v xdotool) ]]; then
@ -314,6 +335,8 @@ check_unmerged(){
fi fi
} }
check_version(){ check_version(){
echo "# Checking version"
[[ -n $reference_branch ]] && return
local version_url=$(format_version_url) local version_url=$(format_version_url)
local upstream=$(curl -Ls "$version_url" | awk -F= '/^version=/ {print $2}') local upstream=$(curl -Ls "$version_url" | awk -F= '/^version=/ {print $2}')
local res=$(get_response_code "$version_url") local res=$(get_response_code "$version_url")
@ -377,10 +400,11 @@ prompt_dl(){
dl_changelog(){ dl_changelog(){
local mdbranch local mdbranch
local md local md
source "$config_file"
[[ $branch == "stable" ]] && mdbranch="dzgui" [[ $branch == "stable" ]] && mdbranch="dzgui"
[[ $branch == "testing" ]] && mdbranch="testing" [[ $branch == "testing" ]] && mdbranch="testing"
local md="$url_prefix/${mdbranch}/$file" local changelog="$url_prefix/${mdbranch}/CHANGELOG.md"
curl -Ls "$md" > "$state_path/CHANGELOG.md" curl -Ls "$changelog" > "$state_path/CHANGELOG.md"
} }
test_display_mode(){ test_display_mode(){
pgrep -a gamescope | grep -q "generate-drm-mode" pgrep -a gamescope | grep -q "generate-drm-mode"
@ -391,6 +415,7 @@ test_display_mode(){
fi fi
} }
check_architecture(){ check_architecture(){
echo "# Checking system architecture"
local cpu=$(< /proc/cpuinfo awk -F": " '/AMD Custom APU [0-9]{4}$/ {print $2; exit}') local cpu=$(< /proc/cpuinfo awk -F": " '/AMD Custom APU [0-9]{4}$/ {print $2; exit}')
read -a APU_MODEL <<< "$cpu" read -a APU_MODEL <<< "$cpu"
if [[ ${APU_MODEL[3]} != "0932" ]] && [[ ${APU_MODEL[3]} != "0405" ]]; then if [[ ${APU_MODEL[3]} != "0932" ]] && [[ ${APU_MODEL[3]} != "0405" ]]; then
@ -407,6 +432,7 @@ check_architecture(){
logger INFO "Setting architecture to 'Steam Deck'" logger INFO "Setting architecture to 'Steam Deck'"
} }
check_map_count(){ check_map_count(){
echo "# Checking map count"
[[ $is_steam_deck -gt 0 ]] && return 0 [[ $is_steam_deck -gt 0 ]] && return 0
local map_count_file="/proc/sys/vm/max_map_count" local map_count_file="/proc/sys/vm/max_map_count"
local min_count=1048576 local min_count=1048576
@ -456,6 +482,7 @@ tdialog(){
$steamsafe_zenity --info --text="$1" "${zenity_flags[@]}" $steamsafe_zenity --info --text="$1" "${zenity_flags[@]}"
} }
steam_deps(){ steam_deps(){
echo "# Checking Steam"
local flatpak local flatpak
local steam local steam
[[ $(command -v flatpak) ]] && flatpak=$(flatpak list | grep valvesoftware.Steam) [[ $(command -v flatpak) ]] && flatpak=$(flatpak list | grep valvesoftware.Steam)
@ -464,56 +491,70 @@ steam_deps(){
local msg="Found neither Steam nor Flatpak Steam" local msg="Found neither Steam nor Flatpak Steam"
raise_error_and_quit "$msg" raise_error_and_quit "$msg"
exit 1 exit 1
elif [[ -n "$steam" ]] && [[ -n "$flatpak" ]]; then
[[ -n $preferred_client ]] && return 0
if [[ -z $preferred_client ]]; then
preferred_client="steam"
fi fi
elif [[ -n "$steam" ]]; then
preferred_client="steam"
else
preferred_client="flatpak"
fi
update_config
logger INFO "Preferred client set to '$preferred_client'"
}
migrate_files(){
if [[ ! -f $config_path/dztuirc.oldapi ]]; then
cp $config_file $config_path/dztuirc.oldapi
logger INFO "Migrated old API file"
fi
[[ ! -f $hist_file ]] && return
rm $hist_file
logger INFO "Wiped old history file"
} }
stale_symlinks(){ stale_symlinks(){
echo "# Cleaning stale symlinks"
local game_dir="$steam_path/steamapps/common/DayZ" local game_dir="$steam_path/steamapps/common/DayZ"
for l in "$(find "$game_dir" -xtype l)"; do readarray -t links < <(find "$game_dir" -xtype l)
logger DEBUG "Updating stale symlink '$l'" for link in "${links[@]}"; do
unlink "$l" logger DEBUG "Updating stale symlink '$link'"
unlink "$link"
done done
} }
check_availability() {
if [[ -z $1 ]]; then
return 1
fi
local url=$1
local timeout_sec="2"
if [[ $2 ]]; then
timeout_sec=$2
fi
if ! ping -w "$timeout_sec" "$url" > /dev/null 2>&1; then
logger WARN "Failed to reach $url, service may be down."
return 1
fi
}
local_latlon(){ local_latlon(){
echo "# Checking coordinates"
if [[ -z $(command -v dig) ]]; then if [[ -z $(command -v dig) ]]; then
local local_ip=$(curl -Ls "https://ipecho.net/plain") local url_ipecho="https://ipecho.net/plain"
if ! check_availability "ipecho.net"; then
logger WARN "Failed to get external ip address, ipecho.net service may be down."
return 1
fi
local local_ip=$(curl -Ls "$url_ipecho")
else else
# TODO : implement checking remote
local local_ip=$(dig -4 +short myip.opendns.com @resolver1.opendns.com) local local_ip=$(dig -4 +short myip.opendns.com @resolver1.opendns.com)
fi fi
local url="http://ip-api.com/json/$local_ip" if calc_local_coords "$local_ip"; then
local res=$(curl -Ls "$url" | jq -r '"\(.lat)\n\(.lon)"') return 0
fi
local url_ip_api="http://ip-api.com/json/$local_ip"
if ! check_availability "ip-api.com"; then
logger WARN "Failed to get local coordinates, ip-api.com service may be down."
return 1
fi
local res=$(curl -Ls "$url_ip_api" | jq -r '"\(.lat)\n\(.lon)"')
if [[ -z "$res" ]]; then if [[ -z "$res" ]]; then
logger WARN "Failed to get local coordinates" logger WARN "Failed to get local coordinates"
return 1 return 1
fi fi
echo "$res" > "$coords_file" echo "$res" > "$coords_file"
} }
lock(){ lock(){
echo "# Setting up lock file"
[[ ! -f $lock_file ]] && touch $lock_file [[ ! -f $lock_file ]] && touch $lock_file
local pid=$(cat $lock_file) local pid=$(cat $lock_file)
ps -p $pid -o pid= >/dev/null 2>&1 ps -p $pid -o pid= >/dev/null 2>&1
res=$? res=$?
if [[ $res -eq 0 ]]; then if [[ $res -eq 0 ]]; then
local msg="DZGUI already running ($pid)" local msg="DZGUI is already running ($pid)"
raise_error_and_quit "$msg" raise_error_and_quit "$msg"
elif [[ $pid == $$ ]]; then elif [[ $pid == $$ ]]; then
: :
@ -542,13 +583,13 @@ fetch_a2s(){
logger INFO "Updated A2S helper to sha '$sha'" logger INFO "Updated A2S helper to sha '$sha'"
} }
fetch_dzq(){ fetch_dzq(){
local sum="9caed1445c45832f4af87736ba3f9637" local sum="0a334e1e144e76e560419d155435c91e"
local file="$helpers_path/a2s/dayzquery.py" local file="$helpers_path/a2s/dayzquery.py"
if [[ -f $file ]] && [[ $(get_hash "$file") == $sum ]]; then if [[ -f $file ]] && [[ $(get_hash "$file") == $sum ]]; then
logger INFO "DZQ is current" logger INFO "DZQ is current"
return 0 return 0
fi fi
local sha=3088bbfb147b77bc7b6a9425581b439889ff3f7f local sha=a22a9f428cbe075d7dda62f78000296955eea92a
local author="yepoleb" local author="yepoleb"
local repo="dayzquery" local repo="dayzquery"
local url="https://raw.githubusercontent.com/$author/$repo/$sha/dayzquery.py" local url="https://raw.githubusercontent.com/$author/$repo/$sha/dayzquery.py"
@ -585,11 +626,12 @@ fetch_helpers_by_sum(){
[[ -f "$config_file" ]] && source "$config_file" [[ -f "$config_file" ]] && source "$config_file"
declare -A sums declare -A sums
sums=( sums=(
["ui.py"]="f128a97e744e9e11036d707198feb8a8" ["funcs"]="5523d933e9f0668fc14b3faf0d4aafd8"
["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067"
["servers.py"]="ed442c3aecf33f777d59dcf53650d263"
["ui.py"]="d09b9d8bed3854efd51377289786c5ac"
["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397"
["funcs"]="94287c75a5ae0a06a95dd439711e6fba" ["pefile.py"]="b452974a84bff1d821872fcebf59e380"
["lan"]="c62e84ddd1457b71a85ad21da662b9af"
) )
local author="aclist" local author="aclist"
local repo="dztui" local repo="dztui"
@ -608,6 +650,10 @@ fetch_helpers_by_sum(){
realbranch="dzgui" realbranch="dzgui"
fi fi
if [[ -n $reference_branch ]]; then
realbranch="$reference_branch"
fi
for i in "${!sums[@]}"; do for i in "${!sums[@]}"; do
file="$i" file="$i"
sum="${sums[$i]}" sum="${sums[$i]}"
@ -631,7 +677,6 @@ fetch_helpers_by_sum(){
logger INFO "Updated '$full_path' to sum '$sum'" logger INFO "Updated '$full_path' to sum '$sum'"
fi fi
[[ $file == "funcs" ]] && chmod +x "$full_path" [[ $file == "funcs" ]] && chmod +x "$full_path"
[[ $file == "lan" ]] && chmod +x "$full_path"
done done
return 0 return 0
} }
@ -646,15 +691,12 @@ fetch_km_helper(){
} }
get_response_code(){ get_response_code(){
local url="$1" local url="$1"
curl -Ls -I -o /dev/null -w "%{http_code}" "$url" curl --connect-timeout 3 -Ls -I -o /dev/null -w "%{http_code}" "$url"
}
raise_error_and_quit(){
echo "$1"
exit 1
} }
fetch_ip_db(){ fetch_ip_db(){
parse_dl_url(){ parse_dl_url(){
curl -Ls "$url" \ curl --connect-timeout 3 -Ls "$url" \
| grep "csv.gz" \ | grep "csv.gz" \
| awk -F"['']" '{print $2}' | awk -F"['']" '{print $2}'
} }
@ -670,6 +712,9 @@ fetch_ip_db(){
fetch(){ fetch(){
logger INFO "Triggering fetch routine" logger INFO "Triggering fetch routine"
local url="$1" local url="$1"
local ip_file="${state_path}/ips.csv"
local base_file="${state_path}/${this_month}.csv.gz"
local extracted_file="${state_path}/${this_month}.csv"
curl -Ls "$url" > "$base_file" curl -Ls "$url" > "$base_file"
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
@ -689,7 +734,7 @@ fetch_ip_db(){
logger WARN "Abnormal exit while parsing IPs" logger WARN "Abnormal exit while parsing IPs"
return return
fi fi
rm "${state_path}/${month}.csv" rm "${state_path}/${this_month}.csv"
readarray -t records < <(cat $ip_file | awk -F, 'NR==1 {print $1} END {print $1}') readarray -t records < <(cat $ip_file | awk -F, 'NR==1 {print $1} END {print $1}')
if [[ ${records[0]} != "0.0.0.0" ]] && [[ ${records[1]} != "224.0.0.0" ]]; then if [[ ${records[0]} != "0.0.0.0" ]] && [[ ${records[1]} != "224.0.0.0" ]]; then
@ -704,16 +749,13 @@ fetch_ip_db(){
rm "$ip_file" rm "$ip_file"
fi fi
echo "$month" > "$month_file" echo "$this_month" > "$month_file"
logger INFO "Wrote '$month' to stub '$month_file'" logger INFO "Wrote '$this_month' to stub '$month_file'"
logger INFO "Updated '$ip_file'" logger INFO "Updated '$ip_file'"
} }
local url="https://db-ip.com/db/download/ip-to-city-lite" check_remote(){
local month_file="${state_path}/.month" local url="$1"
local ip_file="${state_path}/ips.csv"
# test main url
local res=$(get_response_code "$url") local res=$(get_response_code "$url")
if [[ $res -ne 200 ]]; then if [[ $res -ne 200 ]]; then
logger WARN "Failed to retrieve remote resource: '$url' ($res)" logger WARN "Failed to retrieve remote resource: '$url' ($res)"
@ -729,30 +771,35 @@ fetch_ip_db(){
return return
fi fi
logger INFO "Resolved download URL: '$dl_url'" logger INFO "Resolved download URL: '$dl_url'"
fetch "$dl_url"
}
local month=$(parse_dl_url_date "$dl_url") local url="https://db-ip.com/db/download/ip-to-city-lite"
local base_file="${state_path}/${month}.csv.gz" month_file="${state_path}/.month"
local extracted_file="${state_path}/${month}.csv" this_month=$(date +%Y-%m)
# no stub file # no stub file
if [[ ! -f $month_file ]]; then if [[ ! -f $month_file ]]; then
logger WARN "No stub file '$month_file' present" logger WARN "No stub file '$month_file' present"
fetch "$dl_url" check_remote "$url"
return
fi
# local needs update
local last_month=$(< "$month_file")
if [[ $last_month != "$month" ]]; then
logger WARN "Local stub '$last_month' does not match remote stub '$month'"
fetch "$dl_url"
return return
fi fi
# if stub is same date, abort # if stub is same date, abort
local last_month=$(< "$month_file")
if [[ $last_month == "$this_month" ]]; then
logger INFO "Local stub '$last_month' is identical to remote, skipping" logger INFO "Local stub '$last_month' is identical to remote, skipping"
return
fi
# local needs update
logger WARN "Local stub '$last_month' does not match '$this_month'"
check_remote "$url"
return
} }
fetch_helpers(){ fetch_helpers(){
echo "# Checking helper files"
fetch_a2s fetch_a2s
fetch_dzq fetch_dzq
fetch_km_helper fetch_km_helper
@ -851,7 +898,7 @@ file_picker(){
} }
find_library_folder(){ find_library_folder(){
local search_path="$1" local search_path="$1"
readarray -t paths < <(python3 "$helpers_path/vdf2json.py" -i "$1/steamapps/libraryfolders.vdf" \ readarray -t paths < <(python3.13 "$helpers_path/vdf2json.py" -i "$1/steamapps/libraryfolders.vdf" \
| jq -r '.libraryfolders[]|select(.apps|has("221100")).path') | jq -r '.libraryfolders[]|select(.apps|has("221100")).path')
if [[ ! $? -eq 0 ]]; then if [[ ! $? -eq 0 ]]; then
logger WARN "Failed to parse Steam path using '$search_path'" logger WARN "Failed to parse Steam path using '$search_path'"
@ -879,6 +926,7 @@ create_config(){
unset default_steam_path unset default_steam_path
unset steam_path unset steam_path
preferred_client="steam"
while true; do while true; do
local player_input="$($steamsafe_zenity \ local player_input="$($steamsafe_zenity \
--forms \ --forms \
@ -939,8 +987,9 @@ create_config(){
done done
} }
varcheck(){ varcheck(){
echo "# Checking config file"
local msg="Config file '$config_file' missing. Start first-time setup now?" local msg="Config file '$config_file' missing. Start first-time setup now?"
local msg2="The Steam paths set in your config file appear to be invalid. Restart first-time setup now?" local msg2="The Steam paths set in your config file appear to be invalid (DayZ was moved or uninstalled). Restart first-time setup now?"
if [[ ! -f $config_file ]]; then if [[ ! -f $config_file ]]; then
qdialog "$msg" "Yes" "Exit" qdialog "$msg" "Yes" "Exit"
if [[ $? -eq 1 ]]; then if [[ $? -eq 1 ]]; then
@ -963,18 +1012,16 @@ varcheck(){
create_config create_config
return 0 return 0
fi fi
if [[ $src_path != $(realpath "$0") ]]; then
src_path=$(realpath "$0")
update_config
fi
} }
is_dzg_downloading(){ is_dzg_downloading(){
echo "# Checking DayZ"
if [[ -d $steam_path ]] && [[ -d $steam_path/downloading/$aid ]]; then if [[ -d $steam_path ]] && [[ -d $steam_path/downloading/$aid ]]; then
logger WARN "DayZ may be scheduling updates" logger WARN "DayZ may be scheduling updates"
return 0 return 0
fi fi
} }
is_steam_running(){ is_steam_running(){
echo "# Checking Steam"
local res=$(ps aux | grep "steamwebhelper" | grep -v grep) local res=$(ps aux | grep "steamwebhelper" | grep -v grep)
if [[ -z $res ]]; then if [[ -z $res ]]; then
logger WARN "Steam may not be running" logger WARN "Steam may not be running"
@ -987,6 +1034,7 @@ get_response_code(){
curl -Ls -I -o /dev/null -w "%{http_code}" "$url" curl -Ls -I -o /dev/null -w "%{http_code}" "$url"
} }
test_connection(){ test_connection(){
echo "# Testing connection"
declare -A hr declare -A hr
local res1 local res1
local res2 local res2
@ -1020,6 +1068,8 @@ legacy_cols(){
mv $cols_file.new $cols_file mv $cols_file.new $cols_file
} }
stale_mod_signatures(){ stale_mod_signatures(){
echo "# Checking stale mods"
[[ ! -f "$versions_file" ]] && return
local workshop_dir="$steam_path/steamapps/workshop/content/$aid" local workshop_dir="$steam_path/steamapps/workshop/content/$aid"
if [[ -d $workshop_dir ]]; then if [[ -d $workshop_dir ]]; then
readarray -t old_mod_ids < <(awk -F, '{print $1}' $versions_file) readarray -t old_mod_ids < <(awk -F, '{print $1}' $versions_file)
@ -1032,18 +1082,48 @@ stale_mod_signatures(){
} }
create_new_links(){ create_new_links(){
echo "# Setting up symlinks"
"$func_helper" "update_symlinks" "$func_helper" "update_symlinks"
} }
calc_local_coords(){
local ip="$1"
[[ ! -f "$geo_helper" ]] && return 1
IFS="." read -ra split <<< "$ip"
[[ ${#split[@]} -ne 4 ]] && return 1
prefix="^${split[0]}.${split[1]}."
readarray -t res < <(grep "$prefix" "$geo_helper")
for address in "${res[@]}"; do
IFS="," read -ra fields <<< "$address"
upper="${fields[1]}"
IFS="." read -ra upper_digits <<< "$upper"
if [[ ${split[2]} -gt ${upper_digits[2]} ]]; then
continue
fi
if [[ ${split[2]} -eq ${upper_digits[2]} ]]; then
if [[ ${split[3]} -gt ${upper_digits[3]} ]]; then
continue
fi
if [[ ${split[3]} -eq ${upper_digits[3]} ]]; then
echo "${fields[-2]}" >> "$coords_file"
echo "${fields[-1]}" >> "$coords_file"
return 0
fi
fi
if [[ ${split[2]} -lt ${upper_digits[2]} ]]; then
echo "${fields[-2]}" >> "$coords_file"
echo "${fields[-1]}" >> "$coords_file"
return 0
fi
done
return 1
}
initial_setup(){ initial_setup(){
setup_dirs
setup_state_files
depcheck
check_pyver
test_gobject
watcher_deps
check_architecture check_architecture
test_connection test_connection
fetch_helpers > >(pdialog "Checking helper files") fetch_helpers
varcheck varcheck
source "$config_file" source "$config_file"
lock lock
@ -1052,14 +1132,12 @@ initial_setup(){
check_version check_version
check_map_count check_map_count
steam_deps steam_deps
migrate_files
stale_symlinks stale_symlinks
stale_mod_signatures stale_mod_signatures
create_new_links create_new_links
local_latlon local_latlon
is_steam_running is_steam_running
is_dzg_downloading is_dzg_downloading
print_config_vals
} }
uninstall(){ uninstall(){
_full(){ _full(){
@ -1103,22 +1181,82 @@ uninstall(){
rm "$self" rm "$self"
echo "Uninstall routine complete" echo "Uninstall routine complete"
} }
usage(){
cat <<- EOM
DZGUI - Free and Open Source DayZ launcher
Usage:
dzgui.sh [options]
Description:
When no option is provided, the script launches DZGUI.
Options:
-u, --uninstall:
Uninstalls the software
-v, --version:
Prints the version
-h, --help:
Prints this message
EOM
}
main(){ main(){
local zenv=$(zenity --version 2>/dev/null) # setup zenity environment
local zenv=""
zenv=$(zenity --version 2>/dev/null)
[[ -z $zenv ]] && { echo "Requires zenity >= ${deps[$steamsafe_zenity]}"; exit 1; } [[ -z $zenv ]] && { echo "Requires zenity >= ${deps[$steamsafe_zenity]}"; exit 1; }
if [[ $1 == "--uninstall" ]] || [[ $1 == "-u" ]]; then
uninstall && # parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
"--uninstall" | "-u")
uninstall
exit 0 exit 0
fi # shift
;;
"--version" | "-v")
echo $version
exit 0
# shift
;;
"--help" | "-h")
usage
exit 0
# shift
;;
*)
echo "Unrecognized command!"
return 1
esac
done
set_im_module set_im_module
printf "Initializing setup...\n" printf "Checking dependencies...\n"
initial_setup setup_dirs
setup_state_files
depcheck
check_pyver
test_gobject
watcher_deps
initial_setup > >(pdialog "Initializing setup")
printf "All OK. Kicking off UI...\n" printf "All OK. Kicking off UI...\n"
python3 "$ui_helper" "--init-ui" "$version" "$is_steam_deck" python3.13 "$ui_helper" "--init-ui" "$version" "$is_steam_deck"
} }
if [[ $(basename "$0") == "dzgui.sh" ]]; then
main "$@" main "$@"
#TODO: tech debt: cruddy handling for steam forking #TODO: tech debt: cruddy handling for steam forking
[[ $? -eq 1 ]] && pkill -f dzgui.sh [[ $? -eq 1 ]] && pkill -f dzgui.sh
fi

View File

@ -1,9 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -o pipefail set -o pipefail
version="5.8.0" version="6.0.2"
#CONSTANTS #CONSTANTS
aid=221100 aid=221100
exp=1024020
game="dayz" game="dayz"
app_name="dzgui" app_name="dzgui"
app_name_upper="DZGUI" app_name_upper="DZGUI"
@ -46,9 +47,11 @@ _cache_my_servers="$cache_dir/$prefix.my_servers"
_cache_history="$cache_dir/$prefix.history" _cache_history="$cache_dir/$prefix.history"
_cache_launch="$cache_dir/$prefix.launch_mods" _cache_launch="$cache_dir/$prefix.launch_mods"
_cache_address="$cache_dir/$prefix.launch_address" _cache_address="$cache_dir/$prefix.launch_address"
_cache_binary="$cache_dir/$prefix.binary"
_cache_coords="$cache_path/$prefix.coords" _cache_coords="$cache_path/$prefix.coords"
_cache_cooldown="$cache_path/$prefix.cooldown" _cache_cooldown="$cache_path/$prefix.cooldown"
_cache_lan="$cache_path/$prefix.lan" _cache_lan="$cache_path/$prefix.lan"
_cache_src_path="$cache_path/$prefix.src"
#XDG #XDG
freedesktop_path="$HOME/.local/share/applications" freedesktop_path="$HOME/.local/share/applications"
@ -82,38 +85,28 @@ forum_url="https://old.reddit.com/r/dzgui"
sponsor_url="$gh_prefix/sponsors/$author" sponsor_url="$gh_prefix/sponsors/$author"
battlemetrics_server_url="https://www.battlemetrics.com/servers/dayz" battlemetrics_server_url="https://www.battlemetrics.com/servers/dayz"
steam_api_url="https://steamcommunity.com/dev/apikey" steam_api_url="https://steamcommunity.com/dev/apikey"
#TODO: update link in docs
battlemetrics_api_url="https://www.battlemetrics.com/developers" battlemetrics_api_url="https://www.battlemetrics.com/developers"
bm_api="https://api.battlemetrics.com/servers" bm_api="https://api.battlemetrics.com/servers"
if [[ $preferred_client == "steam" ]]; then steam_cmd="$preferred_client"
steam_cmd="steam"
else
steam_cmd="flatpak run com.valvesoftware.Steam"
fi
declare -A funcs=( declare -A funcs=(
["Highlight stale"]="find_stale_mods" ["Highlight stale"]="find_stale_mods"
["My servers"]="dump_servers"
["Change player name"]="update_config_val" ["Change player name"]="update_config_val"
["Change Steam API key"]="update_config_val" ["Change Steam API key"]="update_config_val"
["Change Battlemetrics API key"]="update_config_val" ["Change Battlemetrics API key"]="update_config_val"
["Change client"]="update_config_val"
["Change favorite server"]="add_record" ["Change favorite server"]="add_record"
["Quick-connect to favorite server"]="quick_connect"
["Add server by IP"]="add_record" ["Add server by IP"]="add_record"
["Add server by ID"]="add_record" ["Add server by ID"]="add_record"
["Connect by IP"]="validate_and_connect" ["try_connect"]="try_connect"
["Connect by ID"]="validate_and_connect"
["Connect from table"]="connect_from_table"
["find_id"]="find_id" ["find_id"]="find_id"
["toggle"]="toggle" ["toggle"]="toggle"
["Open link"]="open_link" ["Open link"]="open_link"
["filter"]="dump_servers"
["dump_servers"]="dump_servers"
["get_unique_maps"]="get_unique_maps"
["get_dist"]="get_dist" ["get_dist"]="get_dist"
["test_cooldown"]="test_cooldown" ["test_cooldown"]="test_cooldown"
["query_config"]="query_config" ["query_config"]="query_config"
["query_favorites"]="query_favorites"
["start_cooldown"]="start_cooldown" ["start_cooldown"]="start_cooldown"
["List installed mods"]="list_mods" ["List installed mods"]="list_mods"
["Delete selected mods"]="delete_local_mod" ["Delete selected mods"]="delete_local_mod"
@ -130,21 +123,31 @@ declare -A funcs=(
["Remove from history"]="remove_from_history" ["Remove from history"]="remove_from_history"
["Force update local mods"]="force_update" ["Force update local mods"]="force_update"
["Resolve IP"]="resolve_ip" ["Resolve IP"]="resolve_ip"
["Handshake"]="final_handshake" ["Handshake"]="handshake"
["Handshake_EXP"]="handshake_exp"
["get_player_count"]="get_player_count" ["get_player_count"]="get_player_count"
["lan_scan"]="lan_scan"
["update_symlinks"]="update_symlinks" ["update_symlinks"]="update_symlinks"
) )
lan_scan(){ clone_symlinks(){
local port="$1" local path="$(< $_cache_binary)"
local res path=$(dirname "$path")
res=$("$lan_helper" "$port") for dir in $(find $game_dir -type l); do
if [[ -z $res ]]; then local link=$(basename $dir)
printf "\n" ln -sf "${dir}" "${path}/${link}"
else done
printf "%s\n" "$res" }
handshake(){
final_handshake "$aid"
}
handshake_exp(){
final_handshake "$exp"
}
query_favorites(){
if [[ -z "${ip_list[@]}" ]]; then
return 1
fi fi
printf "%s\n" "${ip_list[@]}"
} }
find_stale_mods(){ find_stale_mods(){
local res local res
@ -200,44 +203,6 @@ get_player_count(){
printf "%s\n%s" "$players" "$queue" printf "%s\n%s" "$players" "$queue"
} }
validate_and_connect(){
local context="$1"
local addr="$2"
local record
case "$context" in
"Connect by ID")
if [[ -z "$api_key" ]]; then
printf "No Battlemetrics API key set"
return 4
fi
record=$(map_id_to_ip "$addr")
if [[ $? -eq 1 ]]; then
logger WARN "Not a valid record: '$addr'"
printf "Not a valid ID"
return 2
fi
logger INFO "Battlemetrics ID resolved to IP $record"
;;
"Connect by IP")
if [[ $(validate_ip "$addr") -eq 1 ]]; then
printf "Not a valid IP format. Supply IP:Queryport"
return 2
fi
local ip=$(<<< $addr awk -F: '{print $1}')
local qport=$(<<< $addr awk -F: '{print $2}')
local res
res=$(a2s $ip $qport info)
if [[ ! $? -eq 0 ]]; then
printf "Timed out when querying the server. Is this a valid server?"
return 2
fi
local gameport="$(<<< $res jq -r '.[].gameport')"
record="${ip}:${gameport}:${qport}"
logger INFO "Record resolved to $record"
esac
try_connect "$record"
}
map_id_to_ip(){ map_id_to_ip(){
local id="$1" local id="$1"
local res=$(curl -s "$bm_api" -H "Authorization: Bearer "$api_key"" \ local res=$(curl -s "$bm_api" -H "Authorization: Bearer "$api_key"" \
@ -303,32 +268,10 @@ add_record(){
;; ;;
esac esac
} }
connect_by_id(){
if [[ $(validate_ip "$addr") -eq 1 ]]; then
printf "Not a valid IP format. Supply IP:Queryport"
return 2
fi
local ip=$(<<< $addr awk -F: '{print $1}')
local qport=$(<<< $addr awk -F: '{print $2}')
local res
res=$(a2s $ip $qport info)
if [[ ! $? -eq 0 ]]; then
printf "Timed out when querying the server. Is this a valid server?"
return 2
fi
#res contains modlist
}
start_cooldown(){ start_cooldown(){
logger WARN "API response empty. Started 60s cooldown at $(date +%s)" logger WARN "API response empty. Started 60s cooldown at $(date +%s)"
date +%s > $_cache_cooldown date +%s > $_cache_cooldown
} }
initialize_remote_servers(){
local file="$_cache_servers"
[[ -f $file ]] && rm "$file"
local res
res=$(get_remote_servers)
parse_server_json "$res" >> "$file"
}
is_dlc(){ is_dlc(){
local dlc local dlc
local ip="$1" local ip="$1"
@ -343,7 +286,7 @@ a2s(){
local mode="$3" local mode="$3"
logger INFO "Querying '$ip:$qport' with mode '$mode'" logger INFO "Querying '$ip:$qport' with mode '$mode'"
local res local res
res=$(python3 "$query_helper" "$ip" "$qport" "$mode") res=$(python3.13 "$query_helper" "$ip" "$qport" "$mode")
if [[ $? -eq 1 ]]; then if [[ $? -eq 1 ]]; then
res=$(try_fallback "$ip" "$qport" "$mode") res=$(try_fallback "$ip" "$qport" "$mode")
if [[ $? -eq 1 ]]; then if [[ $? -eq 1 ]]; then
@ -366,7 +309,7 @@ is_in_favs(){
find_id(){ find_id(){
local file="$default_steam_path/config/loginusers.vdf" local file="$default_steam_path/config/loginusers.vdf"
[[ ! -f $file ]] && return 1 [[ ! -f $file ]] && return 1
local res=$(python3 "$helpers_path/vdf2json.py" \ local res=$(python3.13 "$helpers_path/vdf2json.py" \
-i "$file" \ -i "$file" \
| jq -r '.users | jq -r '.users
|to_entries[] |to_entries[]
@ -395,7 +338,7 @@ list_mods(){
name=$(awk -F\" '/name/ {print $2}' "${dir}/meta.cpp") name=$(awk -F\" '/name/ {print $2}' "${dir}/meta.cpp")
base_dir=$(basename $(readlink -f $game_dir/$symlink)) base_dir=$(basename $(readlink -f $game_dir/$symlink))
size=$(du -s "$(readlink -f "$game_dir/$symlink")" | awk '{print $1}') size=$(du -s "$(readlink -f "$game_dir/$symlink")" | awk '{print $1}')
size=$(python3 -c "n=($size/1024) +.005; print(round(n,4))") size=$(python3.13 -c "n=($size/1024) +.005; print(round(n,4))")
LC_NUMERIC=C printf "%s$sep%s$sep%s$sep%3.3f\n" "$name" "$symlink" "$base_dir" "$size" LC_NUMERIC=C printf "%s$sep%s$sep%s$sep%3.3f\n" "$name" "$symlink" "$base_dir" "$size"
done | sort -k1) done | sort -k1)
# user may have manually pruned mods out-of-band # user may have manually pruned mods out-of-band
@ -424,6 +367,10 @@ local_latlon(){
get_dist(){ get_dist(){
shift shift
local given_ip="$1" local given_ip="$1"
if [[ ! -f $_cache_coords ]]; then
printf "Unknown"
return
fi
readarray -t coords < "$_cache_coords" readarray -t coords < "$_cache_coords"
readarray -t n < <(<<< "$given_ip" awk 'BEGIN{RS="."}{$1=$1}1') readarray -t n < <(<<< "$given_ip" awk 'BEGIN{RS="."}{$1=$1}1')
@ -446,60 +393,11 @@ get_dist(){
else else
logger INFO "Resolved remote server geolocation to '$remote_lat, $remote_lon'" logger INFO "Resolved remote server geolocation to '$remote_lat, $remote_lon'"
local dist=$($km_helper "$local_lat" "$local_lon" "$remote_lat" "$remote_lon") local dist=$($km_helper "$local_lat" "$local_lon" "$remote_lat" "$remote_lon")
LC_NUMERIC=C printf "%d" "$dist" LC_NUMERIC=C printf "%.0f" "$dist"
logger INFO "Distance: $dist km" logger INFO "Distance: $dist km"
fi fi
} }
get_remote_servers(){
params=(
"\\nor\1\map\chernarusplus\\nor\1\map\sakhal\\nor\1\map\enoch\empty\1\\nor\1\map\namalsk"
"\map\namalsk\empty\1"
"\map\namalsk\noplayers\1"
"\map\chernarusplus\empty\1"
"\map\chernarusplus\noplayers\1"
"\map\\sakhal\empty\1"
"\map\\sakhal\noplayers\1"
"\map\\enoch\empty\1"
"\map\\enoch\noplayers\1"
)
local limit=10000
local url="https://api.steampowered.com/IGameServersService/GetServerList/v1/?"
_fetch(){
local param="$1"
curl -LsG "$url" \
-d filter="\appid\221100${param}" \
-d limit=$limit \
-d key=$steam_api \
| jq -M -r '.response.servers'
}
for ((i=0; i <${#params[@]}; i++ )); do
_fetch "${params[$i]}" > $_cache_temp.${i}
done
# ubuntu variants do not support jq 1.7 chained operators
# https://github.com/jqlang/jq/releases/tag/jq-1.7
jq -n '[ [inputs]|add ][]' $_cache_temp.* && rm $_cache_temp.*
}
get_unique_maps(){
shift
local context="$1"
local filter_file
case "$context" in
"My saved servers")
filter_file="$_cache_my_servers"
;;
"Server browser")
filter_file="$_cache_servers"
;;
"Recent servers")
filter_file="$_cache_history"
esac
logger INFO "Map filter context is: '$context', using cached file at '$filter_file'"
< "$filter_file" awk -F$separator '{print $2}' | sort -u
}
query_config(){ query_config(){
[[ -n $2 ]] && local key=$2 [[ -n $2 ]] && local key=$2
keys=( keys=(
@ -510,6 +408,7 @@ query_config(){
"fav_label" "fav_label"
"preferred_client" "preferred_client"
"fullscreen" "fullscreen"
"default_steam_path"
) )
if [[ -n $key ]]; then if [[ -n $key ]]; then
if [[ -n ${!key} ]]; then if [[ -n ${!key} ]]; then
@ -524,154 +423,7 @@ query_config(){
echo "${!i}" echo "${!i}"
done done
} }
filter_servers(){
local filtered="$(< "$1")"
shift
readarray -t filters < <(printf "%s\n" "$@")
for ((i=0; i< ${#filters[@]}; ++i)); do
if [[ ${filters[$i]} =~ Keyword ]]; then
keyword=$(<<< ${filters[$i]} awk -F␞ '{print $2}')
elif [[ ${filters[$i]} =~ Map ]]; then
map=$(<<< ${filters[$i]} awk -F= '{print $2}')
fi
done
filter_ascii(){
if [[ ${filters[*]} =~ Non ]]; then
echo -n "$filtered"
else
<<< "$filtered" sed 's/␞/@@DZGUI_PLACEHOLDER@@/g' | grep -v -P '[^[:ascii:]]' | sed 's/@@DZGUI_PLACEHOLDER@@/␞/g'
fi
}
filter_time(){
if [[ ${filters[*]} =~ Day ]] && [[ ${filters[*]} =~ Night ]]; then
echo -n "$filtered"
elif [[ ${filters[*]} =~ Day ]]; then
<<< "$filtered" awk -F$separator '$4~/^([0][6-9]:|[1][0-6])/'
elif [[ ${filters[*]} =~ Night ]]; then
<<< "$filtered" awk -F$separator '$4~/^([1][7-9]:|[2][0-3]:|[0][0-5])/'
else
echo -n ""
fi
}
filter_perspective(){
if [[ ${filters[*]} =~ 1PP ]] && [[ ${filters[*]} =~ 3PP ]]; then
echo -n "$filtered"
elif [[ ${filters[*]} =~ 1PP ]]; then
<<< "$filtered" awk '!/3PP/'
elif [[ ${filters[*]} =~ 3PP ]]; then
<<< "$filtered" awk '!/1PP/'
else
echo -n ""
fi
}
filter_lowpop(){
if [[ ${filters[*]} =~ Low ]]; then
echo -n "$filtered"
else
<<< "$filtered" awk -F$separator '{if (($5 > 0) && ($5/$6)*100 >=30){print $0}}'
fi
}
filter_full(){
if [[ ${filters[*]} =~ Full ]]; then
echo -n "$filtered"
else
<<< "$filtered" awk -F$separator '$5 != $6'
fi
}
filter_empty(){
if [[ ${filters[*]} =~ Empty ]]; then
echo -n "$filtered"
else
<<< "$filtered" awk -F$separator '$5 != "0"'
fi
}
filter_map(){
if [[ $map == "All maps" ]]; then
echo "$filtered"
else
<<< "$filtered" awk -v var="$map" -F$separator '$2 == var'
fi
}
filter_keyword(){
keyword=$(sanitize "$keyword")
<<< "$filtered" awk -F$separator -v keyword="$keyword" 'tolower($0) ~ tolower(keyword)'
}
filter_duplicates(){
if [[ ${filters[*]} =~ Duplicate ]]; then
echo -n "$filtered"
else
<<< "$filtered" awk -F$separator '!seen[$1]++'
fi
}
filter_official(){
if [[ ${filters[*]} =~ Official ]]; then
echo -n "$filtered"
else
<<< "$filtered" awk -F$separator '$10 == "Community"'
fi
}
filter_community(){
if [[ ${filters[*]} =~ Unoffic ]]; then
echo -n "$filtered"
else
<<< "$filtered" awk -F$separator '$10 == "Official"'
fi
}
filtered=$(filter_perspective)
filtered=$(filter_full)
filtered=$(filter_empty)
filtered=$(filter_time)
filtered=$(filter_map)
filtered=$(filter_lowpop)
filtered=$(filter_ascii)
filtered=$(filter_duplicates)
filtered=$(filter_keyword)
filtered=$(filter_official)
filtered=$(filter_community)
if [[ -z "$filtered" ]]; then
logger WARN "Filter result is empty"
echo -n ""
return
fi
logger INFO "Returning sorted server list back to UI"
printf "%s\n" "$filtered" | sort -k1
}
sanitize(){
echo "$1" | sed \
-e 's/\//\\\//g' \
-e 's/\$/\\$/g' \
-e 's/\[/\\[/g' \
-e 's/\]/\\]/g' \
-e 's/\#/\\#/g' \
-e 's/\./\\./g' \
-e 's/\^/\\^/g' \
-e 's/\=/\\=/g' \
-e 's/|/\\|/g' \
-e 's/\+/\\+/g' \
-e 's/(/\\(/g' \
-e 's/)/\\)/g'
}
parse_server_json(){
local response="$1"
# some servers pad SOH in name
<<< "$response" sed 's/\\u0001//g' | jq -r '
.[]|"\(.name)␞" +
"\(.map|if type == "string" then ascii_downcase else "null" end)␞" +
"\(if .gametype == null then "null" else (.gametype|split(",")|if any(. == "no3rd") then "1PP" else "3PP" end) end)␞" +
"\(if .gametype == null then "null" else (.gametype as $time|$time|test("[0-9]{2}:[0-9]{2}$") as $match|(if $match == true then ($time|scan("[0-9]{2}:[0-9]{2}$")) else "XXXX" end)) end)␞" +
"\(.players)␞" +
"\(.max_players)␞" +
"\(if .gametype == null then "0" elif .gametype|split("lqs")[1] == null then "0" else .gametype|split("lqs")[1]|split(",")[0] end)␞" +
"\(.addr|split(":")[0]):\(if .gameport == null then "XXXX" else .gameport end)␞" +
"\(.addr|split(":")[1])␞" +
"\(if .gametype == null then "null" else (.gametype|split(",")|if any(. == "external") then "Community" else "Official" end) end)"
' | sort -k1
}
align_versions_file(){ align_versions_file(){
shift shift
local mod="$1" local mod="$1"
@ -723,69 +475,13 @@ test_cooldown(){
local old_time=$(< $_cache_cooldown) local old_time=$(< $_cache_cooldown)
local cur_time=$(date +%s) local cur_time=$(date +%s)
local delta=$(($cur_time - $old_time)) local delta=$(($cur_time - $old_time))
if [[ $delta -lt 60 ]]; then if [[ $delta -lt 30 ]]; then
local remains=$((60 - $delta)) local remains=$((30 - $delta))
local suffix=$(pluralize "seconds" $remains) local suffix=$(pluralize "seconds" $remains)
printf "Global API cooldown in effect. Please wait %s %s." "$remains" "$suffix" printf "Global API cooldown in effect. Please wait %s %s." "$remains" "$suffix"
exit 1 exit 1
fi fi
} }
dump_servers(){
local context="$1"
local subcontext="$2"
local ip
local qport
local res
_iterate(){
local file="$1"
shift
for server in "$@"; do
ip=$(<<< $server awk -F: '{print $1}')
qport=$(<<< $server awk -F: '{print $3}')
res=$(a2s "$ip" "$qport" info)
if [[ ! $? -eq 0 ]]; then
continue
fi
parse_server_json "$res" >> "$file"
done
}
case "$subcontext" in
*Server[[:space:]]browser*)
local file="$_cache_servers"
if [[ ! $subcontext =~ Name ]]; then
initialize_remote_servers
fi
;;
*My[[:space:]]saved[[:space:]]servers*)
local file="$_cache_my_servers"
if [[ ! $subcontext =~ Name ]]; then
[[ -f $file ]] && rm $file
_iterate "$file" "${ip_list[@]}"
fi
;;
*Recent[[:space:]]servers*)
local file="$_cache_history"
if [[ ! $subcontext =~ Name ]]; then
[[ -f $file ]] && rm $file
readarray -t iters < <(cat $history_file)
_iterate "$file" "${iters[@]}"
fi
;;
*Scan[[:space:]]LAN[[:space:]]servers*)
local port=$(<<< "$subcontext" awk -F: '{print $2}')
local file="$_cache_lan"
if [[ ! $subcontext =~ Name ]]; then
[[ -f $file ]] && rm $file
local lan=$(lan_scan $port)
readarray -t iters <<< "$lan"
_iterate "$file" "${iters[@]}"
fi
;;
esac
shift
logger INFO "Server context is '$subcontext', reading from file '$file'"
filter_servers "$file" "$@"
}
redact(){ redact(){
sed 's@\(/home/\)[^/]*@\1REDACTED@g' sed 's@\(/home/\)[^/]*@\1REDACTED@g'
} }
@ -893,9 +589,6 @@ default_steam_path="$default_steam_path"
#Preferred Steam launch command (for Flatpak support) #Preferred Steam launch command (for Flatpak support)
preferred_client="$preferred_client" preferred_client="$preferred_client"
#DZGUI source path
src_path="$src_path"
END END
} }
format_version_url(){ format_version_url(){
@ -949,6 +642,7 @@ download_new_version(){
return 1 return 1
fi fi
local version_url="$(format_version_url)" local version_url="$(format_version_url)"
local src_path="$(< "$_cache_src_path")"
mv "$src_path" "$src_path.old" mv "$src_path" "$src_path.old"
curl -Ls "$version_url" > "$src_path" curl -Ls "$version_url" > "$src_path"
rc=$? rc=$?
@ -1005,13 +699,6 @@ toggle(){
debug="" debug=""
fi fi
;; ;;
Toggle[[:space:]]Steam/Flatpak)
if [[ $preferred_client == "steam" ]]; then
preferred_client="flatpak"
else
preferred_client="steam"
fi
;;
Toggle[[:space:]]DZGUI[[:space:]]fullscreen[[:space:]]boot) Toggle[[:space:]]DZGUI[[:space:]]fullscreen[[:space:]]boot)
if [[ $fullscreen == "true" ]]; then if [[ $fullscreen == "true" ]]; then
fullscreen="false" fullscreen="false"
@ -1095,10 +782,10 @@ test_steam_api(){
} }
test_bm_api(){ test_bm_api(){
local key="$1" local key="$1"
if [[ ! $key =~ ^[0-9]+$ ]]; then if [[ -z $key ]]; then
echo 1
return return
fi fi
[[ -z $key ]] && return 1
local code=$(curl -ILs "$bm_api" \ local code=$(curl -ILs "$bm_api" \
-H "Authorization: Bearer "$key"" -G \ -H "Authorization: Bearer "$key"" -G \
-d "filter[game]=$game" \ -d "filter[game]=$game" \
@ -1110,29 +797,35 @@ update_config_val(){
local context="$1" local context="$1"
local value="$2" local value="$2"
case $1 in case $1 in
"Change client")
key="preferred_client"
;;
"Change player name") "Change player name")
key="name" key="name"
if [[ -z "${value// }" ]]; then
printf "Invalid name"
return 2
fi
;; ;;
"Change Steam API key") "Change Steam API key")
key="steam_api" key="steam_api"
if [[ ${#value} -lt 32 ]] || [[ $(test_steam_api "$value") -eq 1 ]]; then if [[ ${#value} -lt 32 ]] || [[ $(test_steam_api "$value") -eq 1 ]]; then
printf "Invalid API key" printf "Invalid API key"
return 2 return 78
fi fi
;; ;;
"Change Battlemetrics API key") "Change Battlemetrics API key")
key="api_key" key="api_key"
if [[ $(test_bm_api "$value") -eq 1 ]]; then if [[ $(test_bm_api "$value") -eq 1 ]]; then
printf "Invalid API key" printf "Invalid API key"
return 2 return 79
fi fi
;; ;;
esac esac
declare -n nr=$key declare -n nr=$key
nr="$value" nr="$value"
update_config update_config
echo "Updated the key '$key' to '$value'" return 80
return 90
} }
show_log(){ show_log(){
< "$debug_log" sed 's/Keyword␞/Keyword/' < "$debug_log" sed 's/Keyword␞/Keyword/'
@ -1190,24 +883,6 @@ open_link(){
xdg-open "$url" xdg-open "$url"
fi fi
} }
quick_connect(){
if [[ -z $fav_server ]]; then
printf "No favorite server currently set"
return 1
fi
try_connect "$fav_server"
}
connect_from_table(){
shift
local record="$1"
try_connect "$record"
}
pretty_print(){
while read -r line; do
printf "\t%s\n" "$line"
done < "$@"
}
generate_log(){ generate_log(){
source $config_file source $config_file
cat <<-DOC > $system_log cat <<-DOC > $system_log
@ -1348,6 +1023,9 @@ update_history(){
echo "$record" >> "$history_file" echo "$record" >> "$history_file"
} }
update_symlinks(){ update_symlinks(){
if [[ ! -d "$workshop_dir" ]]; then
return
fi
legacy_symlinks legacy_symlinks
symlinks symlinks
} }
@ -1394,10 +1072,17 @@ try_fallback(){
esac esac
} }
try_connect(){ try_connect(){
shift
local record="$1" local record="$1"
local appid="$2"
local binary="$3"
local ip=$(<<< $record awk -F: '{print $1}') local ip=$(<<< $record awk -F: '{print $1}')
local gameport=$(<<< $record awk -F: '{print $2}') local gameport=$(<<< $record awk -F: '{print $2}')
local qport=$(<<< $record awk -F: '{print $3}') local qport=$(<<< $record awk -F: '{print $3}')
[[ $appid -eq $exp ]] && echo "$binary" > $_cache_binary
local remote_mods local remote_mods
remote_mods=$(a2s $ip $qport rules) remote_mods=$(a2s $ip $qport rules)
if [[ $? -eq 1 ]]; then if [[ $? -eq 1 ]]; then
@ -1422,11 +1107,11 @@ try_connect(){
return 1 return 1
fi fi
case $auto_install in case $auto_install in
"") manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods";; "") manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods" "" "$appid";;
1|2) manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods" "auto" ;; 1|2) manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods" "auto" "$appid" ;;
esac esac
else else
launch "$ip" "$gameport" "$sanitized_mods" launch "$ip" "$gameport" "$sanitized_mods" "$appid"
fi fi
} }
check_architecture(){ check_architecture(){
@ -1437,68 +1122,6 @@ check_architecture(){
echo 0 echo 0
fi fi
} }
focus_beta_client(){
_wid(){
wmctrl -ilx |\
awk 'tolower($3) == "steamwebhelper.steam"' |\
awk '$5 ~ /^Steam|Steam Games List/' |\
awk '{print $1}'
}
$steam_cmd steam://open/library 2>/dev/null 1>&2 &&
$steam_cmd steam://open/console 2>/dev/null 1>&2 &&
sleep 1s
until [[ -n $(_wid) ]]; do
sleep 0.1s
done
wmctrl -ia $(_wid)
sleep 0.1s
local wid=$(xdotool getactivewindow)
local geo=$(xdotool getwindowgeometry $wid)
local pos=$(<<< "$geo" awk 'NR==2 {print $2}' | sed 's/,/ /')
local dim=$(<<< "$geo" awk 'NR==3 {print $2}' | sed 's/x/ /')
local pos1=$(<<< "$pos" awk '{print $1}')
local pos2=$(<<< "$pos" awk '{print $2}')
local dim1=$(<<< "$dim" awk '{print $1}')
local dim2=$(<<< "$dim" awk '{print $2}')
local dim1=$(((dim1/2)+pos1))
local dim2=$(((dim2/2)+pos2))
xdotool mousemove $dim1 $dim2
xdotool click 1
$steam_cmd steam://open/library 2>/dev/null 1>&2 &&
$steam_cmd steam://open/console 2>/dev/null 1>&2
}
auto_mod_install(){
# currently unused, merged with manual method
local ip="$1"
local gameport="$2"
local diff="$3"
local sanitized_mods="$4"
console_dl "$diff" &&
$steam_cmd steam://open/downloads
local total=$(<<< "$diff" wc -l)
until [[ -z $(compare "$diff") ]]; do
local missing=$(compare "$diff" | wc -l)
echo "# Downloaded $(($total-missing)) of $total mods. ESC cancels"
done | $steamsafe_zenity --pulsate --progress --title="DZG Watcher" --auto-close --no-cancel --width=500 2>/dev/null
if [[ ! $? -eq 0 ]]; then
echo "User aborted connect process. Steam may have mods pending for download."
exit 1
fi
local diff=$(compare "$sanitized_mods")
if [[ -z $diff ]]; then
#wipe old version file and replace with latest stamps
rm "$versions_file"
check_timestamps
logger INFO "Local modlist matches remote, initiating launch request"
launch "$ip" "$gameport" "$sanitized_mods"
fi
}
force_update(){ force_update(){
if [[ ! $auto_install -eq 1 ]]; then if [[ ! $auto_install -eq 1 ]]; then
printf "Only available when mod auto-install is ON" printf "Only available when mod auto-install is ON"
@ -1506,21 +1129,10 @@ force_update(){
fi fi
rm "$versions_file" rm "$versions_file"
local update=$(check_timestamps) local update=$(check_timestamps)
manual_mod_install "null" "null" "$update" "null" "force" manual_mod_install "null" "null" "$update" "null" "force" "null"
echo "Finished requesting mod updates." echo "Finished requesting mod updates."
return 0 return 0
} }
console_dl(){
readarray -t modids <<< "$@"
focus_beta_client
sleep 1.5s
for i in "${modids[@]}"; do
xdotool type --delay 0 "workshop_download_item $aid $i"
sleep 0.5s
xdotool key Return
sleep 0.5s
done
}
get_local_stamps(){ get_local_stamps(){
readarray -t modlist < <(printf "%s\n" "$@") readarray -t modlist < <(printf "%s\n" "$@")
local max="${#modlist[@]}" local max="${#modlist[@]}"
@ -1559,8 +1171,6 @@ check_timestamps(){
local aligned=$(<<< "$local_stamps" jq -r '.response.publishedfiledetails[]|"\(.publishedfileid),\(.time_updated)"') local aligned=$(<<< "$local_stamps" jq -r '.response.publishedfiledetails[]|"\(.publishedfileid),\(.time_updated)"')
readarray -t remote_ids < <(<<< "$aligned" awk -F, '{print $1}') readarray -t remote_ids < <(<<< "$aligned" awk -F, '{print $1}')
readarray -t remote_times < <(<<< "$aligned" awk -F, '{print $2}') readarray -t remote_times < <(<<< "$aligned" awk -F, '{print $2}')
readarray -t old_ids < <(< $versions_file awk -F, '{print $1}')
readarray -t old_times < <(< $versions_file awk -F, '{print $2}')
if [[ ! -f $versions_file ]]; then if [[ ! -f $versions_file ]]; then
logger INFO "No prior versions file found, creating" logger INFO "No prior versions file found, creating"
@ -1570,6 +1180,9 @@ check_timestamps(){
return 0 return 0
fi fi
readarray -t old_ids < <(< $versions_file awk -F, '{print $1}')
readarray -t old_times < <(< $versions_file awk -F, '{print $2}')
declare -A remote_version declare -A remote_version
declare -A local_version declare -A local_version
@ -1612,6 +1225,9 @@ concat_mods(){
local encoded_id local encoded_id
local link local link
for i in "${concat_arr[@]}"; do for i in "${concat_arr[@]}"; do
until [[ -f $workshop_dir/$i/meta.cpp ]]; do
sleep 0.1s
done
id=$(awk -F"= " '/publishedid/ {print $2}' "$workshop_dir"/$i/meta.cpp | awk -F\; '{print $1}') id=$(awk -F"= " '/publishedid/ {print $2}' "$workshop_dir"/$i/meta.cpp | awk -F\; '{print $1}')
encoded_id=$(encode $id) encoded_id=$(encode $id)
link="@$encoded_id;" link="@$encoded_id;"
@ -1630,6 +1246,8 @@ launch(){
local ip="$1" local ip="$1"
local gameport="$2" local gameport="$2"
local mods="$3" local mods="$3"
local appid="$4"
local concat local concat
if [[ -n $mods ]]; then if [[ -n $mods ]]; then
concat=$(concat_mods "$mods") concat=$(concat_mods "$mods")
@ -1638,18 +1256,22 @@ launch(){
fi fi
update_symlinks update_symlinks
[[ $appid -eq $exp ]] && clone_symlinks
if [[ $debug -eq 1 ]]; then if [[ $debug -eq 1 ]]; then
local launch_options="$steam_cmd -applaunch $aid -connect=$ip:$gameport -nolauncher -nosplash -name=$name -skipintro -mod=$concat" local launch_options="$steam_cmd -applaunch $appid -connect=$ip:$gameport -nolauncher -nosplash -name=$name -skipintro -mod=$concat"
printf "Debug mode: these options would have been used to launch the game: $launch_options" printf "Debug mode: these options would have been used to launch the game: $launch_options"
return 0 return 0
fi fi
echo "$concat" > "$_cache_launch" echo "$concat" > "$_cache_launch"
echo "$ip:$gameport" > "$_cache_address" echo "$ip:$gameport" > "$_cache_address"
logger INFO "Saved launch params: '$concat'" logger INFO "Saved launch params: '$concat'"
printf "Launch conditions satisfied. DayZ will now launch after you confirm this dialog." printf "Launch conditions satisfied. DayZ will launch after you confirm this dialog."
return 100 [[ $appid == "$aid" ]] && return 100
[[ $appid == "$exp" ]] && return 101
} }
final_handshake(){ final_handshake(){
local appid="$1"
local saved_mods=$(< "$_cache_launch") local saved_mods=$(< "$_cache_launch")
local saved_address=$(< "$_cache_address") local saved_address=$(< "$_cache_address")
local res=$(is_dayz_running) local res=$(is_dayz_running)
@ -1666,7 +1288,7 @@ final_handshake(){
params+=("-skipintro") params+=("-skipintro")
params+=("-name=$name") params+=("-name=$name")
params+=("-mod=$saved_mods") params+=("-mod=$saved_mods")
$steam_cmd -applaunch $aid "${params[@]}" & $steam_cmd -applaunch $appid "${params[@]}" &
until [[ $(is_dayz_running) -eq 1 ]]; do until [[ $(is_dayz_running) -eq 1 ]]; do
sleep 0.1s sleep 0.1s
done done
@ -1680,6 +1302,7 @@ manual_mod_install(){
local diff="$3" local diff="$3"
local sanitized_mods="$4" local sanitized_mods="$4"
local mode="$5" local mode="$5"
local appid="$6"
local ex="$state_path/dzg.watcher" local ex="$state_path/dzg.watcher"
readarray -t stage_mods <<< "$diff" readarray -t stage_mods <<< "$diff"
@ -1688,7 +1311,6 @@ manual_mod_install(){
_watcher(){ _watcher(){
for((i=0;i<${#stage_mods[@]};i++)); do for((i=0;i<${#stage_mods[@]};i++)); do
[[ -f $ex ]] && return 1 [[ -f $ex ]] && return 1
log ${stage_mods[$i]}
if [[ $mode == "auto" ]] || [[ $mode == "force" ]]; then if [[ $mode == "auto" ]] || [[ $mode == "force" ]]; then
$steam_cmd "steam://url/CommunityFilePage/${stage_mods[$i]}+workshop_download_item $aid ${stage_mods[$i]}" $steam_cmd "steam://url/CommunityFilePage/${stage_mods[$i]}+workshop_download_item $aid ${stage_mods[$i]}"
@ -1738,7 +1360,7 @@ manual_mod_install(){
rm "$versions_file" rm "$versions_file"
check_timestamps check_timestamps
fi fi
launch "$ip" "$gameport" "$sanitized_mods" launch "$ip" "$gameport" "$sanitized_mods" "$appid"
else else
printf "User aborted download process, or some mods may have failed to download. Try connecting again to resync." printf "User aborted download process, or some mods may have failed to download. Try connecting again to resync."
exit 1 exit 1
@ -1762,7 +1384,7 @@ foreground(){
} }
main(){ main(){
local params="$(printf '"%s", ' "$@")" local params="$(printf '"%s", ' "$@")"
logger INFO "Received request from UI constructor with params [${params::-2}]" logger INFO "Received request from UI with params [${params::-2}]"
func=${funcs["$1"]} func=${funcs["$1"]}
[[ -z $func ]] && return 1 [[ -z $func ]] && return 1
if [[ -z $2 ]]; then if [[ -z $2 ]]; then

View File

@ -1,36 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#define R 6371
#define TO_RAD (3.1415926536 / 180)
double dist(double th1, double ph1, double th2, double ph2)
{
double dx, dy, dz;
ph1 -= ph2;
ph1 *= TO_RAD, th1 *= TO_RAD, th2 *= TO_RAD;
dz = sin(th1) - sin(th2);
dx = cos(ph1) * cos(th1) - cos(th2);
dy = sin(ph1) * cos(th1);
return asin(sqrt(dx * dx + dy * dy + dz * dz) / 2) * 2 * R;
}
int main(int argc, const char * argv[])
{
if(argc < 5 || argc > 5){
return 1;
}
float coords[4];
for(int i=1;i<5;i++){
if(atof(argv[i]) == 0){
return 1;
}
coords[i] = atof(argv[i]);
}
double d = dist(coords[1], coords[2], coords[3], coords[4]);
printf("%.1f\n", d);
return 0;
}

492
helpers/pefile.py Normal file
View File

@ -0,0 +1,492 @@
import json
import struct
import typing # noqa
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from shlex import shlex
from typing import BinaryIO, Union
# https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
endian = "<"
IMAGE_DIRECTORY_ENTRY = 2
VERSION_RESOURCE = 16
RESOURCE_NODE = ".rsrc"
PE32_x86 = "0x10b"
PE32_x64 = "0x20b"
VS_VERSION_INFO_MAGIC = "0xfeef04bd0000"
VS_VERSION_INFO_ID = "VS_VERSION_INFO"
class VersionMatch(Enum):
LOCAL_OLDER = 1
LOCAL_NEWER = 2
SAME_VERSION = 3
class u8:
fmt = "B"
class u16:
fmt = "H"
class u32:
fmt = "L"
class u64:
fmt = "Q"
class i8:
fmt = "b"
class i16:
fmt = "h"
class i32:
fmt = "l"
class i64:
fmt = "q"
class PackedData:
@classmethod
def unpack(cls, data: BinaryIO):
r = []
for key, value in cls.__annotations__.items():
if value == str:
f = data.read(8).rstrip(b"\x00\x00").decode()
else:
fmt = endian + (value.fmt)
size = struct.calcsize(fmt)
f = struct.unpack(fmt, data.read(size))[0]
r.append(f)
return cls(*r)
@dataclass(slots=True, frozen=True)
class COFF_FILE_HDR(PackedData):
machine_type: u16
number_of_sections: u16
timestamp: u32
pointer_to_symbol_table: u32
number_of_symbols: u32
size_of_optional_header: u16
characteristics: u16
@dataclass(slots=True, frozen=True)
class OPTIONAL_HDR_X86(PackedData):
magic: u16
major_linker_ver: u8
minor_linker_ver: u8
size_of_code: u32
size_of_initialized_data: u32
size_of_uninitialized_data: u32
address_of_entry_point: u32
base_of_code: u32
base_of_data: u32
@dataclass(slots=True, frozen=True)
class OPTIONAL_HDR_X64(PackedData):
magic: u16
major_linker_ver: u8
minor_linker_ver: u8
size_of_code: u32
size_of_initialized_data: u32
size_of_uninitialized_data: u32
address_of_entry_point: u32
base_of_code: u32
@dataclass(slots=True, frozen=True)
class OPTIONAL_HDR_WIN_X86(PackedData):
image_base: u32
section_alignment: u32
file_alignment: u32
major_operating_system_version: u16
minor_operating_system_version: u16
major_image_version: u16
minor_image_version: u16
major_subsystem_version: u16
minor_subsystem_version: u16
win32_version_value: u32
size_of_image: u32
size_of_headers: u32
checksum: u32
subsystem: u16
dll_characteristics: u16
size_of_stack_reserve: u32
size_of_stack_commit: u32
size_of_heap_reserve: u32
size_of_heap_commit: u32
loader_flags: u32
number_of_rva_and_sizes: u32
@dataclass(slots=True, frozen=True)
class OPTIONAL_HDR_WIN_X64(PackedData):
image_base: u64
section_alignment: u32
file_alignment: u32
major_operating_system_version: u16
minor_operating_system_version: u16
major_image_version: u16
minor_image_version: u16
major_subsystem_version: u16
minor_subsystem_version: u16
win32_version_value: u32
size_of_image: u32
size_of_headers: u32
checksum: u32
subsystem: u16
dll_characteristics: u16
size_of_stack_reserve: u64
size_of_stack_commit: u64
size_of_heap_reserve: u64
size_of_heap_commit: u64
loader_flags: u32
number_of_rva_and_sizes: u32
@dataclass(slots=True, frozen=True)
class DATA_DIR(PackedData):
virtual_address: u32
size: u32
@dataclass(slots=True, frozen=True)
class SECTION_HDR(PackedData):
name: str
virtual_size: u32
virtual_address: u32
size_of_raw_data: u32
pointer_to_raw_data: u32
pointer_to_relocations: u32
pointer_to_line_numbers: u32
number_of_relocations: u16
number_of_line_numbers: u16
characteristics: u32
@dataclass(slots=True, frozen=True)
class RESOURCE_DIRECTORY_TABLE(PackedData):
characteristics: u32
timestamp: u32
major_version: u16
minor_version: u16
number_of_name_entries: u16
number_of_id_entries: u16
@dataclass(slots=True, frozen=True)
class RESOURCE_DIRECTORY_ENTRY(PackedData):
"""
This field is either a string identifying a data leaf
(if the high bit is set) or an ID to another nested directory
(if the high bit is clear). The outermost level is always a
directory. If it is a name, the lower 31 bits are the offset from the
beginning of the resource section's raw data to the name
(the name consists of 16 bits length and trailing wide characters,
in Unicode, not 0-terminated).
"""
name_or_id: u32
data_or_subdir: u32
@dataclass(slots=True, frozen=True)
class RESOURCE_DATA_ENTRY(PackedData):
data_rva: u32
size: u32
codepage: u32
reserved: u32
@dataclass(slots=True, frozen=True)
class VS_VERSION_INFO_HDR(PackedData):
size: u16
value_length: u16
value_type: u16
@dataclass(slots=True, frozen=True)
class DayZVersion:
major: int
minor: int
patch: int
@dataclass(slots=True, frozen=True)
class FileVersion:
major: int
minor: int
build: int
revision: int
@dataclass(slots=True, frozen=True)
class Result:
local: Union[str, None]
remote: str
build: str
path: Union[Path, None]
match: VersionMatch
error: Union[Exception, None]
class PeFileError(Exception):
"""Expected contents missing from headers or resource nodes"""
pass
class AppNotInstalledError(Exception):
"""App not present in user's libraryfolders"""
pass
class AppMovedError(Exception):
"""VDF points to a nonexistent location on disk"""
pass
class VDFLoadError(Exception):
"""Malformed VDF or JSON conversion"""
pass
def parse_version_number(data: BinaryIO):
# https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
minor = struct.unpack("<L", data.read(4))[0] >> 16 & 0xFFFF
major = struct.unpack("<L", data.read(4))[0] >> 0 & 0xFFFF
build = struct.unpack("<L", data.read(4))[0] >> 0 & 0xFFFF
revision = struct.unpack("<L", data.read(4))[0] >> 16 & 0xFFFF
return FileVersion(major, minor, build, revision)
def seek_to_hex(address: str, data: BinaryIO) -> None:
data.seek(int(address, base=16))
def seek_to_pe_stub(data: BinaryIO) -> None:
MAGIC = "0x3c"
seek_to_hex(MAGIC, data)
e_lfanew = hex(struct.unpack("<h", data.read(2))[0])
seek_to_hex(e_lfanew, data)
pe_stub = data.read(4).rstrip(b"\x00\x00\x00\x00").decode()
if pe_stub != "PE":
raise PeFileError("missing PE header data")
def get_dayz_version(file: Path) -> DayZVersion | Exception:
try:
version = get_version(file)
except Exception as e:
raise e
patch = str(version.build) + str(version.revision)
dz_vers = DayZVersion(version.major, version.minor, int(patch))
return dz_vers
def dayz_version_to_str(v: DayZVersion) -> str:
return ".".join(str(el) for el in [v.major, v.minor, v.patch])
def dayz_version_from_str(v: str) -> DayZVersion:
vers = v.split(".")
assert len(vers) == 3
return DayZVersion(*[int(el) for el in vers])
def get_version(file):
with open(file, "rb") as f:
seek_to_pe_stub(f)
COFF = COFF_FILE_HDR.unpack(f)
pos = f.tell()
blob = f.read()
magic = hex(struct.unpack("<H", (blob[0:2]))[0])
f.seek(pos)
if magic == PE32_x86:
OPTIONAL_HDR_X86.unpack(f)
OBJW = OPTIONAL_HDR_WIN_X86.unpack(f)
else:
OPTIONAL_HDR_X64.unpack(f)
OBJW = OPTIONAL_HDR_WIN_X64.unpack(f)
if OBJW.number_of_rva_and_sizes < 1:
raise PeFileError("no data resource directory")
data_dirs = []
for rva in range(OBJW.number_of_rva_and_sizes):
data_dir = DATA_DIR.unpack(f)
data_dirs.append(data_dir)
res_dir = data_dirs[IMAGE_DIRECTORY_ENTRY]
dir_va = res_dir.virtual_address
for section in range(COFF.number_of_sections):
hdr = SECTION_HDR.unpack(f)
if hdr.name == RESOURCE_NODE:
va = hdr.virtual_address
ptr = hdr.pointer_to_raw_data
offset = dir_va - va + ptr
seek_to_hex(hex(offset), f)
break
if hdr.name != RESOURCE_NODE:
raise PeFileError("no root resource node found")
table = RESOURCE_DIRECTORY_TABLE.unpack(f)
total = table.number_of_name_entries + table.number_of_id_entries
for entry in range(total):
entry = RESOURCE_DIRECTORY_ENTRY.unpack(f)
if entry.name_or_id == VERSION_RESOURCE:
while entry.data_or_subdir & (1 << 31):
shift = entry.data_or_subdir & ~(1 << 31)
seek_to_hex(hex(offset + shift), f)
table = RESOURCE_DIRECTORY_TABLE.unpack(f)
total = (
table.number_of_name_entries
+ table.number_of_id_entries
)
for entry in range(total):
entry = RESOURCE_DIRECTORY_ENTRY.unpack(f)
break
if entry.name_or_id > VERSION_RESOURCE:
raise PeFileError("no version info node found")
seek_to_hex(hex(offset + entry.data_or_subdir), f)
data = RESOURCE_DATA_ENTRY.unpack(f)
# https://stackoverflow.com/questions/2170843/va-virtual-address-rva-relative-virtual-address
offset = data.data_rva - hdr.virtual_address + hdr.pointer_to_raw_data
seek_to_hex(hex(offset), f)
hdr = VS_VERSION_INFO_HDR.unpack(f)
# https://learn.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo
byte_len = len(VS_VERSION_INFO_ID.encode("utf-16le"))
label = f.read(byte_len).decode("utf-16le")
if label != VS_VERSION_INFO_ID:
raise PeFileError(f"header identifier != '{VS_VERSION_INFO_ID}'")
f.read(32 - byte_len)
identifier = hex(struct.unpack("<Q", f.read(8))[0])
if identifier != VS_VERSION_INFO_MAGIC:
raise PeFileError(
f"{VS_VERSION_INFO_ID} address != '{VS_VERSION_INFO_MAGIC}'"
)
try:
version = parse_version_number(f)
except Exception as e:
return PeFileError(e)
return version
def get_pefile_path(path: str, appid: int) -> Path:
binary = "DayZ_x64.exe"
identifier = {221100: "DayZ", 1024020: "DayZ Exp"}
name = identifier[appid]
pe_path = None
path = path + "/steamapps/libraryfolders.vdf"
with open(path, "r") as f:
try:
j = json.loads(vdf_to_json(f))
except Exception:
raise VDFLoadError("Failed to parse libraryfolders")
for obj in j["libraryfolders"]:
if str(appid) in j["libraryfolders"][obj]["apps"]:
pe_path = j["libraryfolders"][obj]["path"]
pe_path += f"/steamapps/common/{name}/{binary}"
break
if pe_path is None:
raise AppNotInstalledError(
f"Failed to find a libraryfolder for the appid '{appid}'"
)
pe_path = Path(pe_path)
if pe_path.exists() is False:
raise AppMovedError(
f"Path '{pe_path}' specified in libraryfolders does not exist"
)
return pe_path
def compare_versions(local: DayZVersion, remote: DayZVersion):
"""
packaging.version module is not available OOTB on some distributions
"""
if dayz_version_to_str(local) == dayz_version_to_str(remote):
return VersionMatch.SAME_VERSION
if local.major < remote.major:
return VersionMatch.LOCAL_OLDER
if local.major > remote.major:
return VersionMatch.LOCAL_NEWER
if local.major == remote.major:
if local.minor < remote.minor:
return VersionMatch.LOCAL_OLDER
if local.minor > remote.minor:
return VersionMatch.LOCAL_NEWER
if local.minor == remote.minor:
if local.patch < remote.patch:
return VersionMatch.LOCAL_OLDER
if local.patch > remote.patch:
return VersionMatch.LOCAL_NEWER
if local.patch == remote.patch:
return VersionMatch.SAME_VERSION
def vdf_to_json(stream):
def _istr(indent, string):
return (indent * " ") + string
jbuf = "{\n"
lex = shlex(stream)
indent = 1
while True:
tok = lex.get_token()
if not tok:
return jbuf + "}\n"
if tok == "}":
indent -= 1
jbuf += _istr(indent, "}")
ntok = lex.get_token()
lex.push_token(ntok)
if ntok and ntok != "}":
jbuf += ","
jbuf += "\n"
else:
ntok = lex.get_token()
if ntok == "{":
jbuf += _istr(indent, tok + ": {\n")
indent += 1
else:
jbuf += _istr(indent, tok + ": " + ntok)
ntok = lex.get_token()
lex.push_token(ntok)
if ntok != "}":
jbuf += ","
jbuf += "\n"

472
helpers/servers.py Normal file
View File

@ -0,0 +1,472 @@
import json
import math
import os
import re
import socket
import subprocess
import sys
import typing # noqa
from dataclasses import dataclass
from urllib import request, parse
from urllib.error import HTTPError
from typing import Union
sys.path.append("a2s")
import a2s # noqa
from a2s import dayzquery # noqa
params = [
r"\nor\1\map\chernarusplus\nor\1\map\sakhal\nor\1\map\enoch\empty\1\nor\1\map\namalsk", # noqa
r"\map\namalsk\empty\1",
r"\map\namalsk\noplayers\1",
r"\map\chernarusplus\empty\1",
r"\map\chernarusplus\noplayers\1",
r"\map\sakhal\empty\1",
r"\map\sakhal\noplayers\1",
r"\map\enoch\empty\1",
r"\map\enoch\noplayers\1",
]
class BmAPIError(Exception):
pass
class BmIdError(Exception):
pass
class InvalidIpError(Exception):
pass
def get_netmask() -> str:
hostname = os.uname()[1]
i = socket.gethostbyname(hostname)
netmask = i.rsplit(".", 1)[0]
return netmask
def test_ip(suffix: int, port: int) -> dict | None:
netmask = get_netmask()
hostname = f"{netmask}.{str(suffix)}"
ping = ["ping", "-c1", "-i", "0.1", "-w", "1"]
output = subprocess.run(ping + [hostname], capture_output=True)
if output.returncode == 0:
return query_direct(hostname, port)
return None
def sanitize(name: str) -> str:
name = re.sub(r"\r", r"", name)
name = re.sub(r"\n", r"", name)
name = re.sub(r"\x01", r"", name)
name = re.sub(r"\ufeff", r"", name)
name = re.sub(r"(^!\s*)", r"-", name)
name = re.sub(r"(^-\s*)", r"", name)
name = re.sub(r"(^-)", r"", name)
name = re.sub(r"(^\s*)", r"", name)
name = re.sub(r"\t", "", name)
return name
def parse_json(json: list) -> list:
"""
Server metadata is underspecified and server operators
tend to insert random garbage in the headers. In case
sanitization failed, discard malformed rows rather than
aborting outright.
"""
rows = []
for row in json:
try:
name = sanitize(row["name"])
if name == "":
continue
except KeyError:
continue
malformed = False
for key in [
"map",
"gametype",
"players",
"max_players",
"addr",
"gameport",
]:
try:
row[key]
except KeyError:
malformed = True
break
if malformed is True:
continue
try:
r = row["gametype"].split(",")
except KeyError:
continue
if "no3rd" in r:
view = "1PP"
else:
view = "3PP"
if "external" in r:
provider = "Unoffic."
else:
provider = "Official"
if "mod" in r:
modded = True
else:
modded = False
try:
r = row["gametype"].split("lqs")
queue = r[1].split(",")[0]
except IndexError:
queue = 0
try:
test_time = re.search(r"[0-9]{2}:[0-9]{2}", row["gametype"])
if test_time:
time = test_time.group(0)
else:
time = "Unknown"
except AttributeError:
time = "Unknown"
try:
ip = row["addr"].split(":")[0] + ":" + str(row["gameport"])
qport = row["addr"].split(":")[1]
except IndexError:
continue
try:
ping = row["ping"]
except KeyError:
ping = 9999
mapname = row["map"].lower()
players = row["players"]
max_players = row["max_players"]
raw = [
name,
mapname,
view,
time,
int(players),
int(max_players),
int(queue),
ip,
int(qport),
ping,
provider,
modded,
]
rows.append(raw)
return rows
def query_direct(ip: str, qport: int, TIMEOUT: float=3.0) -> dict | None:
try:
info = a2s.info((ip, qport), TIMEOUT)
name = info.server_name
mapname = info.map_name
address = ip + ":" + str(qport)
gameport = str(info.port)
players = info.player_count
max_players = info.max_players
keywords = info.keywords
try:
ping = info.ping
ping = math.floor(info.ping * 1000)
except AttributeError:
ping = 9999
res = {}
res["name"] = name
res["map"] = mapname
res["gametype"] = keywords
res["players"] = players
res["max_players"] = max_players
res["addr"] = address
res["gameport"] = gameport
res["ping"] = ping
return res
except TimeoutError:
return None
except KeyError:
return None
@dataclass(slots=True, frozen=True)
class Res:
status: int
parsed: bool
json: Union[str, None]
@dataclass(slots=True, frozen=True)
class Ping:
addr: str
iteration: int
ping: int
@dataclass(slots=True, frozen=True)
class Details:
data: Union[list, None]
description: str
success: bool
@dataclass(slots=True, frozen=True)
class Prereqs:
password: bool
gameport: int
appid: Union[int, None]
version: Union[str, None]
@dataclass(slots=True)
class Record:
"""
The gameport field is manipulated by the RowType.CONN_BY_IP method
"""
ip: str
gameport: int
qport: int
def get_prereqs(ip: str, qport: int) -> Prereqs:
try:
info = a2s.info((ip, qport))
except TimeoutError:
return Prereqs(False, 0, None, None)
gameport = getattr(info, "port", 0)
is_password = getattr(info, "password_protected", False)
appid = getattr(info, "game_id", None)
version = getattr(info, "version", None)
return Prereqs(is_password, gameport, appid, version)
def details(ip: str, qport: int) -> Details:
default_str = "None provided"
try:
info = a2s.info((ip, qport))
except TimeoutError:
return Details(None, default_str, False)
try:
rules = dayzquery.dayz_rules((ip, int(qport)))
except TimeoutError:
return Details(None, default_str, False)
try:
keywords = info.keywords.split(",")
except AttributeError:
return Details(None, default_str, False)
battleye = "Disabled"
if "battleye" in keywords:
battleye = "Enabled"
day_accel = 0.0
night_accel = 0.0
for keyword in keywords:
if "etm" in keyword:
day_accel = float(keyword.lstrip("etm"))
day_accel = float(f"{day_accel:g}")
if "entm" in keywords:
night_accel = float(keyword.lstrip("entm"))
night_accel = float(f"{night_accel:g}")
try:
password = info.password_protected
if password is False:
password = "Disabled"
else:
password = "Enabled"
except AttributeError:
password = "-"
try:
vac = info.vac_enabled
if vac is False:
vac = "Disabled"
else:
vac = "Enabled"
except AttributeError:
vac = "-"
try:
version = info.version
except AttributeError:
version = "-"
try:
dlc = rules.dlc_flags
if dlc == 0:
dlc = "None"
if dlc == 2:
dlc = "Frostline"
except AttributeError:
dlc = "not specified"
try:
platform = rules.platform
if platform == "win":
platform = "Windows"
if platform == "?":
platform = "Linux"
except AttributeError:
platform = "not specified"
try:
description = rules.description.strip()
if description == "":
description = default_str
except AttributeError:
description = default_str
rows = [
["Battleye", battleye],
["Daytime acceleration", f"{day_accel}x"],
["DLC", dlc],
["Night-time acceleration", f"{night_accel}x"],
["Password", password],
["Platform", platform],
["Valve Anti-Cheat", vac],
["Version", version],
]
return Details(rows, description, True)
def ping(iteration: int, row: list) -> Ping:
addr = row[7]
qport = row[8]
res = None
if row[9] != 9999:
return Ping(addr, iteration, row[9])
try:
ip = addr.split(":")[0]
except IndexError:
ping = 9999
try:
res = query_direct(ip, qport, 0.5)
except Exception:
pass
if res is None:
ping = 9999
else:
ping = res["ping"]
return Ping(addr, iteration, ping)
def query_api(key: str, appid: int, param: str) -> Res:
LIMIT = 10000
url = "https://api.steampowered.com/IGameServersService/GetServerList/v1/?"
payload: dict[str, Union[int, str]] = {
"filter": r"\appid" + fr"\{appid}" + param,
"limit": LIMIT,
"key": key,
}
par = parse.urlencode(payload)
url = f"{url}{par}"
status = 200
parsed = True
data = None
try:
with request.urlopen(url) as response:
if response.status != 200:
status = response.status
try:
parsed = True
data = json.load(response)
except json.decoder.JSONDecodeError:
parsed = False
data = None
except HTTPError:
status = 403
parsed = False
data = None
finally:
return Res(status, parsed, data)
def query_bm_api(api_key: str, bm_id: str) -> Record:
if bm_id.isnumeric() is False:
raise BmIdError("ID must be numeric only")
payload: dict[str, Union[int, str]] = {
"sort": "-players",
"filter[game]": "dayz",
"filter[ids][whitelist]": bm_id,
}
url = "https://api.battlemetrics.com/servers?"
par = parse.urlencode(payload)
url = f"{url}{par}"
hdr = {"Authorization": "Bearer " + api_key}
r = request.Request(url, headers=hdr)
try:
with request.urlopen(r) as response:
try:
j = json.load(response)
except json.decoder.JSONDecodeError:
raise BmAPIError("Malformed response from Battlemetrics")
if len(j["data"]) < 1:
raise BmAPIError("Not a valid Battlemetrics ID")
j = j["data"][0]["attributes"]
return Record(j["ip"], j["port"], j["portQuery"])
except HTTPError:
raise BmAPIError("Failed to query Battlemetrics")
def validate_ip(addr: str) -> Record:
fields = addr.split(":")
if len(fields) != 2:
raise InvalidIpError("Address must be formatted as IP:Queryport")
ip = fields[0]
port = fields[1]
try:
int(port)
except ValueError:
raise InvalidIpError(f"'{port}' is not a valid port")
if int(port) > 65535 or int(port) < 0:
raise InvalidIpError(f"'{port}' is not a valid port")
try:
socket.inet_aton(ip)
except OSError:
raise InvalidIpError(f"'{ip}' is not a valid IP")
ip = addr.split(":")[0]
qport = int(addr.split(":")[1])
record = Record(ip, 0, qport)
return record

1
helpers/tools/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
latlon

24
helpers/tools/Makefile Normal file
View File

@ -0,0 +1,24 @@
bin := latlon
CC = gcc
CFLAGS += -Wall
CFLAGS += -Wextra
CFLAGS += -Wpedantic
CFLAGS += -std=c23
CFLAGS += -O3
LDFLAGS += -lm
PREFIX ?= $(HOME)/.local/share/dzgui
all: $(bin)
install: $(bin)
install -Dm 755 $(bin) $(PREFIX)
$(bin): latlon.c
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
clean:
rm -f $(bin)

77
helpers/tools/latlon.c Normal file
View File

@ -0,0 +1,77 @@
// This small program computes the distance between two points on the Earths
// surface. The algorithm used for computing is called haversine formula.
// Source for the algorithm can be found here:
// https://en.wikipedia.org/wiki/Haversine_formula
#include <math.h>
#include <stdlib.h>
#include <stdio.h>
// Earth's radius in meters.
#define RADIUS ((12756L / 2.0L) * 1000L)
// PI natural constant
#define PI 3.1415926536L
// coordinates struct for storing points
struct Coordinate {
double latitude;
double longitude;
};
/**
* @brief Calculates a radian value from an angle
*/
double radian_from(const double angle) {
return (angle * PI) / 180;
}
/**
* @brief Calculate the haversine function from an angle in radian
*/
double haversine(const double angle) { return ((1.0L - cos(angle)) / 2); }
/**
* @brief Calculate the distance between two coordinates
*/
double great_circle_distance(const struct Coordinate a,
const struct Coordinate b) {
// calculate longitude and latitude differences
struct Coordinate diff_coord = {
.longitude = a.longitude - b.longitude,
.latitude = a.latitude - b.latitude,
};
// calculate haversine(theta) value, where theta is the angle between the
// two coordinates
double hav_theta = haversine(diff_coord.latitude)
+ cos(a.latitude) * cos(b.latitude) * haversine(diff_coord.longitude);
// calculate distance from radius, and haversine(theta) values
double distance = 2 * RADIUS * asin(sqrt(hav_theta));
return distance;
}
int main(int argc, const char* argv[]) {
if (argc < 5 || argc > 5) {
return 1;
}
const struct Coordinate a = {
.latitude = radian_from(atof(argv[1])),
.longitude = radian_from(atof(argv[2])),
};
const struct Coordinate b = {
.latitude = radian_from(atof(argv[3])),
.longitude = radian_from(atof(argv[4])),
};
// Returns the distance in meters.
// To pretty-print the result in a consumer of this module, it is
// recommended to divide the result by 1,000 and round it.
printf("%.1f\n", great_circle_distance(a, b));
return 0;
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 608 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 569 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB