Compare commits

..

625 commits
0.0.0 ... main

Author SHA1 Message Date
GitHub Action
61ef15909b Add/Update badge 2026-04-07 05:24:08 +00:00
Cain
6e5279b529 fix(MSRV docs): add lib readme in VERSIONS.md and change lib readme to 1.85.1 msrv 2026-02-22 18:56:10 +00:00
Cain
2432f3b770 chore(license): update copyright year to 2026 2026-02-22 18:41:46 +00:00
Cain
61c0b529b2 chore(cli): bump version to 0.5.0 and update changelog 2026-02-22 18:40:58 +00:00
Cain
94772752e6 chore(lib): bump version to 0.9.0 and update changelog 2026-02-22 18:35:45 +00:00
Cain
b338062553 chore(cli): update dependency versions and format 2026-02-22 18:21:56 +00:00
Cain
79fc1db14c fix(MSRV): update to 1.85.1 across the project 2026-02-22 18:19:48 +00:00
Cain
b0725834e4 chore(deps): update dependency versions and clean up cargo.toml 2026-02-22 18:11:47 +00:00
Cain
4ea333f16b fix(clippy): add lifetime annotations to various methods and structs (mismatched_lifetime_syntaxes) 2026-02-22 17:47:05 +00:00
Cain
a5f15a040a
fix(minecraft java): EncoderException error when timestamp not included (#255) 2026-02-22 17:42:19 +00:00
Paul Hansen
61ff780470 Fix minecraft EncoderException error 2026-02-21 14:20:49 -06:00
GitHub Action
64608d95ee Add/Update badge 2026-02-03 05:01:51 +00:00
GitHub Action
2b9a014484 Add/Update badge 2025-09-09 03:47:37 +00:00
Cain
91d4f03b1f
revert(ci): downgrade back to v4 labeler 2025-09-07 15:22:28 +01:00
Cain
8370020ae2
Merge pull request #246 from gamedig/dependabot/github_actions/dot-github/workflows/actions/labeler-6
chore(deps): bump actions/labeler from 4 to 6 in /.github/workflows
2025-09-05 20:24:36 +01:00
dependabot[bot]
d519665c06
chore(deps): bump actions/labeler from 4 to 6 in /.github/workflows
Bumps [actions/labeler](https://github.com/actions/labeler) from 4 to 6.
- [Release notes](https://github.com/actions/labeler/releases)
- [Commits](https://github.com/actions/labeler/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/labeler
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-05 13:08:05 +00:00
dependabot[bot]
1b79a3b825
chore(deps): bump actions/checkout from 4 to 5 in /.github/workflows (#244)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 09:56:41 +01:00
Cain
080f327ab6
Maintenance: dep updates and clippy fixs (#243)
* chore(deps): update crc32fast to v1.5

* chore(deps): update ureq to v2.12

* chore(deps): update phf to v0.13

* chore(deps): update clap to v4.5

* chore(deps): update lazy_static to v1.5

* chore(deps): update thiserror to v2.0.16

* chore(deps): update clap to v4.5

* chore(deps): update bson to v2.15

* chore(deps): update base64 to v0.22

* chore(deps): update quick-xml to v0.38

* chore(deps): update webbrowser to v1.0

* chore(clippy): fix a load of "variables can be used directly"

* chore(ci): update MSRV toolchain to 1.82.0

* chore(pre-commit): update MSRV to 1.82.0

* chore(README): update Minimum Supported Rust Version to 1.82.0

* chore(VERSIONS): update MSRV to 1.82.0

* chore(cli): bump version to 0.4.0 and update MSRV to 1.82.0

* chore(release): bump version to 0.8.0 and update MSRV to 1.82.0
2025-08-25 12:52:54 +01:00
Cain
d6fe78f0e6
chore(workflow): remove --locked arg on codeql 2025-07-01 19:39:37 +01:00
Cain
4ad07377f2
chore(workflow): try none codeql build mode 2025-07-01 19:37:34 +01:00
Cain
90458ed443
chore(workflow): codeql autobuild doesnt support rust 2025-07-01 19:34:19 +01:00
Cain
085913f903
chore(workflow): rust is built in on ci 2025-07-01 19:29:27 +01:00
Cain
4ddb0fd960
chore(workflow): attempt to setup codeql for rust 2025-07-01 19:23:16 +01:00
GitHub Action
380bbd4ab4 Add/Update badge 2025-07-01 04:12:14 +00:00
Cain
350f297193
maintenance(workspace): updates and clippy fixs (#242) 2025-04-23 22:04:37 +01:00
Cain
ad8f1b8081 chore(cli): bump semver to v0.3.0 2025-04-23 21:59:16 +01:00
Cain
5d36f9bcff fix(cli): crates io dont like wildcards 2025-04-23 21:58:37 +01:00
Cain
b44c85c118 chore(changelog): update cli and lib with changes 2025-04-23 21:48:59 +01:00
Cain
d969e4e0b7 chore(cli): use v0.X.X gamedig to cut down patches 2025-04-23 21:48:20 +01:00
Cain
fddfd830b7 chore(lib): bump to 0.7.0 2025-04-20 23:19:30 +01:00
Cain
e87bde53e7 chore(nightly): bump to nightly-2025-04-19 as the other toolchain is 2 years old 2025-04-20 23:11:35 +01:00
Cain
8160a866b0 chore(format): with new config 2025-04-20 23:06:55 +01:00
Cain
0a7ba6848e chore(rustfmt): bump to 1.8.0 and fix deprecated warns 2025-04-20 23:05:33 +01:00
Cain
b7144e015d chore(clippy): clean up warnings 2025-04-20 22:59:46 +01:00
Cain
4c5ffde2e5 chore(msrv): bump to 1.81.0 2025-04-20 22:44:19 +01:00
GitHub Action
7c4a5e4f9c Add/Update badge 2025-04-20 21:33:57 +00:00
Cain
0a304c7513
feat(game): arma 3 support (#240) 2025-04-20 22:33:26 +01:00
Cain
8ebb685b33 chore(changelog): follow format for arma 3 change 2025-04-20 22:30:40 +01:00
Cain
13814ee6e3 chore(changelog): add arma 3 2025-04-20 22:27:27 +01:00
Thomas
b3ae7b7fb1 Applied formatting to valve.rs 2025-04-18 09:03:54 +02:00
Thomas
6953840af9 Fix wrong app id in Valve game query modules 2025-04-17 19:00:53 +02:00
Thomas
3588b97b83 Added arma 3 2025-04-17 18:57:14 +02:00
GitHub Action
43a613b7c0 Add/Update badge 2025-03-25 03:47:25 +00:00
GitHub Action
67b97ca1c9 Add/Update badge 2025-01-21 03:37:35 +00:00
GitHub Action
373a4553ce Add/Update badge 2025-01-14 03:37:04 +00:00
Cain
fa84a07fca
chore(license): update year (happy new year) 2025-01-02 17:09:15 +00:00
Cain
85a733386b chore(cli): add patch to semver and update changelog 2024-12-05 21:32:23 +00:00
Cain
07bfd69961 chore(lib): add patch to semver and update changelog 2024-12-05 21:29:54 +00:00
Cain
a9fe5858b0 chore(games): add starbound to supported games 2024-12-05 21:22:33 +00:00
GitHub Action
fc5113507f Add/Update badge 2024-12-05 20:46:21 +00:00
Cain
d1eb6d7ca5
impl(game): starbound support (#234) 2024-12-05 20:45:47 +00:00
Kae
b6696e1af3 Add Starbound support 2024-12-01 18:31:02 +11:00
Cain
c04f442bbe
chore: maintenance (#233) 2024-11-27 15:21:11 +00:00
Cain
68cd20963a chore(readme): missed a msrv semver 2024-11-26 18:15:50 +00:00
Cain
adf65276dd chore(readme): make dep badge use latest 2024-11-26 18:09:05 +00:00
Cain
961f57d6ec chore(msrv): bump to 1.71.1 2024-11-26 13:50:01 +00:00
Cain
1d112cc661 chore(cli): bump to 0.2.0 2024-11-26 13:20:44 +00:00
Cain
1d4e415a5f chore(cli): update changelog 2024-11-26 13:19:52 +00:00
Cain
4fb0d24a4e chore(lib): bump to 0.6.0 2024-11-26 13:17:59 +00:00
Cain
0321cfb34f chore(lib): update changelog 2024-11-26 13:17:02 +00:00
Cain
e539104a8a chore(readme): update msrv 2024-11-26 13:14:00 +00:00
Cain
54d3693cb0 chore(cli): fix typos in readme 2024-11-26 13:12:10 +00:00
Cain
7985fb2613 chore(ci): format 2024-11-26 12:38:32 +00:00
Cain
102e48914b chore(lib): bump msrv 1.65.0 -> 1.67.0 for linux deps 2024-11-26 12:36:41 +00:00
Cain
98cff08512 chore(ci): discontinue github releases 2024-11-26 12:29:09 +00:00
Cain
3bcf9385f2 refactor(cli): improve how we serve the bin to users 2024-11-26 02:05:36 +00:00
Cain
8fab167157
fix: Change the app id for "The Forest" so it doesn't fail (#232) 2024-11-25 17:50:32 +00:00
Paul Hansen
a2fe00e1c4 Use both the app id and dedicated app id for the forest 2024-11-25 11:45:12 -06:00
Paul Hansen
d66fe6baf4 Change the app id for the forest so it doesn't fail
Changes it to use the app id of the game instead of the dedicated
server.
2024-11-25 01:35:12 -06:00
dependabot[bot]
f66b33f113
chore(deps): update thiserror requirement from 1.0.43 to 2.0.0 in /crates/cli 2024-11-07 23:36:21 +00:00
Cain
6aa900671e
chore(deps): update quick-xml requirement from 0.36.0 to 0.37.0 in /crates/cli 2024-11-07 23:33:06 +00:00
CosminPerRam
bcc92d17df
feat: minetest support (#218)
* feat: initial minetest support

* move player to master, remove comments

* add markdown lines

* oops, change player namespace

* fix some edge cases

* add entry to responses, tweak field names
2024-10-30 13:24:40 +02:00
dependabot[bot]
24134d6f23
chore(deps): update quick-xml requirement in /crates/cli
Updates the requirements on [quick-xml](https://github.com/tafia/quick-xml) to permit the latest version.
- [Release notes](https://github.com/tafia/quick-xml/releases)
- [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md)
- [Commits](https://github.com/tafia/quick-xml/compare/v0.36.0...v0.37.0)

---
updated-dependencies:
- dependency-name: quick-xml
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-29 20:44:20 +00:00
CosminPerRam
30ae60e4dc
ci: fix audit failing by generating cargo lock file before running it (#229) 2024-10-29 22:42:33 +02:00
CosminPerRam
664cf8b2db feat: bump version to 0.5.2 2024-10-20 20:43:52 +03:00
GitHub Action
1b1ecc651e Add/Update badge 2024-10-15 03:43:09 +00:00
Cain
c446bcab54 chore(lib): update changelog 2024-10-13 20:40:53 +01:00
Cain
e4baf07e48
Fix: match port on epic protocol (#225) 2024-10-13 20:09:57 +01:00
GitHub Action
344622629e Add/Update badge 2024-10-08 03:43:07 +00:00
GitHub Action
c7451b098b Add/Update badge 2024-10-01 03:47:27 +00:00
Cain
480ff2b531
Fix: Unused var in most features #224 2024-09-26 23:43:12 +01:00
Cain
3964735af8 fix(lib socket): feature gate local_addr to capture 2024-09-26 23:23:42 +01:00
Cain
c82dc7d653
chore(workflow deps): #223 bump audit-check from 1.4.1 to 2.0.0 2024-09-25 15:27:58 +01:00
dependabot[bot]
4866003252
chore(deps): bump rustsec/audit-check in /.github/workflows
Bumps [rustsec/audit-check](https://github.com/rustsec/audit-check) from 1.4.1 to 2.0.0.
- [Release notes](https://github.com/rustsec/audit-check/releases)
- [Changelog](https://github.com/rustsec/audit-check/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustsec/audit-check/compare/v1.4.1...v2.0.0)

---
updated-dependencies:
- dependency-name: rustsec/audit-check
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-24 13:54:01 +00:00
GitHub Action
16fd486208 Add/Update badge 2024-09-17 03:36:58 +00:00
Cain
ada3c548f0
Merge pull request #221 from gamedig/fix/read-string-may-panic-index-out-of-range 2024-09-07 11:55:30 +01:00
Cain
4784e0a281 chore(buf): format a line for ci 2024-09-07 00:58:27 +01:00
Cain
786e9dad94 fix(buf): use correct len fn on OOB check 2024-09-07 00:53:21 +01:00
Cain
e39b880364 refactor(buf): clean up out of bounds err msg 2024-09-07 00:33:53 +01:00
Cain
73f39510fe fix(buf): add error for out of bounds panic on read string 2024-09-07 00:23:57 +01:00
GitHub Action
70571218b6 Add/Update badge 2024-09-03 03:38:23 +00:00
GitHub Action
486ae3b52c Add/Update badge 2024-08-20 03:37:50 +00:00
GitHub Action
11088e7786 Add/Update badge 2024-07-23 03:39:39 +00:00
CosminPerRam
41a3d88fb5
feat: add minetest master server service (#209)
* feat: add minetest master server service"

* restore tf2 example

* chore: replace default with None

* fix: make it available only on TLS and serde

* docs: update changelog
2024-07-21 03:01:10 +03:00
Cain
397817b6d6
fix(cli): release-profile (#216
Fix CLI release profile
2024-07-19 23:24:38 +01:00
Cain
3300c65b07 fix(cli): add explicit cargo config for crate 2024-07-16 22:49:09 +01:00
Cain
3aaa32edb8
chore: release gamedig-cli v0.1.1 (#215) 2024-07-15 20:20:37 +01:00
Cain
1af2d146f3 chore(cli): bump semver with changes 2024-07-15 11:58:18 +01:00
Cain
5e5d5ab05e chore(cli): update readme 2024-07-15 11:42:38 +01:00
Cain
81c81e929c
Merge pull request #214 from gamedig/dependabot/cargo/crates/cli/quick-xml-0.36.0 2024-07-10 10:09:47 +01:00
dependabot[bot]
1a023d62eb
chore(deps): update quick-xml requirement in /crates/cli
Updates the requirements on [quick-xml](https://github.com/tafia/quick-xml) to permit the latest version.
- [Release notes](https://github.com/tafia/quick-xml/releases)
- [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md)
- [Commits](https://github.com/tafia/quick-xml/compare/v0.35.0...v0.36.0)

---
updated-dependencies:
- dependency-name: quick-xml
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-09 13:23:46 +00:00
GitHub Action
cac5c234ee Add/Update badge 2024-07-09 03:36:13 +00:00
Cain
927c4dc30d
Merge pull request #213 from gamedig/dependabot/cargo/crates/cli/quick-xml-0.35.0 2024-07-01 18:19:28 +01:00
Cain
d51e54452f
Merge pull request #205 from gamedig/dependabot/cargo/crates/lib/pnet_packet-0.35 2024-07-01 18:18:18 +01:00
dependabot[bot]
723461399a
chore(deps): update quick-xml requirement in /crates/cli
Updates the requirements on [quick-xml](https://github.com/tafia/quick-xml) to permit the latest version.
- [Release notes](https://github.com/tafia/quick-xml/releases)
- [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md)
- [Commits](https://github.com/tafia/quick-xml/compare/v0.31.0...v0.35.0)

---
updated-dependencies:
- dependency-name: quick-xml
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 13:41:22 +00:00
CosminPerRam
79aeec8df6
chore: apply clippy suggestions (#208)
* perf: apply clippy suggestions

* allow some unused imports, remove unused import
2024-06-29 21:51:15 +03:00
GitHub Action
27840e3ff3 Add/Update badge 2024-06-25 03:36:16 +00:00
GitHub Action
6f358340f1 Add/Update badge 2024-06-22 00:29:06 +00:00
CosminPerRam
942ce2b601
feat: add Soulmask support (#207) 2024-06-22 03:28:30 +03:00
CosminPerRam
14a4475d51 docs: add deps.rs shield badge 2024-06-22 03:13:39 +03:00
GitHub Action
2663cf950d Add/Update badge 2024-06-18 03:36:56 +00:00
dependabot[bot]
c18c6f202c
chore(deps): update pnet_packet requirement in /crates/lib
Updates the requirements on [pnet_packet](https://github.com/libpnet/libpnet) to permit the latest version.
- [Release notes](https://github.com/libpnet/libpnet/releases)
- [Commits](https://github.com/libpnet/libpnet/compare/v0.34.0...v0.35.0)

---
updated-dependencies:
- dependency-name: pnet_packet
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-31 13:41:17 +00:00
CosminPerRam
e52d7fbd6f feat: bump version to 0.5.1 2024-05-12 21:42:11 +03:00
CosminPerRam
111cdd5eae Merge branch 'main' of github.com:gamedig/rust-gamedig 2024-05-12 21:35:32 +03:00
CosminPerRam
196d7121de feat: bump clap to 4.5.4 on cli 2024-05-12 21:34:00 +03:00
CosminPerRam
1481cfd41a feat: bump crc32fast, ureq and clap lib deps 2024-05-12 21:33:02 +03:00
GitHub Action
48aa0ec221 Add/Update badge 2024-05-11 18:49:58 +00:00
CosminPerRam
49b95861f0 feat: add abiotic factor support 2024-05-11 21:49:17 +03:00
GitHub Action
b2da9bee3c Add/Update badge 2024-05-05 20:53:12 +00:00
CosminPerRam
e57efb392d
feat: nova-life amboise support (#203) 2024-05-05 23:52:43 +03:00
CosminPerRam
50ccac3cc9 feat: replace reduntant closure for method call 2024-05-01 18:55:20 +03:00
CosminPerRam
78b52c0d23 feat: replace two cases of not using fun call on unwrapping 2024-05-01 18:51:48 +03:00
CosminPerRam
c2f6a68648 feat: make GameMode::as_str constant 2024-05-01 18:50:05 +03:00
CosminPerRam
32c267621e feat: add eq where partialeq is already and is possible 2024-05-01 18:48:11 +03:00
CosminPerRam
1877a16457 feat: use Self instead of struct name whereas applicable 2024-05-01 18:46:37 +03:00
CosminPerRam
f53635993e feat: use map_or_else instead of if else on option 2024-05-01 18:44:21 +03:00
GitHub Action
d19c3696ad Add/Update badge 2024-04-28 20:47:47 +00:00
CosminPerRam
829ab5b7f2
feat: add arma reforger support (#202) 2024-04-28 23:47:17 +03:00
dependabot[bot]
ddd95be413
chore(deps): update webbrowser requirement in /crates/cli (#201)
Updates the requirements on [webbrowser](https://github.com/amodm/webbrowser-rs) to permit the latest version.
- [Release notes](https://github.com/amodm/webbrowser-rs/releases)
- [Changelog](https://github.com/amodm/webbrowser-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amodm/webbrowser-rs/compare/v0.8.12...v1.0.0)

---
updated-dependencies:
- dependency-name: webbrowser
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-28 22:43:27 +03:00
GitHub Action
c357b4594b Add/Update badge 2024-04-28 19:29:06 +00:00
CosminPerRam
462677f928 fix: aliens vs predator specify year as there is also an older game not to be confused with 2024-04-28 22:28:41 +03:00
CosminPerRam
4df721e3b6 feat: add Aliens vs. Predator support 2024-04-28 22:25:35 +03:00
CosminPerRam
e032eb3441 docs: add missing protocol column value on pixark in games.md 2024-04-21 19:22:01 +03:00
CosminPerRam
45ffa53de3
feat: add Ark: Survival Ascended support (#197)
* feat: add initial epic client auth call

* fix: working client auth

* feat: unfinished initial EOS query

* first successful query

* first successful server query

* run fmt

* be a bit more detailed about servers

* properly run fmt for sure this time fr fr

* port of what node gamedig has done

* feat: remove query_raw_values to query_raw

* feat: add raw field to epic response

* feat: pass SocketAddr to epic

* feat: remove unused pub access to internal only struct

* feat: add initial generic impl

* fix: possibly conditional comp

* feat: add epic to the protocol list

* feat: add version and add epic to RESPONSES.md

* feat: add asa to definitions

* feat: add initial protocol macros

* feat: conditional serde ser and des

* fix: cfg serde stuff

* fix: epic macro warn dead code

* partial feature gate epic to tls

* fix: remove asa from game definitions
2024-04-21 18:53:33 +03:00
CosminPerRam
1620ba36b8 Merge branch 'main' of github.com:gamedig/rust-gamedig 2024-04-14 23:21:45 +03:00
CosminPerRam
8a17bd6345 chore: add underscore for pvak2 steam ids 2024-04-14 23:16:09 +03:00
GitHub Action
4651990e8b Add/Update badge 2024-04-14 20:16:03 +00:00
CosminPerRam
e9f5e3e5db Merge branch 'main' of github.com:gamedig/rust-gamedig 2024-04-14 23:15:30 +03:00
CosminPerRam
66a9ed8b3f feat: add PixARK support. 2024-04-14 23:15:17 +03:00
CosminPerRam
3c9f109942 chore: reformat markdown files 2024-04-14 23:08:11 +03:00
GitHub Action
4faf2f89f4 Add/Update badge 2024-04-14 20:07:17 +00:00
CosminPerRam
29f1098daf feat: add Pirates, Knights and Vinkins 2 support 2024-04-14 23:06:43 +03:00
GitHub Action
00840cb4a6 Add/Update badge 2024-04-07 20:10:03 +00:00
CosminPerRam
9264d5fe4a feat: add myth of empires support 2024-04-07 23:09:16 +03:00
GitHub Action
cca938674c Add/Update badge 2024-04-07 20:04:40 +00:00
CosminPerRam
a207c39915
feat: add enshrouded support (#199) 2024-04-07 23:04:04 +03:00
GitHub Action
14bf759fa7 Add/Update badge 2024-04-02 03:35:14 +00:00
GitHub Action
40b70d6576 Add/Update badge 2024-03-24 21:59:58 +00:00
CosminPerRam
f488658afc feat: add mordhau support 2024-03-24 23:59:22 +02:00
CosminPerRam
6e53ef0c22
feat(http): add per-request headers option (#196)
* http: Add per-request headers option

* http: Improve tests

---------

Co-authored-by: Douile <douile@douile.com>
2024-03-16 17:57:07 +02:00
CosminPerRam
f54321da18 fix(cli): release binaries workflow 2024-03-15 18:32:52 +02:00
CosminPerRam
ea6140c5d9 docs(lib): fix eco not being hyperlinked in changelog 2024-03-15 18:30:49 +02:00
CosminPerRam
275fb7d4cd
feat: release 0.5.0 (#191)
* feat: add initial crates readmes

* feat: add initial CLI readme

* fix: some links

* feat: fix node badge, split changelogs, fix some other links add docs in cli

* feat: feature the gamedig site in the lib

* feat: fill in cli package fields

* feat: set CLI version to 0.1.0

* feat: update changelogs to set vers

* feat: update the release workflow
2024-03-15 18:14:59 +02:00
GitHub Action
bcb9ac64c0 Add/Update badge 2024-03-10 21:42:48 +00:00
CosminPerRam
328dfd312b Merge branch 'main' of github.com:gamedig/rust-gamedig 2024-03-10 23:42:18 +02:00
CosminPerRam
04803996cd fix: counter strike 2 gid 2024-03-10 23:41:43 +02:00
GitHub Action
0f0a9da609 Add/Update badge 2024-03-10 21:40:09 +00:00
CosminPerRam
967dc37d64 feat: add Double Action: Boogaloo support 2024-03-10 23:39:39 +02:00
CosminPerRam
82b7a5f169 feat: add counter-strike 2 support 2024-03-10 23:36:04 +02:00
dependabot[bot]
b2e34b32f8
chore(deps): update base64 requirement in /crates/cli (#192)
Updates the requirements on [base64](https://github.com/marshallpierce/rust-base64) to permit the latest version.
- [Changelog](https://github.com/marshallpierce/rust-base64/blob/master/RELEASE-NOTES.md)
- [Commits](https://github.com/marshallpierce/rust-base64/compare/v0.21.7...v0.22.0)

---
updated-dependencies:
- dependency-name: base64
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-04 17:29:00 +02:00
dependabot[bot]
03fd0c10b2
chore(deps): bump rust-build/rust-build.action in /.github/workflows (#190)
Bumps [rust-build/rust-build.action](https://github.com/rust-build/rust-build.action) from 1.4.4 to 1.4.5.
- [Release notes](https://github.com/rust-build/rust-build.action/releases)
- [Commits](https://github.com/rust-build/rust-build.action/compare/v1.4.4...v1.4.5)

---
updated-dependencies:
- dependency-name: rust-build/rust-build.action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-26 16:06:21 +02:00
CosminPerRam
8a38d742f6 chore: run formatting on minecraft types 2024-02-26 00:16:59 +02:00
CosminPerRam
a9fcfe1bb3 tests: add minecraft varint and string types 2024-02-26 00:15:33 +02:00
CosminPerRam
b913b0c7e7 fix: remove unused test 2024-02-25 23:56:21 +02:00
CosminPerRam
1080a94bd2 fix: replace 2 instances of .get(0) with .first() 2024-02-25 23:54:19 +02:00
GitHub Action
4d214ad5fb Add/Update badge 2024-02-25 21:50:57 +00:00
CosminPerRam
c1454805bb
feat: add black ops 3 support (#189)
* feat: add black ops 3 support

* chore: formatting
2024-02-25 23:50:27 +02:00
CosminPerRam
ef017d4703 docs: fix gramatical error in eco protocol 2024-02-25 19:39:27 +02:00
GitHub Action
5d48de178b Add/Update badge 2024-02-10 18:27:29 +00:00
CosminPerRam
310b62664c
feat: http client and eco support (#175)
* feat: initial http and eco support

* http: Replace reqwest with ureq and add HTTPS support

ureq markets itself as a lightweight blocking HTTP client which might be
a good choice for rust-gamedig at the moment. However the main reason
for changing to ureq is that it allows setting a "resolver" function
which overrides the IP address to connect to. This is useful because it
allows us to pass a URL with the desired hostname without the HTTP
library doing an extra DNS lookup (this allows HTTPS to work when we
specify the exact IP and port to connect to external to the URL).

Other changes in this commit are:
- Feature gated things that depend on serde: this means that the eco
  game won't be available if the library is compiled without serde
- Added the TLS feature to enable TLS support in the HTTP library
- Added HTTPSettings to set the protocol (HTTP/HTTPS) and the hostname
- Setting a user-agent string on HTTP requests (allows the server to see
  what program is being used to query them)
- Store the address as a parsed Url so we don't re-parse it on every
  request
- Add a method to POST JSON data and parse response
- Renamed the request() method to get_json() in anticipation of a future
  method that will send a GET request and handle the raw bytes instead
  of using serde
- Improved documentation

* eco: Add generic impls

* eco: fixes

* http: Add headers to HttpSettings and rename from HTTPSettings

* eco: Add extra request settings

* http: Add support for querying raw bytes

* http: Add unit-tests

* http: Rename HttpProtocol

* crate: Make serde dependency non-optional

The serde feature now only enable serde derivations for our types that
don't need it for the library to function.

* http: Add helper for creating HttpClients to query APIs

Adds the from_url helper that should make working with master server web
APIs easier.

* Add/Update badge

* crate: Require games feature for eco example

* docs: Update changelog

---------

Co-authored-by: Douile <douile@douile.com>
2024-02-10 18:26:49 +00:00
Cain
2a65c39cb6 chore: update changelog 2024-02-09 03:26:40 +00:00
Cain
e86e80522b
feat: Add packet capture functionality and many more CLI improvements (#182) 2024-02-07 22:31:31 +00:00
Cain
7369dbab19 refactor: use slices where possable in pcap 2024-02-07 16:10:04 +00:00
Cain
730c938ad2 chore: format 2024-02-07 15:30:20 +00:00
Cain
7b37e71221 chore: remove unused else 2024-02-07 15:29:13 +00:00
Cain
9a2b953fff refactor: capture socket and move to capture dir 2024-02-07 15:28:41 +00:00
Cain
c34392a3da refactor: pcap packet buffer size 2024-02-07 15:26:17 +00:00
dependabot[bot]
80129ce012
chore(deps): bump emibcn/badge-action in /.github/workflows (#187)
Bumps [emibcn/badge-action](https://github.com/emibcn/badge-action) from 2.0.2 to 2.0.3.
- [Release notes](https://github.com/emibcn/badge-action/releases)
- [Commits](https://github.com/emibcn/badge-action/compare/v2.0.2...v2.0.3)

---
updated-dependencies:
- dependency-name: emibcn/badge-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-07 16:22:45 +02:00
dependabot[bot]
ca61c1c0b0
chore(deps): bump dorny/paths-filter from 2 to 3 in /.github/workflows (#185)
Bumps [dorny/paths-filter](https://github.com/dorny/paths-filter) from 2 to 3.
- [Release notes](https://github.com/dorny/paths-filter/releases)
- [Changelog](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md)
- [Commits](https://github.com/dorny/paths-filter/compare/v2...v3)

---
updated-dependencies:
- dependency-name: dorny/paths-filter
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-06 19:53:27 +02:00
Tom
fd497764f8
feat: Improve doc-comments for ErrorKind (#186)
* Improve doc-comments for ErrorKind

* error: Improve ErrorKind::BadGame doc comment

* error: fmt ErrorKind doc comments
2024-02-06 17:06:54 +02:00
GitHub Action
175dcf6aa6 Add/Update badge 2024-02-06 03:34:59 +00:00
Cain
48aa5115c0 fix: add xml utf8 and semver declaration 2024-02-02 21:10:41 +00:00
Cain
4675b24ff3 refactor: better xml support and error prop 2024-02-02 20:57:07 +00:00
Cain
eb24b8ec3d
Update license (#179)
* chore: change year

* chore: update LICENSE.md
2024-02-02 10:39:02 +02:00
Cain
744230455c revert: lib default feat to exclude packet capture 2024-02-02 02:34:02 +00:00
Cain
5310200181 fix: cli xml feat not having json dep 2024-02-02 02:01:25 +00:00
Cain
422cb57efa chore: update cli default features 2024-02-02 02:00:15 +00:00
Cain
df51521a79 fix: remove bug panic due to xml protocol format now being supported 2024-02-02 01:58:48 +00:00
Cain
5365845bb5 fix: cli xml LastElementNameNotAvailable error case 2024-02-02 01:44:44 +00:00
Cain
8f381f733c chore: change cli header year 2024-02-02 00:45:17 +00:00
GitHub Action
76604ac3fc Add/Update badge 2024-01-30 03:37:04 +00:00
Douile
bba9f5f11b
cli: Improve capture help string 2024-01-22 22:02:34 +00:00
GitHub Action
144d7ca03d Add/Update badge 2024-01-22 11:36:43 +00:00
Tom
89ed19f089
feat(protocols): Add more control over gathering additional information (#180)
* protocols: Add more control over gathering additional information

Adds GatherToggle which allows choosing the behaviour for how the query
handles fetching additional information. The choices are:
- DontGather - Don't attempt to fetch information
- AttemptGather - Try to fetch the information but ignore errors
- Required - Try to fetch information and fail if it errors

A handy macro was also added to utils to dispatch additional queries
based on a GatherToggle value.

* Add/Update badge

* protocols: Improve GatherToggle enum names

Co-Authored-By: Cain <75994858+cainthebest@users.noreply.github.com>
Co-Authored-By: CosminPerRam <cosmin.p@live.com>

* Add/Update badge

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Cain <75994858+cainthebest@users.noreply.github.com>
Co-authored-by: CosminPerRam <cosmin.p@live.com>
2024-01-22 13:36:17 +02:00
GitHub Action
6d0c25d6ea Add/Update badge 2024-01-19 18:43:30 +00:00
Tom
f922270c60
ci: Only update the node badge on the main branch (#183) 2024-01-19 20:43:02 +02:00
Cain
2deb1df4ae impl: cli release workflow (x86_64, win, dar, linux, wasm32-wasi) 2024-01-19 04:30:20 +00:00
Cain
5dff511e6f refactor: clippy fix + docs 2024-01-18 18:59:59 +00:00
Cain
b1e42f9023 refactor: clippy fixs 2024-01-18 18:59:19 +00:00
Cain
bedd277027 fix: variant Action::Query does not have this field 2024-01-18 18:37:31 +00:00
Cain
e615c63ed2 fix: cli unused import + undeclared crate 2024-01-18 18:18:11 +00:00
Cain
3eb20b9deb fix: mindustry leak crate-private type 2024-01-18 18:13:42 +00:00
Cain
962c856418 fix: packet support for msrv 2024-01-18 18:08:27 +00:00
Cain
b49525543d chore: format 2024-01-18 17:56:58 +00:00
Cain
1991c9f5eb chore: add missing cli fn docs 2024-01-18 17:48:11 +00:00
Tom
32dd486632
fix: various crate/clippy/ci changes (#181)
* crate: Set default binary to gamedig-cli

* crate: Fix bad no-default-features option on gamedig-id dependency

* ci: Run tests when Cargo.toml changes

* id-tests: Clippy fixes
2024-01-18 15:34:05 +02:00
Cain
36d957ceb4
Merge branch 'main' into feat/rootless-capture 2024-01-18 01:56:14 +00:00
Cain
15004f3dae
chore: optimize further 2024-01-17 23:44:47 +00:00
Cain
89d4ddeac7
fix: bson output 2024-01-17 23:44:41 +00:00
Cain
c30f28741f
impl: tcp fin 2024-01-17 23:44:36 +00:00
Cain
61ecbab312
refactor: clean up and add more features 2024-01-17 23:44:30 +00:00
Cain
6cf6800bff
chore: add source link 2024-01-17 23:44:22 +00:00
Cain
6aee5ebb76
chore: format 2024-01-17 23:44:16 +00:00
Cain
0543cabce2
refactor: clean up 2024-01-17 23:44:11 +00:00
Cain
3d47180e85
refactor: backport pcap impl 2024-01-17 23:44:05 +00:00
Cain
49096e46bb
merge: Douile fork + local (broken af) 2024-01-17 23:43:53 +00:00
GitHub Action
0f9bada4f3 Add/Update badge 2024-01-17 13:54:10 +00:00
Tom
07de5168f4
Add support for Mindustry (#178)
* buffer: Add UTF8LengthPrefixed string decoder

* games: Use expression for default port

This allows us to refer to constants for the default ports if we want to
(literals will still work).

* games: Add support for mindustry
2024-01-17 13:53:40 +00:00
GitHub Action
ba92466ae1 Add/Update badge 2024-01-10 23:48:39 +00:00
CosminPerRam
a3bc8b79e5 feat(games): add zombie panic: source support 2024-01-11 01:48:05 +02:00
Tom
b248a7661e
feat: Move ID tests into their own crate with a CLI (#177)
* fix: ID tests not in correct directory

* refactor: Move game-id test logic into its own crate

* id-tests: Add CLI that reads JSON input

* id-tests: Update crate docs

* Remove node ID test

* id-tests: Don't try to parse unneeded info

* id-tests: Enable cli feature by default
2024-01-11 01:31:02 +02:00
CosminPerRam
88bf996a5e Merge branch 'main' of github.com:gamedig/rust-gamedig 2024-01-07 23:44:40 +02:00
CosminPerRam
94102d0d7b feat(games): add base defense support 2024-01-07 23:44:35 +02:00
GitHub Action
12a6c2af58 Add/Update badge 2024-01-07 21:35:12 +00:00
CosminPerRam
c71e783e1e fix(crate): formatting 2024-01-07 23:34:46 +02:00
CosminPerRam
109a3db13e feat(games): add americas army proving grounds support 2024-01-07 23:34:26 +02:00
CosminPerRam
90b038eed0 Merge branch 'main' of github.com:gamedig/rust-gamedig 2024-01-07 23:22:27 +02:00
CosminPerRam
1333655d53 fix(games): atlas definitions steam app id 2024-01-07 23:22:17 +02:00
GitHub Action
d9c0a63e8c Add/Update badge 2024-01-07 21:16:08 +00:00
CosminPerRam
ae9a38907f feat(games): add atlas support 2024-01-07 23:15:35 +02:00
Tom
bd3727d7fe
chore: Tidy up some out of place types (#160)
* tidy: Move TimeoutSettings out of protocol types

* tidy: Move game type to a types file

* tidy: Move generic query functions to own file

* tidy: Move ExtraRequestSettings to game types

* tidy: Move generic query functions into games module

* Revert "tidy: Move ExtraRequestSettings to game types"

This reverts commit aa0d23fc2acc12d68f03265a9caff6f98bf25054.

* Revert "tidy: Move TimeoutSettings out of protocol types"

This reverts commit 7ee10711ed11f17cc06565d4cc9102040d53a319.

* tidy: Re-export types needed for query at the root

* Add/Update badge

---------

Co-authored-by: GitHub Action <action@github.com>
2024-01-02 00:17:43 +02:00
GitHub Action
483d728ac8 Add/Update badge 2023-12-30 15:32:01 +00:00
CosminPerRam
a7ee331dc3 feat: add Rising World support 2023-12-30 17:31:22 +02:00
Cain
079e9877ba
fix: Remove unused manifest key (#174) 2023-12-24 11:12:54 +00:00
Cain
0e241056bf
fix: remove unused manifest key in cargo.toml 2023-12-24 11:01:08 +00:00
Tom
87ed02420e
feat: Add best effort test to validate game ID rules (#111)
* [Test] Add best effort test to validate game ID rules

An attempt to implement the rules specified in #108 as a programmatic
test.

* [Test] Refactor ID rules to check if a mod exists after - in game name

This allows fivem to pass the check following rule 8, but could also
cause a false pass in some cases.

* [Test] Add unit tests for ID rule checker

Adds unit tests based on the examples in CONTRIBUTING.md to confirm in
those cases we would allow ID to pass. However these tests don't check
any error cases.

* test/id: Correctly extract protocol names

* games/defs: Fix unreal tournament IDs

* tests: Require game definitions to run ID tests

* tests: Improve comments on ID tests

* tests/id: Combine - seperated numbers

* games/defs: Fix darkest hour ID

* Add/Update badge

---------

Co-authored-by: GitHub Action <action@github.com>
2023-12-19 22:20:19 +01:00
CosminPerRam
10169c9107
chore: extract ffow into multiple files (#173)
* chore: extract ffow into multiple files

* fix: serde uses not being conditional
2023-12-19 22:04:45 +01:00
CosminPerRam
bdcf64facf
chore: extract the ship into multiple files (#172)
* chore: extract the ship into multiple files

* fix: actual the ship reference link

* fix: revert last commit and replace in ts
2023-12-19 20:58:15 +01:00
CosminPerRam
99b0269ec2
chore: extract jc2m into multiple files (#171)
* chore: extract jc2m into multiple files

* docs: add jc2m fields public
2023-12-15 20:19:15 +01:00
CosminPerRam
8c52ca6ad3 chore: remove redundant closure 2023-12-11 04:01:41 +02:00
CosminPerRam
44abf6ec71 chore: use Option::map_or_else instead of an if let/else 2023-12-11 04:00:31 +02:00
CosminPerRam
a4bc430868 perf: use of or followed by a function call 2023-12-11 03:58:40 +02:00
CosminPerRam
0aa498b30b chore: simplify condition to use equals operator 2023-12-11 03:56:14 +02:00
CosminPerRam
f746fad157 perf: use of ok_or followed by a function call 2023-12-11 03:54:04 +02:00
CosminPerRam
9c3e6cb51f perf: use of ok_or followed by a function call 2023-12-11 03:53:18 +02:00
CosminPerRam
e1bffd2045 pref: apply const to applicable functions 2023-12-11 03:49:18 +02:00
CosminPerRam
21205fc3cb fix: also add Eq to savage2::Response 2023-12-11 03:47:31 +02:00
CosminPerRam
486abbd9f7 chore: use Self where possible 2023-12-11 03:45:54 +02:00
CosminPerRam
f431508418 chore: use map_or_else instead of if let Some() ... else 2023-12-11 03:41:41 +02:00
CosminPerRam
5d0834ac78 chore: run rustfmt 2023-12-11 03:34:38 +02:00
CosminPerRam
731818ffb1 chore: fixed some long literal lacking separators 2023-12-11 03:34:05 +02:00
CosminPerRam
f1094e0e68 chore: replace the closure with the method itself 2023-12-11 03:31:57 +02:00
CosminPerRam
2836536842 perf: dereference &&str to use specialized ToString implementation 2023-12-11 03:31:18 +02:00
CosminPerRam
81e028e1a0 perf: dereference &&str to use specialized ToString implementation 2023-12-11 03:29:07 +02:00
CosminPerRam
177d22e4b2 chore: instead of .map().unwrap_or_else() use .map_or_else() 2023-12-11 03:26:55 +02:00
CosminPerRam
febba25a91 chore: instead of .map().unwrap_or_else() use .map_or_else() 2023-12-11 03:25:29 +02:00
CosminPerRam
8b4f6083f1 chore: replace ''.to_string with String::new 2023-12-11 03:21:54 +02:00
CosminPerRam
1dc3c6dade chore: replace the closure with the method itself 2023-12-11 03:19:51 +02:00
CosminPerRam
e0cc2a2420 fix: readme examples folder returning not found since monorepo merge 2023-12-10 23:32:12 +02:00
CosminPerRam
35c2aec19b Merge branch 'main' of github.com:gamedig/rust-gamedig 2023-12-10 19:46:21 +02:00
CosminPerRam
bc2b69d183 fix: remove unwrapping in an assert 2023-12-10 19:46:12 +02:00
CosminPerRam
55f498d45a fix: add jc2m in protocols.md 2023-12-10 19:43:32 +02:00
CosminPerRam
dd204936f0 chore: fix unreal 2 entry in protocols.md 2023-12-10 19:41:28 +02:00
GitHub Action
fb6f22b801 Add/Update badge 2023-12-10 17:39:46 +00:00
CosminPerRam
af8e1e9b1a
feat: add savage 2 support (#169)
* feat: savage 2 support

* fix: add savage 2 to definitions

* chore: run rustfmt

* fix: config serde use

* fix: remove needless borrow

* docs: add savage 2 to protocols.md
2023-12-10 19:39:26 +02:00
Tom
dd037daa04
Revert "Bump actions/labeler from 4 to 5 in /.github/workflows (#165)" (#167)
This reverts commit 04da29f2a6.
2023-12-07 22:51:45 +02:00
dependabot[bot]
04da29f2a6
Bump actions/labeler from 4 to 5 in /.github/workflows (#165)
Bumps [actions/labeler](https://github.com/actions/labeler) from 4 to 5.
- [Release notes](https://github.com/actions/labeler/releases)
- [Commits](https://github.com/actions/labeler/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/labeler
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-05 18:03:27 +02:00
CosminPerRam
45f05aec13
feat: add Squad support (#164)
* feat: add Squad support

* Add/Update badge

---------

Co-authored-by: GitHub Action <action@github.com>
2023-12-03 20:59:46 +02:00
CosminPerRam
decff82318
feat: add Post Scriptum support (#163)
* feat: add post scriptum support

* Add/Update badge

---------

Co-authored-by: GitHub Action <action@github.com>
2023-12-03 20:51:43 +02:00
Douile
8d17ca4e48
cli: Add option to create a packet capture 2023-11-26 23:18:51 +00:00
Douile
abbcae618f
capture: Add packet capture infrastructure 2023-11-26 23:18:51 +00:00
Douile
3b1edd8e3d
socket: Add method to get local address 2023-11-26 23:02:17 +00:00
Tom
7510fe3de0
Various improvements for the CLI (#159)
* cli: Do DNS lookup if host is not an IP address

* cli: Add option to output as JSON

* cli: Pass hostname to ExtraRequestSettings if it isn't an IP

* cli: Add help docs to all arguments

* cli: Add options for all extra request settings

* cli: Use a CLI only error for DNS

* cli: Add option to set timeout settings

* docs: Update CHANGELOG

* cli: Add default values to TimeoutSettings

* cli: Refactor finding game definition into its own function

Co-Authored-By: Cain <75994858+cainthebest@users.noreply.github.com>

* cli: Refactor IP resolution into its own set of functions

Co-Authored-By: Cain <75994858+cainthebest@users.noreply.github.com>

* cli: Refactor output formatting into its own functions

Co-Authored-By: Cain <75994858+cainthebest@users.noreply.github.com>

* cli: Improve doc comments for CLI args and derive Debug

Co-Authored-By: Cain <75994858+cainthebest@users.noreply.github.com>

* protocols: Derive Serialize for versioned generic responses

This allows for serializing the output of as_original(). We cannot also
derive Deserialize here because the enums use references to the inner
types, which is unavoidable in the current implementation because
as_original() takes a reference to self.

* cli: Add the output mode options

This allows selected whether to use CommonResponse or the original
response struct when outputting.

* cli: Fix ExtraRequestSettings docs showing up in help output

* cli: Add help headings for timeouts and extra request settings

---------

Co-authored-by: Cain <75994858+cainthebest@users.noreply.github.com>
2023-11-26 22:59:59 +00:00
Tom
b3a29b15b1
[CI] Improvement and fixes (#161)
* protocols: Fix building without the "games" feature

* crate/lib: Add required features for examples

This prevents cargo from running the examples if the required features
aren't enabled.

* ci: Run if ANY Cargo.toml files are changed

* ci: Make sure to run unit tests

* ci: Separate checks for library and CLI

* ci: Add slightly better comments

* ci: Only run deeper tests for CLI or LIB when their files were changed

* ci: Improve act arguments for testing actions locally

* ci: Fix pre-commit not running tests

* ci: Only update shared cache after the initial build

* ci: Make sure that rustup downloads get cached

* tidy: Clean up file formatting

* ci: Fix issue with audit
2023-11-25 00:34:26 +02:00
Tom
0c7dbe76d7
ci: Update node coverage badge to work without games.txt (#156)
* ci: Update node coverage badge to work without games.txt

Fixes #155 using a new node script that should be more robust than the
old shell script.

* ci: Change badge cron time to less common time

By doing at a less common time we should reduce action time usage as
the runners will be under less load.

* Add/Update badge

* ci: Improve node-badge script

Co-Authored-By: CosminPerRam <cosmin.p@live.com>

* ci: Run node badge action if script changes

* ci: Update node badge path for workspaces

* ci: Revert loading games from index in node badge script

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: CosminPerRam <cosmin.p@live.com>
2023-11-23 11:48:24 +02:00
Tom
3f1164ef5d
docs: Add info about commits to CONTRIBUTING (#157)
* docs: Add info about commits to CONTRIBUTING

* docs: Improve commits wording
2023-11-22 13:48:01 +02:00
Tom
e3bdbc2a41
feat: Add connect timeout to TimeoutSettings (#158)
Because TcpSocket connects in Socket::new TimeoutSettings are now
required for Socket::new. Since we already have TimeoutSettings there
Sockets are now expected to apply timeout settings in Socket::new.
2023-11-22 12:40:22 +02:00
CosminPerRam
7416d54b14
chore: Move minecraft protocol to games. (#153)
* chore: initial move and refactor of minecraft

* fix: glob re-exports

* fix: failing example sample

* docs: update changelog to note the mc protocol implementation to games

* docs: add back the reference query standard reference
2023-11-18 19:44:09 +02:00
CosminPerRam
bd73b657c7
fix: minecraft id naming inconsistencies (#152)
* fix: minecraft id naming inconsistencies

* fix: minecraft legacy beta 1.8 being wrongly id named in definitions

* docs: Update CHANGELOG to document minecraft legacy renames

* docs: Update CHANGELOG to note removal of legacy versions game names being prefixed with 'v'
2023-11-16 15:18:15 +02:00
CosminPerRam
13f1c2bf35 docs: Add the initial addition of the CLI notice to CHANGELOG. 2023-11-09 01:21:29 +02:00
CosminPerRam
0d27882150
feat(unreal2): Add password rule check (#149)
* feat: add initial password checking

* feat: add password string check and made the field a bool

* chore: fix formatting

* feat: add password to has_password common response
2023-11-09 01:18:52 +02:00
dependabot[bot]
f01cac8fed
Bump actions/checkout from 3 to 4 in /.github/workflows (#151)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-09 01:15:38 +02:00
Cain
c9c8e700cb
chore: add dependabot config (#150) 2023-11-08 23:56:58 +02:00
Cain
89222b1f44
Feat: merge gamedig-cli partially & monorepo conversion
Related issue: #125
2023-11-08 21:31:02 +00:00
Cain
a11ca7f9aa chore: add release opts 2023-11-08 21:11:25 +00:00
Cain
338df9144c chore: update cargo.toml 2023-11-08 20:44:31 +00:00
Cain
963040fb84 Merge branch 'main' into feat/cli 2023-11-08 20:42:07 +00:00
Cain
92ad618723 chore: remove un-needed fmt skip 2023-11-08 20:22:04 +00:00
Douile
1d7cb31bc4
protocols/unreal2: Only compile game impl macros when needed
Unreal2 hadn't been added when #144 was merged so this got missed out.
2023-10-30 13:50:17 +00:00
Tom
529abe9d76
feat: Add the unreal2 protocol (#124)
* WIP: Add unreal2 protocol

* Add/Update badge

* protocols/unreal2: Update doc comments and TODOs

* protocols/unreal2: Don't pre-allocate as many bot players

* protocols/unreal2: Use "encoding-rs" for decoding unreal2 strings

* Add/Update badge

* protocols/unreal2: Add constants for player pre-allocation.

Also improve some doc comments and update PACKET_SIZE.

* protocols/unreal2: Early break when enough players have been parsed

Add a fast-path to avoid waiting for packet timeout when we have parsed
as many players as specified in the server info packet.

* protocols/unreal2: Use HashSet to store mutators

* protocols/unreal2: Handle server sending multiple values for a rule

* protocols/unreal2: Add GatheringSettings to control what to query

GatheringSettings allows skipping querying rules and/or players which
can make the query return much faster. This also required moving each
individual query into its own helper.

* protocols/unreal2: Add more derives to types

* protocols/unreal2: Simplify ServerInfo::parse()

Co-Authored-By: CosminPerRam <cosmin.p@live.com>

* Docs: Add unreal2 protocol documentation

I used a website to generate the markdown RESPONSES table, the save file
from this website is included to make updating the table easier in the
future.

https://www.tablesgenerator.com/markdown_tables

* Add/Update badge

* protocols/unreal2: Use the correct encoding for UCS2 strings

* Docs: Remove unnecessary TGN file

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: CosminPerRam <cosmin.p@live.com>
2023-10-30 13:37:15 +02:00
Tom
5c1568251a
feat: Only compile game impl macros when they are needed (#144)
These macros are only required when compiling the code gated behind the
games feature, they are unused if that feature is not and are also crate
only.
2023-10-30 11:30:16 +02:00
CosminPerRam
4bbe7e1780 fix: run proper formatting on valve_protocol_query example 2023-10-29 14:49:49 +02:00
CosminPerRam
a3740c5424 fix: remove double similar use on minecraft mod 2023-10-29 14:44:21 +02:00
CosminPerRam
adb2109aea feat: add valve protocol query example 2023-10-29 14:43:35 +02:00
CosminPerRam
f11a50a415 feat: Add Conan Exiles support. 2023-10-29 00:10:47 +03:00
CosminPerRam
1145a064a9 feat: Add The Front support. 2023-10-29 00:00:48 +03:00
CosminPerRam
e3dd7cd1c7
Fix some game ids and clarify rules 6 and 7. (#143)
* Fix wrongly named game ids of l4d, l4d2 and q3a

* Specify how edition/protocol names are appended to games, rule 7

* Clarify rule 6 regarding numbers

* Add/Update badge

* Fix rule 7 misswording

* Fix 7 Days to Die

* List id changes in the changelog

* Forgot to fix test

* Apply Douile suggestion regarding rule 6

Co-authored-by: Tom <25043847+Douile@users.noreply.github.com>

* Fix typo on rule 6 and wrap text

* Clarify further rule 7

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Tom <25043847+Douile@users.noreply.github.com>
2023-10-24 22:39:36 +03:00
CosminPerRam
2cc9e56168
feat: Remove SteamApp (#142)
* Remove the SteamApp enum.

* Further removal

* Replace SteamApp with Engine on game! usage

* Add removal of SteamApp in changelog

* docs: Update reference of SteamApp to Engine

* Update the docs of the Engine type to be more descriptive.
2023-10-21 23:13:14 +03:00
CosminPerRam
501524b0da
feat: Add GatheringSettings on Valve macros (#128)
* Initial macro modification for gathering settings

* Initial Valheim support

* Remove unused use

* Fix macro

Thanks bunch @Douile

* docs: Add Valheim to CHANGELOG and GAMES

* Add commentary regarding gathering settings comment generation

* Add GatheringSettings to game!

* Remove unused stuff

* Fix tests and add comment regarding the game argument
2023-10-19 23:15:10 +03:00
GitHub Action
6b92e883ef Add/Update badge 2023-10-17 23:31:19 +00:00
Douile
9644163c8c
[fmt] Run rustfmt on crates/cli/src/error.rs 2023-10-18 00:29:21 +01:00
Douile
d34d615784
[CI] Update CI to work with workspaces 2023-10-18 00:29:21 +01:00
Tom
1ca6e6e85c
[Clippy] Remove .clone() from timeout_settings as it is now Copy (#127)
These clones are unnecessary when the type implements Copy and this was
generating clippy warnings.
2023-10-17 19:26:03 +03:00
Douile
8a88e826fa
[Repo] Link to CONTRIBUTING.md in README 2023-10-17 13:37:48 +01:00
Cain
7d4649b6f5 refactor: cli to work next to lib 2023-10-17 00:05:32 +01:00
GitHub Action
6084c56d4f Add/Update badge 2023-10-16 22:21:11 +00:00
Cain
80f6b87991 refator: copy cli into mono 2023-10-16 23:20:47 +01:00
CosminPerRam
66ae3c296e [Crate] Bump version to 0.4.1 2023-10-13 00:43:22 +03:00
GitHub Action
ef29ba8eb4 Add/Update badge 2023-10-12 21:40:31 +00:00
CosminPerRam
5b5c41b468 Add Barotrauma support. 2023-10-13 00:40:08 +03:00
Tom
3b9c784e70
Reduce game implementation repetition (#122)
* [Games] Add macro to replace valve game query implementations

This somewhat reduces repeated code (#120), and also adds auto-generated
doc comments to all valve game query functions.

* [Games] Add macro to replace gamespy game query implementations

This somewhat reduces repeated code (#120), and also adds auto-generated
doc comments to all gamespy game query functions.

* [Games] Add macro to replace quake game query implementations

This somewhat reduces repeated code (#120), and also adds auto-generated
doc comments to all quake game query functions.

* [Games] Move all valve game modules into a single file using macros

Vastly reduces the number of files. However does break the game
definition-per-file test, so that was removed.

* [Games] Move all quake game modules into a single file using macros

* [Games] Move all gamespy game modules into a single file using macros

* [Docs] Update CHANGELOG

* [Docs] Improve game query function generation macro documentation

* [Games] Add missed Halo: Combat Evolved to gamespy
2023-10-10 09:26:35 +03:00
CosminPerRam
c8a93357cf
Add Send and Sync on Error Source (#118)
* Add Send and Sync on Error Source and remove 'static

* Add back 'static

* Update the changelog
2023-10-09 19:11:34 +03:00
GitHub Action
b7e1eff9b7 Add/Update badge 2023-10-09 01:12:54 +00:00
CosminPerRam
b584e11336 Remove serde derives Generic Player and Response 2023-10-08 22:26:25 +03:00
CosminPerRam
1e083c2df7 [Generic] Add missing derives to types 2023-10-08 19:45:24 +03:00
CosminPerRam
7164ab5f64 [Protocol] Add derives to RequestSettings and new_just_hostname function 2023-10-08 00:37:11 +03:00
CosminPerRam
2106e965e4 [Crate] Bump version to 0.4.0 2023-10-07 19:27:43 +03:00
CosminPerRam
53fe402f05 [Repo] Fix typo in README. 2023-10-07 19:26:31 +03:00
CosminPerRam
a3800f3ba4 [Crate] Update byteorder from 1.4 to 1.5 2023-10-07 18:58:21 +03:00
CosminPerRam
40d4be2ceb Remove CosminPerRam from authors as he is not the sole maintainer anymore 2023-10-07 18:36:32 +03:00
CosminPerRam
311a5425a8
Remove minecraftping from game definitions as its useless (#117)
* Remove minecraftping from game definitions as its useless

* Update minecraftpocket name
2023-10-07 18:06:18 +03:00
CosminPerRam
5280ecb3c6
[Game] Apply new id naming (#114)
* Apply new id naming

* Fix failing CI on all features

* Update changelog

* Rename tf2 example to teamfortress2

* Fix typo in steamapp game names

* Rename minecraft legacy versions

* Apply CI node badge fix by Douile

* Add/Update badge

---------

Co-authored-by: GitHub Action <action@github.com>
2023-10-06 17:20:30 +03:00
Tom
b4c61781fb
Specify the branch name when pushing node badge commits (#116)
* [CI] Specify the branch name when pushing node badge commits

Fixes #115

* testing: remove games to update badge

* Add/Update badge

* Revert "testing: remove games to update badge"

This reverts commit 8256176839e996810568f5104a7c6a19ab482ab0.

* Add/Update badge

---------

Co-authored-by: GitHub Action <action@github.com>
2023-10-05 11:46:35 +03:00
CosminPerRam
9db873e774
Fix rule 3 example not following rule 2 (#113) 2023-10-04 11:20:03 +03:00
CosminPerRam
e7567c631e
[Repo] Add CONTRIBUTING.md. (#108)
* Add initial CONTRIBUTING.md file

* Update CONTRIBUTING.md

Co-authored-by: Tom <25043847+Douile@users.noreply.github.com>

* Specify to look whether there is an existing relevant issue first

* Specify that ids are lowercased only

* Fix tyope.

* Specify special case with multiple editions/versions

* Clarify an id already existing and fix typo

* Add new rule to specify what to do about names that being with numbers

* Simplify numbering rules

* Simplify numbering rules

* Specify on #1 unless #4 applies

* Specify naming for game names

* Add rule regarding mods that add ability for queries

* Remake rule 7 to include protocol naming

* Specify lowercase aplhanumeric

---------

Co-authored-by: Tom <25043847+Douile@users.noreply.github.com>
2023-10-02 23:38:31 +03:00
CosminPerRam
05eb902891
[Examples] Remove master_querant example due to being obsolete (#109) 2023-10-02 20:10:55 +03:00
CosminPerRam
9107bf5ef2 Update CHANGELOG.md to mention contributors 2023-10-01 12:34:10 +03:00
Tom
efc1828b29
[Protocol] Make gamespy1 parsing less likely to fail (#107)
* [Protocol] Gamespy1 don't skip "playername" field (#88)

* [Protocol] Gamespy1 make more player fields optional (#88)

These fields seem to be missing from bf1942 queries so make them
optional.
2023-09-27 17:07:42 +03:00
CosminPerRam
5bd609af72 [Crate] Update README to have a more title-like project name 2023-09-27 13:59:44 +03:00
Tom
c3281be419
[Protocol] Retry failed requests (#95)
* Add retry count to TimeoutSettings

This can be used to specify how many times to re-send requests that
fail. The default value is "1" so the if the first request fails, 1 more
attempt will be made.

* Add retries to valve queries

* [Protocol] &Optional<TimeoutSettings> add get_retries_or_default

Allow fetching the number of retries or the default retries value from a
borrowed optional TimeoutSettings.

* [Protocol] Add retries to minecraft protocol

* [Protocol] Add retries to quake

* [Protocol] Add retries to gamespy

* [Protocol] Update TimeoutSettings docs, and change default retries to 0

* Remove logging from retry_on_timeout

* [Protocol] TimeoutSettings make retries non-optional

* [Protocol] Move retry logic into lower level query functions

Retries are now implemented as wrappers on the single function that
would need to be retried on timeout.

In order to avoid cloning of TimeoutSettings, Socket::apply_timeouts()
was changed to accept a borrowed TimeoutSettings. And extra helpers were
added to the TimeoutSettings impl to reduce repetition.

* [Examples] Add retries to the generic example

* Also retry on PacketSend error

Sending packets could also timeout and until error_generic_member_access
is stable we have no way of determining the type of the underlying
`std::error::Error`.

* Add retry unit tests

* [Docs] Update changelog
2023-09-25 22:12:54 +03:00
GitHub Action
3784d25774 Add/Update badge 2023-09-22 21:20:17 +00:00
Tom
52750fba76
[Games] Update game definitions to match node-gamedig names (#100)
* [Games] Update game definitions to match node-gamedig names

* Update game file names to match definitions

* [Games] rename minecraft definitions to better match node

* [Games] Add test that all game files match definition name

* Update SteamApp to match node names

* [Games] Update the forest to match node-GameDig#367

* Clippy fix in games test

* [Docs] Update GAMES.md with new names

* [DOCS] Update changelog
2023-09-23 00:20:01 +03:00
Tom
14c3f4525b
[CI] Add node comparison badge (#99)
* [CI] Add node comparison badge

* Add/Update badge

---------

Co-authored-by: GitHub Action <action@github.com>
2023-09-21 23:51:10 +03:00
CosminPerRam
6b1e787cd2
[CI] Fix misc stuff with labeler CI. (#103)
* [CI] Fix games tag with the s

* [CI] Add labeler.yml as file for CI tag

* [CI] Also use checkout on triage
2023-09-21 23:37:11 +03:00
CosminPerRam
a8489e4353 [Crate] Remove license-file field due to cargo warning that only one is needed. 2023-09-21 23:18:25 +03:00
Tom
1a60a0496f
[CI] Only run build checks when a rust related file has changed (#102) 2023-09-21 23:03:12 +03:00
CosminPerRam
1c9f2dc0a8
[CI] Fix labeler permission on guest prs (#101)
* [CI] Fix permissions on guest prs

* Revert last commit

* Revert last commit

* Don't specify branch name in pr target

* Properly space on line

* Specify branch

* Add quotes to labeler points

* Remove the quotes on files in labeler.yml
2023-09-21 22:31:48 +03:00
CosminPerRam
2eb1d12b3d
[CI] Add github PR labeler (#97)
* Add labeler

* Run on prs

* Add move labeling
2023-09-17 21:59:19 +03:00
CosminPerRam
8468c2b821 [Crate] Update Cargo.toml to add license-file and categories fields 2023-09-11 14:57:49 +03:00
CosminPerRam
6179532065 [Crate] Changeup a doc comment 2023-09-10 23:27:06 +03:00
CosminPerRam
cb0486bded [Crate] Solve Cargo Doc redundant explicit link target 2023-09-10 23:14:13 +03:00
CosminPerRam
995ab23b51 [Crate] Separate error code into different files. 2023-09-10 23:12:18 +03:00
CosminPerRam
edbb0e6cf5 [Protocol] Valve: Rename compressed field to decompressed 2023-09-10 22:32:02 +03:00
CosminPerRam
b418319e01 [Protocol] GameSpy 3: Removed instance on unchecked .unwrap 2023-09-06 14:17:37 +03:00
CosminPerRam
527f8f6369 [Protocol] Valve: change up compressed packages structure to use Options 2023-09-06 13:46:52 +03:00
CosminPerRam
7cfecbfff9 [Game] Remove instances of using unwrap without handling the panics 2023-09-06 13:37:48 +03:00
CosminPerRam
d97bd68ada [Crate] Update CHANGELOG to move some stuff to breaking 2023-09-05 01:41:46 +03:00
Tom
6c1fdb1159
[Generic] Add struct for all extra request settings (#93)
* [Generic] Add struct for all extra request settings

Adds a new struct `ExtraRequestSettings` that contains all possible
extra settings for all protocols, and can be implicitly converted with
`.into()` into each protocol's extra settings type.

This is then used in a new query method
`query_with_timeout_and_extra_settings()` that passes the object on to
the selected protocol.

This also updates the generic example to set some of these generic
settings so that it can be used for certain queries like
"mc.hypixel.net".

* [Minecraft] Add `request_settings` parameter to auto query

This allows generic queries to pass through request settings when using
the `mc` game so that servers like `mc.hypixel.net` will still work when
using auto query.

* Fix generic examples tests (and enable example tests in pre-commit)
2023-09-05 01:13:12 +03:00
CosminPerRam
76a3ac2f78 [Game] Add Creativerse support. 2023-09-03 12:12:14 +03:00
CosminPerRam
89d69c1176 [Crate] Fix Changelog not mentioning rename/retype in protocols block 2023-09-01 22:44:05 +03:00
CosminPerRam
e1568f4019 [Crate] Add RequestSettings to CHANGELOG 2023-09-01 22:41:42 +03:00
CosminPerRam
a56ca45de6
[Protocol] Fix Minecraft Java query not being able to specify a hostname (#91)
* Make initial fix

* Fix imports

* Rename query_address to hostname and add this to the mc example

* Fix master_querant example not compiling

* Add extra safety on converting strings to Minecraft Varint strings

* Add docs to RequestSettings

* Fix formatting
2023-09-01 22:21:08 +03:00
CosminPerRam
211cd5fd5f [Crate] Update LICENSE.md to specify GameDig organization 2023-08-23 21:24:25 +03:00
CosminPerRam
51c7e2383e [Crate] Update LICENSE.md to specify GameDig developers 2023-08-23 21:12:54 +03:00
CosminPerRam
a8fc67412c [Protocol] Fix an instance of unwrapping in The Ship 2023-08-17 23:08:59 +03:00
CosminPerRam
7e028ce97d [Protocol] Remove Minecraft unused function to convert a string to bytes 2023-08-17 22:50:23 +03:00
CosminPerRam
f50c50f0f1 [Protocol] Apply many nursery clippy lints 2023-08-17 22:37:17 +03:00
CosminPerRam
c4097fae78 [Crate] Update CHANGELOG with check_app_id addition and player score being fixed. 2023-08-15 22:05:59 +03:00
CosminPerRam
b35e52f795
[Protocol] Add app id checking to valve protocol in gather settings (#87) 2023-08-15 22:00:20 +03:00
CosminPerRam
071fc367df [Protocol] Add buffer comment to refactor at some point remaining_bytes 2023-08-15 21:36:40 +03:00
CosminPerRam
9d8fb1ba94
[Protocol] Standardize fields (#84)
* [Protocol] Standardize The Ship fields

* [Protocol] Standardize FFOW fields

* [Protocol] Rename Valve's protocol field to protocol_version

* [Protocol] Rename Minecraft's version_protocol field to protocol_version

* [Protocol] Rename Valve's version field to game_version

* [Protocol] Rename Minecraft java version_name to game_version

* [Crate] Reformat RESPONSES.md

* [Protocol] Renamed Minecraft Java players_sample to players

* [Protocol] Rename Quake (1,2,3) version field to game_version

* [Protocol] Rename quake (1 and 2) game_type field to game_mode

* [Protocol] Rename Valve, FFOW, TS game field to game_mode

* [Generics] Rename game field/function to game_mode

* [Protocol] Change players_minimum, _maximum and _bots from those who werent u8 or u32 to u32

* [Protocol] Change instances of player score field type from u32 to i32

* [Crate] Nicer gramar in CHANGELOG

* [Protocol] Apply clippy fixes
2023-08-15 20:44:18 +03:00
CosminPerRam
65c56dc196 [Service] Fix valve master server ip gatherer being broken (again) 2023-08-08 18:43:33 +03:00
CosminPerRam
c10555a9d7 [Service] Fix valve master server ip gatherer being broken 2023-08-08 18:42:43 +03:00
CosminPerRam
b509a6dcc4 [Crate] Convert pub(crate) to pub in buffer as its not exported externally 2023-08-06 21:55:39 +03:00
CosminPerRam
5aa2ce99ec [Crate] Apply strict clippy on buffer 2023-08-06 21:54:19 +03:00
CosminPerRam
4ff50ea711 [Crate] Apply strict clippy suggestions and fix doc line. 2023-08-06 21:50:17 +03:00
CosminPerRam
47547a77bd [Crate] Remove unnecessary path prefix in error code 2023-08-06 21:32:44 +03:00
CosminPerRam
c43cc0438a [Crate] Update CHANGELOG.md 2023-08-06 21:29:40 +03:00
Tom
5e7e010d24
[Crate] Add rich error type (#80)
* Add rich error type with source and backtrace

Adds a rich error type that will take a backtrace and allow capturing
the source of the error. The best way to use this is with the included
helpers that will automatically capture the backtrace and convert the
source error:

```
GDRichError::packet_bad_from_into("Reason packet was bad")
```

* [Crate] Bump MSRV to 1.65.0

This is required for backtraces in rich errors.

* Remove leftover debug logging

* [Errors] Replace variant overloads with single .rich method on kind enum

This overhaul replaces the exhaustive impls of each variant as multiple
methods on the rich error type with a singular .rich() method on the
kind enum. This consumes the variant and converts it to a rich error
with a source (.into() can be used if a source is not needed).

I also took the liberty of replacing all usages with the this new method
as I saw fit (adding various error messages) and converting a few
PacketBad errors to TypeParse errors when they are the result of parse
failing.

* Fix problem with rustdoc

* Remove BadGame's owned string

* Rename GDError to GDErrorKind

* Rename GDRichError to GDError

* Remove error impl from GDErrorKind

* Fix tests not passing

* Use error context everywhere map_err is used

* Improve GDError display formatter

* Add tests for new error type
2023-08-05 12:36:48 +03:00
CosminPerRam
f7b5463073
[Crate] Add issue templates (#81) 2023-08-03 21:36:03 +03:00
Tom
fb447edbc2
[Docs] Add rustdoc checks to CI and tests to pre-commit (#83) 2023-08-02 17:44:42 +03:00
Tom
a4df444c86
[Docs] Improve generic example (#79)
- Allow entering DNS names that can be resolved
- Output a list of games if there is no game provided
2023-07-23 21:37:44 +03:00
CosminPerRam
ada60f2376 [Protocol] Fixed quake 2 bug that version wouldn't always be present 2023-07-22 16:17:52 +03:00
CosminPerRam
a81a6ef968 [Protocol] GS 1 rename Player's frags field to score to be more inline with the other protocols 2023-07-20 19:27:06 +03:00
CosminPerRam
bec0f518b4 [Crate] Bump version to 0.3.0 2023-07-18 15:58:49 +03:00
CosminPerRam
84118d2590 [Crate] Generalize dependencies 2023-07-18 15:52:47 +03:00
CosminPerRam
23669531b6 [Crate] Fix clippy lints 2023-07-18 15:31:35 +03:00
CosminPerRam
ea1360441c [Crate] Update CHANGELOG to add missing added changes. 2023-07-18 15:25:03 +03:00
Cain
66cc39eb26
[Crate] Refactor: Buffer (#62)
* merge: local -> fresh pr

* fix: utf16 bugs + clean up

* docs: buffer

* [feat/buffer] Replaced errors, partial valve protocol ported

* fix: change buffer name + add endian switch

* chore: update module

* refactor: valve_master

* refactor: mc bedrock

* refactor: mc java

* refactor: mc types

* refactor: mc legacy 1.8

* refactor: mc legacy 1.4

* refactor: valve

* refactor: valve types

* refactor: quake

* refactor: mc legacy 1.6

* refactor: gamespy 1

* fix: make switch endian move cursor

* fix: reset cursor on switch

* chore: add switch endian tests

* chore: remove todo comment

* chore: clean up buffer generic types

* refactor: prop len when switching in mc bedrock

* fix: tests and current pos fn

* refactor: ffow

* refactor: jc2mp

* refactor: gs 3

* refactor: gs 2

* fix: mc bedrock prop on move + move data

* fix: mc java lifetime error

* fix: mc legacy 1.6 using pub not pub crate

* fix: quake client lifetime

* fix: quake 2 clippy warning

* fix: valve lifetime issue

* fix: buffer test

* chore: format to keep ci happy

* fix: buffer move_cursor

* fix: quake client

* feat: GameSpy 1 small optimization

* fix: incomplete gamespy 3 fix

* fix: gamespy 3 fix

* fix: minecraft java

* fix: minecraft bedrock

* feat: update the CHANGELOG to mention the buffer rewrite and thank @cainthebest for it

* fix: minecraft legacy 1.6

---------

Co-authored-by: CosminPerRam <cosmin.p@live.com>
2023-07-18 11:46:53 +03:00
Tom
a8342296d6
[CI] Improve checks (#71)
* [CI] Improve checks

- Adds .actrc so CI actions can be tested locally
- Adds testing for variants of features so changes that break feature
  sets like disabling games can be caught
- Adds more of the CI tests to pre-commit

* [CI] Add more feature sets to test

* [CI] Check github actions before comitting

Requires act: https://github.com/nektos/act

* [CI] Add caching between builds

* [CI] Add caching everywhere

* [CI] Add more feature tests

* [CI] Add YAML schemas

* [CI] Disable audit when running locally

This is needed because the audit action requires a GITHUB_TOKEN which we
don't have when running locally.

* [CI] Move clippy to own job and use action that annotates PRs

Using actions-rs/clippy-check means that PRs will problems will
automatically have the relevant lines annotated

* [CI] Don't unnecessarily cache targets for fmt
2023-07-12 23:40:10 +03:00
Tom
fb9d15f0cc
[Crate] Bump rustfmt version to 1.6.0 (#69) 2023-07-10 18:32:17 +03:00
CosminPerRam
bf8c087b94
[CI] Using recommended CI Cargo audit Github action and update serde (#70)
* [CI] Using recommended CI Github action

* [Crate] Update serde dependencies
2023-07-10 17:09:22 +03:00
Tom
e207e8dc95
[Games] Add timeout settings for proprietary games (#67)
Adding query functions with timeout settings to proprietary games allows
the generic query with timeout function to pass the timeout settings
through.

This does change how the pre-existing FFOW query_with_timeout function
worked: it accepted a non-optional timeout settings, this was changed to
optional to be consistent with other query_with_timeout functions and to
move deciding what to do if the user doesn't provide timeout settings to
a more central location.

Closes #64
2023-07-10 16:17:49 +03:00
Tom
f3a792e325
[Crate] Swap no_games, no_services for games, services (#61) 2023-06-27 02:12:57 +03:00
CosminPerRam
0f9a10f2fb [Games] Add Warsow support. 2023-06-27 01:15:04 +03:00
CosminPerRam
58f7ff8aab [Protocol] Fix common quake 2 player score conversion 2023-06-27 01:08:42 +03:00
CosminPerRam
f7e93fd7cd [Protocol] Renamed quake 2's player's frags field to score to be more inline with the other protocols 2023-06-27 01:06:16 +03:00
CosminPerRam
84ebeb8065 [Protocol] Add optional address field to quake 2 player 2023-06-27 00:58:38 +03:00
Tom
8316dac2cc
[Games] Add method to query with timeout options (#60) 2023-06-27 00:50:06 +03:00
CosminPerRam
dd80d6309f [Protocol] Replace some usage of remaining_length with the more appropiate is_remaining_empty 2023-06-27 00:23:19 +03:00
CosminPerRam
8fe521749a [Protocol] Remove bufferer::remaining_data_vec function and replace its usage 2023-06-27 00:18:56 +03:00
CosminPerRam
d61085b1ab [Protocol] Remove bufferer::data_length function and replace its usage 2023-06-27 00:17:33 +03:00
CosminPerRam
c55254aaf6
[Protocol] Add PROPRIETARY to protocol enumeration (#59)
* [Protocol] Add CUSTOM to protocol enumeration

* [Protocol] Rename CUSTOM to PROPRIETARY

* [Protocol] Rename struct to ProprietaryProtocol and do the same thing to generic response

* [Protocol] Revert proprietary change on generic response
2023-06-26 23:52:40 +03:00
Tom
b368877031
[Protocol] Implement generic response with dyn (#56)
* Implement generic response as enum

* First draft of implementing into_common()

* Make common response type generic

* Use macros and generics to reduce repetition

* [Games] Add dynamically dispatched CommonResponse trait

This adds two traits: "CommonResponse", and "CommonPlayer", when the
generic game query function returns a response it returns a pointer to
its original response type that implements "CommonResponse".

Both common traits require that "as_original()" be implemented, this
returns an enum containing a pointer to the original type.

Both traits have a concrete method "as_json()" that returns a struct
containing data fetched from all of its methods as. This struct
implements serde and can hence be serialized as required.

The traits require a few other methods be implemented, those being the
fields that are common across all types. All other methods have a
default None implementation so that each response type only needs to
implement methods for fields that it has.

* [Game] Implement common traits for JCMP2 response

* [Fmt] Run cargo fmt

* Fix doctest failing

* Run cargo fmt
2023-06-25 16:31:23 +03:00
Tom
bf14ecb4a4
[Crate] Add documentation for where versions need to be updated (#58)
VERSIONS.md tells you what the current versions for things like MSRV is
and which files you need to update if you change it.

Also fixes the incorrect:
- MSRV in README.md
- nightly version in .pre-commit-config.yaml
2023-06-25 16:02:25 +03:00
CosminPerRam
9c93e40650
[CI] Pin specific rustfmt version (#57)
* [CI] Specify specific rustfmt version

* [CI] Specify platform in CI for rustfmt

* [CI] Fix cargo fmt running missing nightly version
2023-06-25 15:42:29 +03:00
Tom
89fbd81331
[Crate] Add clippy and rustfmt pre-commit hooks (#50)
These hooks can be installed using [pre-commit], with:
"pre-commit install". Once installed they will prevent you from
committing if the staged code has clippy warnings or hasn't been
formatted. You can skip the pre-commit checks by adding the "-n" flag to
your git commit command.

The hooks also require rustup as the nightly versions of rustfmt and
clippy are used.

[pre-commit]: https://pre-commit.com/
2023-06-25 14:41:20 +03:00
CosminPerRam
31162b6d6e [Crate] Modify GAMES.md to make it more cohesive. 2023-06-19 23:05:41 +03:00
CosminPerRam
c3e2d948e8
[Game] Add JC2MP support. (#54)
* [Game] Add JC2MP support.

* [Game] Add game to changelog and games

* [Games] Add generic support to JC2MP.

* [Game] Add players_maximum and players_online
2023-06-19 22:10:49 +03:00
CosminPerRam
cb9384e474 [Crate] Updated CHANGELOG.md to include generic query 2023-06-19 19:59:06 +03:00
Tom
01b47d54e1
[Crate] Fix response table not showing inner types (#52) 2023-06-16 19:04:34 +03:00
CosminPerRam
7f9b4ca98a [Crate] Update RESPONSES.md 2023-06-16 18:24:44 +03:00
Tom
4b081371f4
[Crate] Add better documentation for generic game implementation (#49) 2023-06-14 23:25:26 +03:00
CosminPerRam
08e00c64e4 [CI] Replace check by clippy 2023-06-14 15:38:08 +03:00
CosminPerRam
6486c1e17b [CI] Reorder steps again and change some steps names 2023-06-13 22:20:31 +03:00
CosminPerRam
3fd3c7aa5b [CI] Add cargo audit to CI 2023-06-13 22:16:14 +03:00
CosminPerRam
8bc05013ee [CI] Resort check and build steps so build is the first 2023-06-13 22:15:10 +03:00
CosminPerRam
a377b76a55 [Games] Generic query add support for GS2 protocol and Halo:CE game 2023-06-13 22:09:51 +03:00
CosminPerRam
e44a680a59 [Example] Add pretty print of receiving struct. 2023-06-13 22:01:51 +03:00
Tom
d853189e06
[Games] Programmatic games by storing information as data (#45)
* Define games as structs

* Create table of response types

* Ensure serde is always included

* Remove server_ prefix in GenericResponse

* Make players online/max non-optional in generic response

* Use already existing minecraft server enum

* Implement ExtraResponses to prevent cloning when creating generic

* Add game definitions

* Add doc comments to generic types

* Include players in gamespy extra responses

* Add custom response types for TheShip and FFOW

* Cargo format differing files

* Final cleanup
2023-06-13 21:49:58 +03:00
CosminPerRam
26ad1f5d19
[Protocol] Add GameSpy 2 support. (#47)
* [Protocol] Add initial files

* [Protocol] Add test to test the request

* [Protocol] Add initial query response type

* [Protocol] Parse teams

* [Protocol] Add players parse and add nice macro

* [Protocol] Add proper derives to structs

* [Protocol] Change to get all informations from one request

* [Protocol] Add Halo: CE support and update CHANGELOG.md

* [Protocol] Remove a .clone usage

* [Protocol] Add todo comment regarding code performance

* [Protocol] Use iterator instead of index range
2023-06-12 19:38:34 +03:00
CosminPerRam
80637f2398 [Crate] Apply clippy fixes 2023-06-11 13:16:22 +03:00
Tom
b95b2abe0f
[Crate] Enforce formatting in CI (#46)
* [CI] Check formatting

* Format all files
2023-06-10 18:15:12 +03:00
CosminPerRam
a6279177bb [Crate] Add MSRV change to CHANGELOG.md 2023-06-07 00:01:56 +03:00
CosminPerRam
bfa2c9826f [Crate] Fix github workflow ci 2023-06-06 23:34:59 +03:00
CosminPerRam
4a8ad7c3dc [Crate] Update Github workflow to confirm MSRV 2023-06-06 23:31:19 +03:00
CosminPerRam
c73334f45d [Crate] Fix msrv and change it to 1.60 2023-06-06 22:58:24 +03:00
CosminPerRam
d1ca19647d [Crate] Bump version to 0.2.3 2023-06-02 01:19:32 +03:00
CosminPerRam
e0830bdae5
[Protocol] Replace IpAddr with SocketAddr in protocols (#44)
* [Crate] Replace IpAddr with SocketAddr in protocols

* [Crate] Remove usage of address.to_string in socket

* [Crate] Update CHANGELOG.md
2023-06-02 01:06:24 +03:00
CosminPerRam
596d15df78 [Protocol] Quake change get response header to str 2023-06-02 00:31:02 +03:00
CosminPerRam
3a9bd77efe [Protocol] Use string literals to better show what the response header bytes mean 2023-06-02 00:29:23 +03:00
CosminPerRam
a0681f4259 [Games] Add Soldier of Fortune 2 support. 2023-05-30 18:53:23 +03:00
CosminPerRam
b3ba7df6d9 [Games] Add Hell Let Loose support. 2023-05-30 16:48:19 +03:00
CosminPerRam
06a2ceeda9 [Games] Add Quake 3 support and change players frags from u16 to i16 2023-05-30 16:23:37 +03:00
CosminPerRam
af5e0d1fbf [Games] Add Quake 1 support. 2023-05-30 16:18:04 +03:00
CosminPerRam
c874b463e3 [Protocol] Fix Quake 1 implementation. 2023-05-30 16:15:08 +03:00
CosminPerRam
f79f2ea2de [Games] Add Quake 2 support. 2023-05-30 15:57:17 +03:00
CosminPerRam
0ceb31bf86 [Crate] Apply cargo clippy fixes 2023-05-30 15:49:35 +03:00
CosminPerRam
d302d1173f
[Protocol] Add quake protocols. (#35)
* [Protocol] Initial packet receive implementation

* [Protocol] Add key extraction

* [Protocol] Fix new Ipv4Addr query address and get string with unended

* [Protocol] Properly parse the received data

* [Protocol] Add parse players

* [Protocol] Add bots

* [Protocol] Extract into functions

* [Protocol] Remove quotes from player name

* [Protocol] Add two and three files

* [Protocol] Make quake queries very modular

* [Protocol] Remove the need of a client instance

* [Protocol] Revesed if statement

* [Protocol] Apply clippy fixes and replace String by &str in get send header

* [Protocol] Add one and three implementations

* [Protocol] Add quake2 and quake3 to master_querant

* [Protocol] Fix Q2 implementation

* [Protocol] Change from Ipv4Addr to IpAddr

* [Protocol] Fix Q3 response header

* [Protocol] Fix Q3 response

* [Crate] Add Q1, 2 and 3 to changelog and protocols

* [Protocol] Extract client into separate file and add some docs
2023-05-30 15:33:41 +03:00
CosminPerRam
3dbc6498ed [Crate] Add Douile acknowledgement to Changelog 2023-05-29 11:19:16 +03:00
Tom
3f654e0dfd
[Protocol] Enable the use of Ipv6 addresses (#41)
Replace usages of Ipv4Addr with IpAddr which allows the use of either Ipv4 or Ipv6.

This patch essentially consists of running:
"sed -i 's/Ipv4Addr/IpAddr/g' src/**/*.rs examples/*"
and fixing the errors.
2023-05-29 11:10:21 +03:00
CosminPerRam
e620398615 [Crate] Changed all address &str to &Ipv4Addr 2023-05-27 00:41:41 +03:00
CosminPerRam
a69896f737 [Service] Fix tests 2023-05-08 15:34:59 +03:00
CosminPerRam
f843780469 [Service] Removed Filters and SearchFilters lifetimes and changed str instances to String 2023-05-08 15:31:38 +03:00
CosminPerRam
a8e2b51dbb Revert "[Service] Add merge methods on SearchFilters"
This reverts commit a17f5ad4d2.
2023-05-08 15:16:54 +03:00
CosminPerRam
a17f5ad4d2 [Service] Add merge methods on SearchFilters 2023-05-08 14:59:55 +03:00
CosminPerRam
fc52f3fe91 [Protocol] Add derives and serde derives to GatheringSettings 2023-05-08 02:08:22 +03:00
CosminPerRam
726bfd429f [Crate] Bump version to 0.2.2 2023-05-01 20:27:39 +03:00
CosminPerRam
33e8f43cb8 [Games] Add Crysis Wars support. 2023-04-30 01:26:12 +03:00
CosminPerRam
3ef599056a [Protocol] GS3 if no player/team data is gathered, dont try to create a struct 2023-04-30 01:20:16 +03:00
CosminPerRam
8abb657800 [Service] Update docs. 2023-04-30 00:50:51 +03:00
CosminPerRam
9f22a4eadf [Crate] Add no_services feature flag 2023-04-30 00:42:01 +03:00
CosminPerRam
4c4b9d6b45 [Crate] Edit serde feature to clarify serialization/deserialization for response types 2023-04-30 00:34:04 +03:00
CosminPerRam
6c9f554751 [Service] Make Filter to_bytes take self instead of &self 2023-04-30 00:22:47 +03:00
CosminPerRam
ed2934f3fa [Service] Replace repetitive code with a function 2023-04-30 00:20:24 +03:00
CosminPerRam
3b694815cc [Service] Add Copy trait to VMS Filter 2023-04-29 23:03:56 +03:00
CosminPerRam
e159cfebbd [Service] Valve Master Server change filter storage from Vec<Filter> to a HashMap variant for better speed 2023-04-29 22:57:10 +03:00
CosminPerRam
780d42067e [Protocol] Moved GS1 under gamespy::one instead of gamespy 2023-04-28 19:49:52 +03:00
CosminPerRam
c5fd58f794 [Protocol] Some cargo clippy improvements for GS one and three 2023-04-28 19:09:11 +03:00
CosminPerRam
4122d34cfa
[Service] Add valve master server query service (#34)
* [Service] Add initial files

* [Service] Add initial request packet

* [Service] Add filters

* [Service] Some clippy improvements

* [Service] Make query a vector of ipv4addr and port

* [Service] Add complete and singular query

* [Crate] Update md files

* [Service] Add docs and clippy adjustments

* [Service] Add hasTags and fix filters

* [Service] Use let some instead of match

* [Service] Add other filters

* [Service] Add nor and nand filters

* [Service] Remove 0.0.0.0:0 from query

* [Service] Remove dev testing test

* [Service] Add valve_master_server_query example
2023-04-28 18:00:04 +03:00
CosminPerRam
348147b415
[Game] Add Frontlines: Fuel of War support. (#31)
* [Game] Add initial files

* [Game] Initial support

* [Game] Add response struct

* [Game] Add query_with_timeout

* [Game] FFOW: Added some doc comments
2023-04-22 20:03:30 +03:00
CosminPerRam
786da81ea5
[Protocol] Add GameSpy 3 support. (#25)
* [Protocol] Gamespy3 initial code

* [Protocol] Add rest of challenge solving

* [Protocol] Remove unused stuff

* [Protocol] Remove adding unused bytes

* [Protocol] Clean up code

* [Protocol] Make gs3 a struct

* [Protocol] Add initial key-value parsing

* [Protocol] Manage multiple packets

* [Protocol] Split server vars and other vars

* Revert "[Protocol] Split server vars and other vars"

This reverts commit 9a930aeb68802fcf3d0908a2e031dfea054d37d0.

* [Protocol] Proper packet management and initial response struct

* [Protocol] Fix players_minimum

* [Protocol] Fix server vars to parse only the first packet

* [Protocol] Update CHANGELOG.md

* [Protocol] Initial player parsing

* [Protocol] Split GS one and three

* [Protocol] Add common code file

* [Protocol] Change static to const

* [Protocol] Fix players_online and break on data to map on empty key

* [Protocol] Remove unused types and printlns

* [Protocol] Add teams parsing

* [Protocol] Split key_values and parsing data

* [Crate] Update PROTOCOLS.md
2023-04-17 15:10:51 +03:00
Cain
1b13d39856
[Crate] Add formatting (#22)
* chore: add standard for formatting

* chore: manually tidy up imports and format

* chore: remove vscode and add to gitignore

* chore: alphabetically order and fix

* chore: format

* chore: fix format issue with payload

* chore: format as merge had unformatted code

* [format] Fix comments, change max width and binop operator

---------

Co-authored-by: CosminPerRam <cosmin.p@live.com>
2023-03-14 10:31:37 +02:00
CosminPerRam
e023e13236 [Crate] Add serde feature to changelog and lib doc 2023-03-13 17:02:51 +02:00
Cain
84af4230f7
[Crate] Add feature: serde (#21)
* feat(serde): add additional derives

* fix: remove attr on internal enum

* fix add missing derive
2023-03-13 16:51:33 +02:00
Cain
bd2e373d66
[Crate] Make clippy happy (#23)
* fix: clippy::type_complexity

* fix: clippy::needless_doctest_main

* fix: clippy::read_zero_byte_vec

* fix: clippy::useless_conversion

* fix: clippy::slow_vector_initialization
2023-03-13 11:28:49 +02:00
CosminPerRam
7f73eb582d [Crate] Update CHANGELOG to add optimizations thanks 2023-03-13 00:45:54 +02:00
CosminPerRam
9f6b3bae18 [Crate] Reorganize README. 2023-03-13 00:43:03 +02:00
CosminPerRam
7500b09b4d [Bufferer] Use struct functions internally too 2023-03-12 22:53:26 +02:00
Cain
568c53f129
[Tests] udp and tcp socket test (#17)
* impl(test): udp and tcp socket test

* fix(test): try and fix possable thread hang

* fix(test): move socket to thread

* [test/socket] Move listener outside of thread scope to make sure it is binded

* [test/socket] Let the OS to bind to an available port

---------

Co-authored-by: CosminPerRam <cosmin.p@live.com>
2023-03-12 01:07:36 +02:00
Cain
927d56b1ee
[Tests]: Timeout settings (#18) 2023-03-11 11:17:36 +02:00
CosminPerRam
3dacc09173 [Utils] Replace address and port as string string and additions to format! 2023-03-11 00:20:09 +02:00
CosminPerRam
7352c595e9 [Socket] Replace static with const for DEFAULT_PACKET_SIZE value 2023-03-11 00:00:31 +02:00
Cain
bf2a05f488
[Tests] result, display, trait and cloning (#19) 2023-03-10 22:27:30 +02:00
CosminPerRam
2865543975 [Repo] Add Discord Server link 2023-03-10 21:52:56 +02:00
CosminPerRam
a3cbb24d0d [Crate] Further use the Byteorder crate 2023-03-09 17:17:48 +02:00
CosminPerRam
9ad2f143dd [Crate] Use Byteorder crate 2023-03-09 16:41:13 +02:00
CosminPerRam
e163774685 [Protocols] Cargo clippy optimizations 2023-03-09 01:30:28 +02:00
CosminPerRam
e6562d30cb [Crate] Update links to point to gamedig organization rather than cosminperram 2023-03-08 21:50:34 +02:00
CosminPerRam
14c5edc1be [Games] Serious Sam support. 2023-03-04 21:36:01 +02:00
CosminPerRam
8992ffe4df [Docs] Match the docs example to the one from the readme 2023-03-04 14:04:38 +02:00
CosminPerRam
9d0cc15f4c [Crate] Update lib docs spelling 2023-03-04 13:47:20 +02:00
CosminPerRam
c7f706bf35 [Crate] Add preliminary changelog 2023-03-03 18:11:40 +02:00
CosminPerRam
04299c1a2c [Crate] Bump version to 0.2.1. 2023-03-03 18:09:17 +02:00
CosminPerRam
59994bc086 [Games] Use port.unwrap_or instead of matching it 2023-03-03 18:02:54 +02:00
CosminPerRam
5f06f58df8 [Crate] Hyperlink tf2 2023-03-03 17:55:23 +02:00
CosminPerRam
f97de3bb63 [Crate] Add warning about frequent API breaking changes in the Readme 2023-03-03 17:54:13 +02:00
CosminPerRam
950c08c18e
[Protocol] GameSpy 1 support with the games Unreal Tournament and Battlefield 1942. (#9)
* Initial files + unreal tournament

* Fix master_querant

* Split by delimiter and collect into hashmap

* Furter port to accept more packets

* Improve getting the server's values

* Some initial players parsing

* Players parsing

* Add error handling

* Add some more fields

* Add Battlefield 1942 support.

* Add query_vars and some docs
2023-03-03 17:45:18 +02:00
CosminPerRam
5604436553 Update the README.md to capitalize project name 2023-02-22 13:11:38 +02:00
CosminPerRam
cd4cbc09db [Games] V Rising support. 2023-02-19 18:37:10 +02:00
CosminPerRam
e26f0f871a [Protocol] Valve: Fix queries that require multiple challenge responses 2023-02-19 17:56:39 +02:00
CosminPerRam
99c87557c2 [Protocol] Valve: Reverse: No name players not being added to the list 2023-02-19 16:43:55 +02:00
CosminPerRam
ab43675ae5 [Crate] Add feature 'no_games' 2023-02-18 22:13:25 +02:00
CosminPerRam
150bc1762e [Protocol] Valve: support app and dedicated app id 2023-02-18 21:44:17 +02:00
CosminPerRam
fe46359e47 Games: Operation Harsh Doorstop support. 2023-02-18 19:00:54 +02:00
CosminPerRam
719ae9d591 Games: Avorion support. 2023-02-18 18:37:02 +02:00
CosminPerRam
3231653e4c Reword Battalion 1944 query note message 2023-02-18 18:28:06 +02:00
CosminPerRam
e16efee488 Games: BrainBread 2 support. 2023-02-18 18:25:17 +02:00
CosminPerRam
eca9757421 Ballistic Overkill support. 2023-02-18 18:17:19 +02:00
CosminPerRam
df9005cc9f Games: Codename CURE support. 2023-02-18 18:09:20 +02:00
CosminPerRam
bdaa1c4f64 Move up SDTD Steam ID 2023-02-18 18:02:21 +02:00
CosminPerRam
649dfd81ed Games: Onset support. 2023-01-18 18:13:50 +02:00
CosminPerRam
2312ba9114 Games: Colony Survival support. 2023-01-18 18:06:08 +02:00
CosminPerRam
bbd2dd7d97 Games: Don't Starve Together support. 2023-01-18 17:57:19 +02:00
CosminPerRam
dfe544c6aa Bump version to 0.1.0 2023-01-17 02:02:49 +02:00
CosminPerRam
6ec2b8952c Update docs. 2023-01-17 01:21:34 +02:00
CosminPerRam
21a27fd9cc Protocol: Minecraft: Rename java's response struct to JavaResponse 2023-01-17 01:21:17 +02:00
CosminPerRam
f2ae81002e Protocol: Minecraft: Rename players-related fields 2023-01-17 01:01:53 +02:00
CosminPerRam
4fb1350753 Update some github documentation. 2023-01-16 23:31:59 +02:00
CosminPerRam
e2f42008b2 Games: Age of Chivalry support. 2023-01-16 23:10:16 +02:00
CosminPerRam
9c9a096b16 Valve Protocol: Extend split packets correctly 2023-01-16 22:44:55 +02:00
CosminPerRam
f03a1de035 Games: Add Project Zomboid support. 2023-01-16 22:03:28 +02:00
CosminPerRam
328de37b2d Games: Add Black Mesa support. 2023-01-16 21:33:28 +02:00
CosminPerRam
4a7eb400db Games: Add Battalion 1944 from-rules server information's 2023-01-13 23:52:45 +02:00
CosminPerRam
9bcbfbc198 Games: Add Battalion 1944 support. 2023-01-13 23:08:09 +02:00
CosminPerRam
018935fd29 Valve Protocol: Fix BadGame reported appid 2023-01-13 22:58:36 +02:00
CosminPerRam
ff789fcb90 Valve: Rename players-related fields 2023-01-13 19:36:03 +02:00
CosminPerRam
637252ca18 Update CHANGELOG.md 2023-01-13 01:44:24 +02:00
CosminPerRam
6786636945 Update README.md 2023-01-13 01:32:28 +02:00
CosminPerRam
824c4d34c0 Add proper MSRV to Cargo.toml 2023-01-13 01:31:57 +02:00
CosminPerRam
3928d3a818 Implement std::error::Error for GDError 2023-01-13 01:00:31 +02:00
CosminPerRam
e72d7bdf8b Revert error details for the BadGame error 2023-01-13 00:39:58 +02:00
CosminPerRam
c263b17651 Remove errors details as they were quite useless 2023-01-13 00:11:04 +02:00
CosminPerRam
e8619a7df1 Update CHANGELOG.md 2023-01-12 23:50:54 +02:00
CosminPerRam
c5a35016d1 Valve Protocol: Players with no name (name length == 0), are no more added to players details 2023-01-12 23:40:43 +02:00
CosminPerRam
5c664187f9 Remove Test: Value server rule from RoR2 2023-01-12 23:19:13 +02:00
CosminPerRam
66be14df0c Update the README example response. 2023-01-12 23:11:46 +02:00
CosminPerRam
50012dd49f Change Valve Protocol Rules to HashMap<String, String> 2023-01-12 23:04:57 +02:00
CosminPerRam
d8aef7d9e5 Move badges on the same line as the readme title 2023-01-09 20:46:27 +02:00
CosminPerRam
ededf93b38 Add Risk of Rain 2 support. 2023-01-09 19:02:31 +02:00
CosminPerRam
1e99aebbac Bump version to 0.0.7 2023-01-03 00:36:35 +02:00
CosminPerRam
7d164d40a1 Add Copy trait to Valve Request Kind enum 2022-12-31 14:52:17 +02:00
CosminPerRam
ef8ac92506 Change valve get_server_rules to not return an Option 2022-12-31 14:35:36 +02:00
CosminPerRam
a37e2506b4 Change valve get_request_data return type from vec<u8> to Bufferer 2022-12-31 14:32:35 +02:00
CosminPerRam
0e68f8c830 Make public functions that are meant to be used internally private. 2022-12-29 16:59:51 +02:00
CosminPerRam
8c98433da9 Fix online/max players being reversed on minecraft legacy 1.6 2022-12-29 16:37:42 +02:00
CosminPerRam
b09fa4ada5
Change buffer reading implementation (#8)
* Add new implementation an valve protocol refactor

* Refactor minecraft protocol with new bufferer
2022-12-29 16:30:24 +02:00
CosminPerRam
91f8bbb9fe
Minecraft bedrock support (#7)
* Added needed ground stuff

* Minecraft bedrock support!

* Documentation acknowledgements!

* Added utf8_le_undended test, some docs and modified master_querant

* Modified query function to comply with the others

Before: game query -> protocol query (get port or default port)
After: game query (get port or default port) -> protocol query

* Modified md files
2022-12-05 18:47:35 +02:00
CosminPerRam
ae14e37e60 Modified README.md: Removed lines of code badge (as it was broken). 2022-11-30 23:13:51 +02:00
CosminPerRam
e66fa014ca Bumped version to 0.0.6! 2022-11-28 22:10:35 +02:00
CosminPerRam
7828bb9433 Minecraft rework and some docs 2022-11-28 22:08:28 +02:00
CosminPerRam
d671bb0310 Half-Life Deathmatch: Source support. 2022-11-28 21:33:09 +02:00
CosminPerRam
663fb6a66e Modified the master_querant example 2022-11-28 21:20:24 +02:00
CosminPerRam
1c173b76ca Day of Infamy support. 2022-11-28 20:53:12 +02:00
CosminPerRam
1ad5031c6f Added u8_lower_upper test 2022-11-28 01:56:46 +02:00
CosminPerRam
013da5f0c4 Forgot to change address_and_port_as_string in the socket.rs 2022-11-28 01:28:07 +02:00
CosminPerRam
2e8aa50c3a Fixed tests and removed dns error 2022-11-28 01:22:59 +02:00
CosminPerRam
a1d42af2df Simplified some map uses 2022-11-28 01:16:30 +02:00
CosminPerRam
3d95f08ef4 Removed DNS resolving as it was not needed 2022-11-28 01:13:08 +02:00
CosminPerRam
77a68e4a0c Modified CHANGELOG.md to tell the comeback of the csgo rules response and increased valve protocol PACKET_SIZE from 1400 to 6144 2022-11-28 00:56:11 +02:00
CosminPerRam
259e21a4ab Restored full CSGO query capabilities 2022-11-28 00:51:09 +02:00
CosminPerRam
e709cb3ce5 Edited GAMES.md, PROTOCOLS.md and changed 'verify' keyword to 'query' in Cargo.toml 2022-11-28 00:23:19 +02:00
CosminPerRam
ed681025b4 Added error_by_expected_size test. 2022-11-27 23:45:48 +02:00
CosminPerRam
84d05bd958 Tested Alien Swarm and Insurgency: Modern Infantry Combat. 2022-11-27 23:40:07 +02:00
CosminPerRam
9f861df96b Arma 2: Operation Arrowhead support. 2022-11-26 18:08:32 +02:00
CosminPerRam
aec145a847 Counter-Strike support. 2022-11-26 17:17:09 +02:00
CosminPerRam
645582868d More tests and fixed (?) utf16 be string reading 2022-11-26 17:09:34 +02:00
CosminPerRam
3f58e99c28 Rust support. 2022-11-26 16:27:03 +02:00
CosminPerRam
de3ac9aad5 Sven Co-op support. 2022-11-26 16:21:58 +02:00
CosminPerRam
8c5ac24468 Changed The Forest default port to 27016. 2022-11-26 16:15:26 +02:00
CosminPerRam
d086d49cdc Team Fortress Classic support. 2022-11-26 16:15:01 +02:00
CosminPerRam
21b7d91ee6 The Forest support. 2022-11-26 16:08:47 +02:00
CosminPerRam
ed2161a6da Updated CHANGELOG and GAMES 2022-11-26 15:40:34 +02:00
CosminPerRam
7b2cad22ec Unturned support. 2022-11-26 15:38:13 +02:00
CosminPerRam
999998f309 Modified README 2022-11-26 15:22:07 +02:00
CosminPerRam
0a48b0e8eb Ark: Survival Evolved support 2022-11-25 20:27:31 +02:00
CosminPerRam
e689bc766e 7 Days To Die support. 2022-11-25 20:10:16 +02:00
CosminPerRam
2f640e93d5 Modified README 2022-11-25 19:57:41 +02:00
CosminPerRam
f683c17c80 Changed name of the CI file 2022-11-25 19:56:43 +02:00
CosminPerRam
06b9ef0013 Changed example ip to localhost 2022-11-25 19:47:26 +02:00
CosminPerRam
7498b68e81 Edited README.md 2022-11-25 19:44:08 +02:00
CosminPerRam
462014c8ac Renamed workflow file and edited README 2022-11-25 19:37:12 +02:00
CosminPerRam
4c7cecb5c3 Renamed workflow file and edited README 2022-11-25 19:25:47 +02:00
CosminPerRam
0e1ca4304b
Create rust.yml 2022-11-25 19:00:32 +02:00
CosminPerRam
e36161ce5a Edited README.md 2022-11-25 18:59:25 +02:00
CosminPerRam
7b44c5f7eb Reverter errors from taking a &'static str to String 2022-11-25 18:42:10 +02:00
CosminPerRam
dc0926bab7 Renamed ProtocolRule to ProtocolFormat 2022-11-24 23:41:23 +02:00
CosminPerRam
304b8340d2 Better, faster and stronger errors. 2022-11-24 23:38:51 +02:00
CosminPerRam
b988b51cff Some reordering 2022-11-24 22:58:34 +02:00
CosminPerRam
ee0223a7a3
Minecraft implementation (#6)
* Initial minecraft support

* Made previews_chat an option

* Better error handling and removed version structure

* Minecraft Server types

* Fixed compilation and renamed stuff

* 'extract till you drop!' extracted sockets

* extracted java version and fixed socket udp receive

* Legacy 1.4 and 1.6 implementation (incomplete)

* Furter implementation

* Implementations work

* Protocol beta v1.8+ implemented

* Removed bedrock support

* Added auto query

* Renamed minecraft to mc and added to md's

* Docs, renames and small optimization changes

* Changed java version to be able to return None on players sample
2022-11-24 22:52:54 +02:00
CosminPerRam
974e093e23 Move TimeoutSettings to be a type that can be used by multiple protocols 2022-11-16 00:06:13 +02:00
CosminPerRam
f04c883269 Forgot cargo.toml version bump 2022-11-15 21:10:31 +02:00
CosminPerRam
a08afb2712 Bumped version to 0.05 2022-11-15 21:09:51 +02:00
CosminPerRam
caa7329a68 Added socket timeout capability and reduced PACKET_SIZE to 1400 as specified from protocol 2022-11-15 21:07:15 +02:00
CosminPerRam
d3b71fccf6 Changed packet size to protocol specified size 2022-11-15 17:20:01 +02:00
cosminperram
ac9d385fb6 Fixed multipacket response when protocol = 7 with certain apps 2022-10-27 11:09:59 +03:00
CosminPerRam
d3a1dba3c1
Restructured app format, goldsrc full support and added implementation for Day of Defeat and Counter-Strike: Condition Zero (#5)
* [valve_app_restructure] Initial change

* [valve_app_restructure] Some GoldSrc split packet changes

* [valve_app_restructure] Counter-Strike: Condition Zero implementation.

* [valve_app_restructure] Docs changes

* [valve_app_restructure] Added obsolete gold src response

* [valve_app_restructure] Day of Defeat implementation.
2022-10-27 01:01:11 +03:00
cosminperram
96c2c8a335 Fixed uncomplete the ship player struct and added some docs... 2022-10-23 18:09:13 +03:00
cosminperram
9df4bddc09 Modularized reusable structs and changed files structure a bit 2022-10-23 17:34:22 +03:00
cosminperram
faaedf44f0 Version bump! 2022-10-23 14:06:08 +03:00
cosminperram
3ac6a8b603 Changed uses to have a better structure 2022-10-23 14:03:59 +03:00
cosminperram
c0d07cf6f9 Valve Protocol now support anonymously querying 2022-10-23 13:34:40 +03:00
cosminperram
854d395aad Updated CHANGELOG 2022-10-23 13:01:53 +03:00
cosminperram
8e2d76ecfb Insurgency: Modern Infantry Combat implementation. 2022-10-23 12:39:23 +03:00
cosminperram
88a4c82158 Removed examples, added a master_querant change gather_settings to none (representing all) 2022-10-22 23:50:32 +03:00
cosminperram
83bbd5d428 Added Insurgency and Insurgency: Sandstorm implementation. 2022-10-22 19:25:54 +03:00
cosminperram
14abf3d1ab Removed Cargo.lock and updated CHANGELOG 2022-10-22 17:11:14 +03:00
cosminperram
a5bdd05c24 Added Alien Swarm and Alien Swamr: Reactive Drop support 2022-10-22 17:03:14 +03:00
CosminPerRam
e8cbe7b9f5
DNS Resolver Implementation (#4)
* [dns-resolver] Added trust-dns-resolver and restored cargo.lock

* [dns_resolver] Implemented feature
2022-10-22 14:58:59 +03:00
cosminperram
6159a7c385 Version bump! 2022-10-22 02:39:06 +03:00
cosminperram
3b4dd9d9e4 Decompression support 2022-10-22 02:27:11 +03:00
CosminPerRam
e621a9aedd
Better packet structure (#3)
* [packet_structure] Initial implementation

* [packet_structure] Fixed on tf2

* [packet_structure] Fixed info request
2022-10-22 01:22:09 +03:00
CosminPerRam
4e9458f102 Added support for Half-Life 2 Deathmatch 2022-10-21 13:10:09 +03:00
CosminPerRam
d477bbb178 Added support for Left 4 Dead and Left 4 Dead 2 2022-10-21 13:03:15 +03:00
CosminPerRam
046544ea27 Added Garry's Mod support 2022-10-21 12:55:17 +03:00
CosminPerRam
a5f9e755ff Added Day of Defeat: Source support and renamed The_Ship to TS 2022-10-21 12:45:30 +03:00
CosminPerRam
aefd8cc43c Added support for Counter-Strike: Source 2022-10-21 12:35:33 +03:00
CosminPerRam
b5141e8196 Modified README and added SERVICES.md 2022-10-20 23:45:31 +03:00
CosminPerRam
675ed13493 Renamed players and online_players to players_details and players 2022-10-20 23:25:17 +03:00
CosminPerRam
15e6ad5892 Replaced Result<T, GDError> with GDResult<T> 2022-10-20 23:19:57 +03:00
CosminPerRam
526aef9acc
Games structures (#2)
* tf2 skeleton

* tf2 structure done

* csgo structure

* the_ship structure
2022-10-20 23:08:45 +03:00
CosminPerRam
aac3a483c0 Added PROTOCOLS.md, modified README and updated GAMES 2022-10-20 14:41:17 +03:00
CosminPerRam
21d2bc45a1 Bumped version and modified README 2022-10-20 13:09:11 +03:00
CosminPerRam
3c6cbda0f5 Simplified Valve ServerRules and ServerPlayers 2022-10-20 12:56:10 +03:00
CosminPerRam
00ead6d946 Documentation update 2022-10-20 12:49:22 +03:00
cosminperram
40912bb192 CSGO support. 2022-10-20 11:33:31 +03:00
cosminperram
8a93d2fb7d Initial CSGO support 2022-10-20 11:24:45 +03:00
cosminperram
38d7758c4c Fixed bad environment_type linux value and added error branch 2022-10-20 00:20:07 +03:00
cosminperram
401d499d61 The ship support 2022-10-20 00:14:57 +03:00
cosminperram
5cf5615265 Initial the ship support 2022-10-19 22:47:52 +03:00
CosminPerRam
192d50a11d Specified MIT license and made the valve protocol check if the queried game is the right game 2022-10-18 18:20:37 +03:00
CosminPerRam
1cb00f826a Fixed rules response 2022-10-18 11:51:29 +03:00
CosminPerRam
92d9649008 Valve PLAYERS and RULES request first implementations 2022-10-18 01:40:37 +03:00
CosminPerRam
9ab4b8a7fd Error handling and better structure 2022-10-17 23:22:46 +03:00
CosminPerRam
544ce897c5 Better protocol parameters and utils tests 2022-10-17 11:11:40 +03:00
CosminPerRam
3a83588802 Much more readable code! 2022-10-16 17:55:48 +03:00
CosminPerRam
c2742fbcf0 More readable code 2022-10-16 16:14:10 +03:00
CosminPerRam
11964d530f Removed a comment that suggested that a feature doesnt work
it turns out it was working, i just thought it wasnt.
2022-10-16 13:10:48 +03:00
CosminPerRam
3ef9289dfb Added the CHANGELOG.md file. 2022-10-16 03:45:08 +03:00
CosminPerRam
1fb3998033 Let access of protocols and errors stuff 2022-10-16 03:33:25 +03:00
CosminPerRam
fa4e72f80a Bumped the Cargo.toml package version to mark the first usable version! 2022-10-16 03:28:07 +03:00
CosminPerRam
5870a52ce2 Modified the README.md file 2022-10-16 03:25:41 +03:00
CosminPerRam
73c8ade3a2 Modified public presentation files 2022-10-16 03:23:18 +03:00
CosminPerRam
c9eb725a51 Almost completed the valve protocol 2022-10-16 02:42:17 +03:00
CosminPerRam
8098136d09 Initial valve setup and tf2 game setup 2022-10-15 22:20:01 +03:00
138 changed files with 13789 additions and 29 deletions

8
.actrc Normal file
View file

@ -0,0 +1,8 @@
# Configuration file for act (run github actions locally using docker)
# https://github.com/nektos/act
# Swap docker image for the one containing the rust toolchain
-P ubuntu-latest=ghcr.io/catthehacker/ubuntu:rust-latest
# Load custom event
-e .github/.act-event.json

6
.github/.act-event.json vendored Normal file
View file

@ -0,0 +1,6 @@
{
"act": true,
"repository": {
"default_branch": "main"
}
}

23
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,23 @@
---
name: Bug report
about: Create a report for a found bug
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Steps To Reproduce**
Please provide the steps to reproduce the behavior (if not possible, describe as many details as possible).
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots or Data**
If applicable, add screenshots/data to help explain your problem.
**Additional context**
Add any other context about the problem here.

View file

@ -0,0 +1,14 @@
---
name: Feature request
about: Suggest a feature
title: ''
labels: enhancement
assignees: ''
---
**What is this feature about?**
Shortly explain what your requested feature is about.
**Additional context/references**
Add any other context or references about the feature request here.

View file

@ -0,0 +1,17 @@
---
name: Help regarding code/protocol errors
about: Use this if you can't figure out how to use something.
title: ''
labels: ''
assignees: ''
---
**This issue shall be made only if you have already gone through the docs, have you done it?**
Please state if there is something confusing regarding the docs (eg. location or wording).
**What's you problem?**
State as concise as possible what you want to do and can't do.
**Suggestions to make this clearer**
Mention how could stuff be improved so that someone doesn't have the same problem as you (eg. Error should give more information).

0
.github/badges/.gitkeep vendored Normal file
View file

20
.github/badges/node.svg vendored Normal file
View file

@ -0,0 +1,20 @@
<svg width="181.6" height="20" viewBox="0 0 1816 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Node game coverage: 23.46%">
<title>Node game coverage: 23.46%</title>
<linearGradient id="a" x2="0" y2="100%">
<stop offset="0" stop-opacity=".1" stop-color="#EEE"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<mask id="m"><rect width="1816" height="200" rx="30" fill="#FFF"/></mask>
<g mask="url(#m)">
<rect width="1276" height="200" fill="#555"/>
<rect width="540" height="200" fill="#0f80c1" x="1276"/>
<rect width="1816" height="200" fill="url(#a)"/>
</g>
<g aria-hidden="true" fill="#fff" text-anchor="start" font-family="Verdana,DejaVu Sans,sans-serif" font-size="110">
<text x="60" y="148" textLength="1176" fill="#000" opacity="0.25">Node game coverage</text>
<text x="50" y="138" textLength="1176">Node game coverage</text>
<text x="1331" y="148" textLength="440" fill="#000" opacity="0.25">23.46%</text>
<text x="1321" y="138" textLength="440">23.46%</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

16
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,16 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/.github/workflows"
schedule:
interval: "daily"
- package-ecosystem: "cargo"
directory: "/crates/cli"
schedule:
interval: "daily"
- package-ecosystem: "cargo"
directory: "/crates/lib"
schedule:
interval: "daily"

18
.github/labeler.yml vendored Normal file
View file

@ -0,0 +1,18 @@
ci:
- .github/workflows/**
- .github/labeler.yml
- .actrc
- .pre-commit-config.yaml
protocol:
- crates/lib/src/protocols/**
game:
- crates/lib/src/games/**
cli:
- crates/cli/**
crate:
- Cargo.toml
- Cargo.lock

20
.github/workflows/audit.yml vendored Normal file
View file

@ -0,0 +1,20 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/softprops/github-actions-schemas/master/workflow.json
name: Security audit
on:
push:
paths:
- "**/Cargo.toml"
- "**/Cargo.lock"
jobs:
security_audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Generate Cargo.lock # https://github.com/rustsec/audit-check/issues/27
run: cargo generate-lockfile
- name: Audit Check
uses: rustsec/audit-check@v2.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}

199
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,199 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/softprops/github-actions-schemas/master/workflow.json
name: CI
on:
push:
branches: ["main"]
paths:
- "**.rs" # Any rust file
- "**/Cargo.toml" # Any Cargo.toml
- ".rustfmt.toml"
- ".github/workflows/ci.yml" # This action
pull_request:
branches: ["main"]
paths:
- "**.rs" # Any rust file
- "**/Cargo.toml" # Any Cargo.toml
- ".rustfmt.toml"
- ".github/workflows/ci.yml" # This action
env:
CARGO_TERM_COLOR: always
jobs:
# First check that we can build EVERYTHING and that tests pass
build_first:
name: "Build, check, and test with all features"
runs-on: ubuntu-latest
outputs:
cli: ${{ steps.filter.outputs.cli }}
lib: ${{ steps.filter.outputs.lib }}
steps:
- uses: actions/checkout@v5
- uses: Swatinem/rust-cache@v2
with:
shared-key: "cargo-deps"
cache-targets: false
- name: Run Build
run: cargo check --verbose --workspace --bins --lib --examples --all-features
- name: Run Tests
run: cargo test --verbose --workspace --bins --lib --examples --tests --all-features
# Check what paths were modified so we only run the required tests
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
cli:
- 'crates/cli/**'
lib:
- 'crates/lib/**'
# If we were able to build then test different feature combinations compile with the library
build_lib:
runs-on: ubuntu-latest
needs: ["build_first"]
# Only run if library files were modified
if: ${{ needs.build_first.outputs.lib == 'true' }}
strategy:
fail-fast: false
matrix:
include:
- build_type: ""
build_name: "Default"
- build_type: "--no-default-features"
build_name: "No features"
- build_type: "--no-default-features --features games"
build_name: "Just games"
- build_type: "--no-default-features --features services"
build_name: "Just Services"
- build_type: "--no-default-features --features game_defs"
build_name: "Just Game definitions"
- build_type: "--no-default-features --features serde"
build_name: "Just serde"
name: "Build library ${{ matrix.build_name }}"
steps:
- uses: actions/checkout@v5
- uses: Swatinem/rust-cache@v2
with:
shared-key: "cargo-deps"
cache-targets: false
save-if: false
- name: Run Build
run: cargo check -p gamedig --verbose --lib --examples --tests ${{ matrix.build_type }}
# If we were able to build then test different feature combinations compile with the CLI
build_cli:
runs-on: ubuntu-latest
needs: ["build_first"]
# Only run if CLI files were modified
if: ${{ needs.build_first.outputs.cli == 'true' }}
strategy:
fail-fast: false
matrix:
include:
- build_type: ""
build_name: "Default"
- build_type: "--no-default-features"
build_name: "No features"
name: "Build CLI ${{ matrix.build_name }}"
steps:
- uses: actions/checkout@v5
- uses: Swatinem/rust-cache@v2
with:
shared-key: "cargo-deps"
cache-targets: false
save-if: false
- name: Run Build
run: cargo check -p gamedig_cli --verbose --bins --examples --tests ${{ matrix.build_type }}
# If we were able to build then test the MSRV compiles (for the libary as not enforced for CLI)
build_msrv:
name: "Build using MSRV (lib only)"
runs-on: ubuntu-latest
needs: ["build_first"]
# Only run if library files were modified
if: ${{ needs.build_first.outputs.lib == 'true' }}
# Unfortunate hard-coding of rustup directory so that rust-cache caches it
env:
RUSTUP_HOME: /home/runner/.rustup
steps:
# Act's rust runner has rustup in a different place
- if: ${{ env.ACT }}
run: mkdir -p /home/runner && ln -s /usr/share/rust/.rustup /home/runner/.rustup
- uses: actions/checkout@v5
- uses: Swatinem/rust-cache@v2
with:
cache-targets: false
cache-directories: ${{ env.RUSTUP_HOME }}/toolchains
- name: Install MSRV
uses: actions-rs/toolchain@v1
with:
toolchain: 1.85.1
override: true
- name: Run MSRV
run: cargo check -p gamedig
# Check the code is formatted properly
formatting:
name: "Check code formatting"
runs-on: ubuntu-latest
# Unfortunate hard-coding of rustup directory so that rust-cache caches it
env:
RUSTUP_HOME: /home/runner/.rustup
steps:
# Act's rust runner has rustup in a different place
- if: ${{ env.ACT }}
run: mkdir -p /home/runner && ln -s /usr/share/rust/.rustup /home/runner/.rustup
- uses: actions/checkout@v5
- uses: Swatinem/rust-cache@v2
with:
cache-targets: false
cache-directories: ${{ env.RUSTUP_HOME }}/toolchains
- name: Install Formatting nightly
uses: actions-rs/toolchain@v1
with:
toolchain: nightly-2025-04-19
components: rustfmt
override: true
- name: Run Formatting check
run: cargo fmt --check --verbose
# If we were able to build then lint the codebase with clippy
clippy:
name: "Run clippy tests"
runs-on: ubuntu-latest
needs: ["build_first"]
steps:
- uses: actions/checkout@v5
- uses: Swatinem/rust-cache@v2
with:
shared-key: "cargo-deps"
cache-targets: false
save-if: false
# Run github actions version of clippy that adds annotations
- name: Run Clippy
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --workspace --bins --lib --examples --tests --all-features
if: ${{ !env.ACT }} # skip during local actions testing
# Run clippy binary
- name: Run clippy (local)
run: cargo clippy --verbose --workspace --bins --lib --examples --tests --all-features
if: ${{ env.ACT }} # only run during local actions testing
# If we were able to build then test that rustdoc (and rustdoc examples) compile
doc:
name: "Check rustdoc"
runs-on: ubuntu-latest
needs: ["build_first"]
steps:
- uses: actions/checkout@v5
- uses: Swatinem/rust-cache@v2
with:
shared-key: "cargo-deps"
cache-targets: false
save-if: false
- name: "Run cargo doc"
run: cargo doc --workspace
env:
RUSTDOCFLAGS: "-D warnings"

44
.github/workflows/codeql.yml vendored Normal file
View file

@ -0,0 +1,44 @@
name: "CodeQL"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '0 0 1 * *' # monthly on the 1st at 00:00 UTC
jobs:
analyze:
name: Analyze (rust)
runs-on: ubuntu-latest
permissions:
security-events: write
packages: read
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: rust
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Build
run: cargo build --release
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{ matrix.language }}"

16
.github/workflows/labeler.yml vendored Normal file
View file

@ -0,0 +1,16 @@
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
triage:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/labeler@v4
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
dot: true

63
.github/workflows/node-badge.yml vendored Normal file
View file

@ -0,0 +1,63 @@
# Based on: https://github.com/emibcn/badge-action/blob/master/.github/workflows/test.yml
name: "Generate node comparison badge"
on:
push:
paths:
- "crates/lib/src/games/definitions.rs"
- ".github/workflows/node-badge.yml"
- ".github/workflows/scripts/node-badge.mjs"
branches:
- "main" # Limit badge commits to only happen on the main branch
schedule: # This runs on the default branch only, it could still trigger on PRs but only if they develop on default branch and enable actions.
- cron: "34 3 * * 2" # Update once a week in case node-gamedig has changed
workflow_dispatch:
jobs:
badge:
runs-on: "ubuntu-latest"
name: Create node comparison badge
env:
BADGE_PATH: ".github/badges/node.svg"
steps:
- name: Extract branch name
shell: bash
run: echo "branch=${GITHUB_REF#refs/heads/}" >> "${GITHUB_OUTPUT}"
id: extract_branch
- uses: actions/checkout@v5
- uses: actions/checkout@v5
with:
repository: "gamedig/node-gamedig"
path: "node-gamedig"
sparse-checkout: |
lib/games.js
package.json
- name: Calculate comparison
id: comparison
run: node .github/workflows/scripts/node-badge.mjs
- name: Generate the badge SVG image
uses: emibcn/badge-action@v2.0.3
id: badge
with:
label: "Node game coverage"
status: "${{ steps.comparison.outputs.percent }}%"
color: "0f80c1"
path: ${{ env.BADGE_PATH }}
- name: "Commit badge"
continue-on-error: true
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add "${BADGE_PATH}"
git commit -m "Add/Update badge"
- name: Push badge commit
uses: ad-m/github-push-action@master
if: ${{ success() }}
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ steps.extract_branch.outputs.branch }}

View file

@ -0,0 +1,63 @@
"use strict";
//! Calculate the percentage of games from node that we support
// Expects node-gamedig checkout out in git root /node-gamedig
// Expects the generic example to output a list of game IDs when no arguments are provided
import process from "node:process";
import { closeSync, openSync, writeSync } from "node:fs";
import { spawnSync } from "node:child_process";
const setOutput = (key, value) => {
const file = openSync(process.env.GITHUB_OUTPUT, "a");
writeSync(file, `${key}=${value}\n`);
closeSync(file);
};
// Get node IDs
// NOTE: Here we directly import from games to avoid loading
// unecessary parts of the library that would require us
// to install dependencies.
import { games } from "../../../node-gamedig/lib/games.js";
const node_ids = new Set(Object.keys(games));
const node_total = node_ids.size;
// Get rust IDs
const command = spawnSync("cargo", [
"run",
"-p",
"gamedig",
"--example",
"generic",
]);
if (command.status !== 0) {
console.error(command.stderr.toString("utf8"));
process.exit(1);
}
const rust_ids_pretty = command.stdout.toString("utf8");
const rust_ids = new Set(
rust_ids_pretty
.split("\n")
.map((line) => line.split("\t")[0])
.filter((id) => id.length > 0)
);
// Detect missing node IDs
for (const id of rust_ids) {
if (node_ids.delete(id)) {
rust_ids.delete(id);
}
}
console.log("Node remains", node_ids);
console.log("Rust remains", rust_ids);
const percent = 1 - node_ids.size / node_total;
// Output percent to 2 decimal places
setOutput("percent", Math.round(percent * 10000) / 100);

4
.gitignore vendored
View file

@ -11,3 +11,7 @@ Cargo.lock
# Others
.idea/
.venv/
.vscode/
test_everything.py

60
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,60 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: local
hooks:
- id: clippy
name: Check clippy
language: system
files: '([.]rs|Cargo\.toml)$'
pass_filenames: false
entry: rustup run --install nightly-2025-04-19 cargo-clippy -- --workspace --all-features -- -D warnings
- id: build-no-features
name: Check crate build with no features
language: system
files: '([.]rs|Cargo\.toml)$'
pass_filenames: false
entry: cargo check --workspace --no-default-features
- id: build-all-features
name: Check crate builds with all features
language: system
files: '([.]rs|Cargo\.toml)$'
pass_filenames: false
entry: cargo check --workspace --all-features --lib --bins --examples
- id: test
name: Check tests pass
language: system
files: '([.]rs|Cargo\.toml)$'
pass_filenames: false
entry: cargo test --workspace --bins --lib --examples --tests --all-features
- id: format
name: Check rustfmt
language: system
files: '([.]rs|Cargo\.toml)$'
pass_filenames: false
entry: rustup run --install nightly-2025-04-19 cargo-fmt --check
- id: msrv
name: Check MSRV compiles (lib only)
language: system
files: '([.]rs|Cargo\.toml)$'
pass_filenames: false
entry: rustup run --install 1.85.1 cargo check -p gamedig
- id: docs
name: Check rustdoc compiles
language: system
files: '([.]rs|Cargo\.toml)$'
pass_filenames: false
entry: env RUSTDOCFLAGS="-D warnings" cargo doc
- id: actions
name: Check actions work
language: system
files: '^[.]github/workflows/'
pass_filenames: false
entry: act --rm

72
.rustfmt.toml Normal file
View file

@ -0,0 +1,72 @@
attr_fn_like_width = 70
array_width = 60
binop_separator = "Front"
blank_lines_lower_bound = 0
blank_lines_upper_bound = 1
brace_style = "PreferSameLine"
chain_width = 60
color = "Auto"
combine_control_expr = false
comment_width = 80
condense_wildcard_suffixes = true
control_brace_style = "AlwaysSameLine"
disable_all_formatting = false
doc_comment_code_block_width = 100
edition = "2021"
emit_mode = "Files"
empty_item_single_line = true
error_on_line_overflow = false
error_on_unformatted = false
fn_call_width = 60
fn_params_layout = "Tall"
fn_single_line = true
force_explicit_abi = true
force_multiline_blocks = true
format_generated_files = true
format_macro_bodies = true
format_strings = true
group_imports = "Preserve"
hard_tabs = false
show_parse_errors = true
hex_literal_case = "Preserve"
ignore = []
indent_style = "Block"
imports_granularity = "Preserve"
imports_indent = "Block"
imports_layout = "HorizontalVertical"
inline_attribute_width = 0
make_backup = false
match_arm_blocks = true
match_arm_leading_pipes = "Never"
match_block_trailing_comma = false
max_width = 120
merge_derives = true
newline_style = "Auto"
normalize_comments = true
normalize_doc_attributes = false
overflow_delimited_expr = false
reorder_impl_items = false
reorder_imports = true
reorder_modules = true
required_version = "1.8.0"
short_array_element_width_threshold = 10
single_line_if_else_max_width = 50
skip_children = false
space_after_colon = true
space_before_colon = false
spaces_around_ranges = true
struct_field_align_threshold = 0
struct_lit_single_line = true
struct_lit_width = 18
struct_variant_width = 35
tab_spaces = 4
trailing_comma = "Vertical"
trailing_semicolon = true
type_punctuation_density = "Wide"
unstable_features = false
use_field_init_shorthand = false
use_small_heuristics = "Default"
use_try_shorthand = true
style_edition = "2021"
where_single_line = true
wrap_comments = true

119
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,119 @@
# Contributing to rust-GameDig
This project is very open to new suggestions, additions and/or changes, these
can come in the form of *discussions* about the project's state, *proposing a
new feature*, *holding a few points on why we shall do X breaking change* or
*submitting a fix*.
## Communications
GitHub is the place we use to track bugs and discuss new features/changes,
although we have a [Discord](https://discord.gg/NVCMn3tnxH) server for the
community, all bugs, suggestions and changes will be reported on GitHub
alongside with their backing points to ensure the transparency of the project's
development.
## Issues
Before opening an issue, check if there is an existing relevant issue first,
someone might just have had your issue already, or you might find something
related that could be of help.
When opening a new issue, make sure to fill the issue template. They are made
to make the subject to be as understandable as possible, not doing so may result
in your issue not being managed right away, if you don't understand something
(be it regarding your own problem/the issue template/the library), please state
so.
## Development
Note before contributing that everything done here is under the [MIT](https://opensource.org/license/mit/) license.
### Naming
Naming is an important matter, and it shouldn't be changed unless necessary.
Game **names** should be added as they appear on steam (or other storefront
if not listed there) with the release year appended in brackets (except when the
release year is already part of the name).
If there is a mod that needs to be added (or it adds the support for server
queries for the game), its name should be composed of the game name, a separating
**bracket**, the mod name and the release year as specified previously
(e.g. `Grand Theft Auto V - FiveM (2013)`).
A game's **identification** is a lowercase alphanumeric string will and be forged
following these rules:
1. Names composed of a maximum of two words (unless #4 applies) will result in an
id where the words are concatenated (`Dead Cells` -> `deadcells`), acronyms in
the name count as a single word (`S.T.A.L.K.E.R.` -> `stalker`).
2. Names of more than two words shall be made into an acronym made of the
initial
letters (`The Binding of Isaac` -> `tboi`), [hypenation composed words](https://prowritingaid.com/hyphenated-words)
don't count as a single word, but of how many parts they are made of
(`Dino D-Day`, 3 words, so `ddd`).
3. If a game has the exact name as a previously existing id's game
(`Star Wars Battlefront 2`, the 2005 and 2017 one), append the release year to
the newer id (2005 would be `swb2` (suppose we already have this one supported)
and 2017 would be `swb22017`).
4. If a new id (`Day of Dragons` -> `dod`) results in an id that already exists
(`Day of Defeat` -> `dod`), then the new name should ignore rule #2
(`Day of Dragons` -> `dayofdragons`).
5. Roman numbering will be converted to arabic numbering (`XIV` -> `14`).
6. Unless numbers (years included) are at the end of a name, they will be considered
words. If a number is not in the first position, its entire numeric digits will be
used instead of the acronym of that number's digits (`Left 4 Dead` -> `l4d`). If the
number is in the first position the longhand (words: 5 -> five) representation of the
number will be used to create an acronym (`7 Days to Die` -> `sdtd`). Other examples:
`Team Fortress 2` -> `teamfortress2`, `Unreal Tournament 2003` ->
`unrealtournament2003`.
7. If a game supports multiple protocols, multiple entries will be done for said game
where the edition/protocol name (first disposable in this order) will be appended to
the base game id's: `<game_id><protocol_id>` (where the protocol id will follow all
rules except #2) (Minecraft is mainly divided by 2 editions, Java and Bedrock
which will be `minecraftjava` and `minecraftbedrock` respectively, but it also has
legacy versions, which use another protocol, an example would be the one for `1.6`,
so the name would be `Legacy 1.6` which its id will be `legacy16`, resulting in the
entry of `minecraftlegacy16`). One more entry can be added by the base name of the
game, which queries in a group said supported protocols to make generic queries
easier and disposable.
8. If its actually about a mod that adds the ability for queries to be performed,
process only the mod name.
### Making commits
Where possible please format commits as complete atomic changes that don't rely on
any future commits. Also make sure that the commit message is as descriptive as
possible.
To avoid CI failing when you make a PR you can use our pre-commit hooks: tests that
run before you are able to make a commit (you can skip this at any time by adding
the `-n` flag to `git commit`).
To set this up you need the following programs installed
- [pre-commit](https://pre-commit.com/)
- [rustup](https://rustup.rs/)
- [act](https://github.com/nektos/act) (If you want to test changes to github actions workflows)
Once these are installed you can enable the pre-commit hook by running the following in
the root directory of the repository.
```shell
$ pre-commit install
```
### Priorities
Game suggestions will be prioritized by maintainers based on whether the game
uses a protocol already implemented in the library (games that use already
implemented protocols will be added first), except in the case where a
contribution is made with the protocol needed to implement the game.
The same goes for protocols, if 2 were to be requested, the one implemented in
the most games will be prioritized.
### Releases
Currently, there is no release schedule.
Releases are made when the team decides one will be fitting to be done.

View file

@ -1,14 +1,22 @@
[package]
name = "gamedig"
version = "0.0.0"
edition = "2021"
authors = ["CosminPerRam [cosmin.p@live.com]", "mmorrisontx [https://github.com/mmorrisontx]"]
license-file = "LICENSE.md"
description = "Check out servers with this."
homepage = "https://github.com/CosminPerRam/rust-gamedig"
documentation = "https://github.com/CosminPerRam/rust-gamedig"
repository = "https://github.com/CosminPerRam/rust-gamedig"
readme = "README.md"
keywords = ["server", "valve", "games", "checker", "status"]
[workspace]
members = ["crates/cli", "crates/lib", "crates/id-tests"]
[dependencies]
# Edition 2021, uses resolver = 2
resolver = "2"
[profile.release]
opt-level = 3
debug = false
rpath = true
lto = 'fat'
codegen-units = 1
[profile.release.package."*"]
opt-level = 3
# When building locally, use the local version of the library
# Comment this out when you want to resolve the library from crates.io
# This is only for crates that use gamedig as a dependency
# https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html
[patch.crates-io]
gamedig = { path = "./crates/lib" }

104
GAMES.md Normal file
View file

@ -0,0 +1,104 @@
A supported game is defined as a game that has been successfully tested, other games that are not present here but use
one of the implemented protocols might work too, but that isn't guaranteed.
Beware of the `Notes` column, as it contains information about query port offsets or other query
requirements/information.
# Supported games:
| Game | Use name | Protocol | Notes |
|------------------------------------|---------------------|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Team Fortress 2 | TEAMFORTRESS2 | Valve | |
| The Ship | THESHIP | Valve (*Altered) | |
| Counter-Strike: Global Offensive | CSGO | Valve | The server must have the cvar `host_players_show` set to `2` to get the full player list. |
| Counter-Strike: Source | CSS | Valve | |
| Day of Defeat: Source | DODS | Valve | |
| Left 4 Dead | LEFT4DEAD | Valve | |
| Left 4 Dead 2 | LEFT4DEAD2 | Valve | |
| Half-Life 2 Deathmatch | HL2D | Valve | |
| Alien Swarm | ALIENSWARM | Valve | |
| Alien Swarm: Reactive Drop | ASRD | Valve | |
| Insurgency | INSURGENCY | Valve | |
| Insurgency: Sandstorm | INSURGENCYSANDSTORM | Valve | Query port offset: 1. |
| Insurgency: Modern Infantry Combat | IMIC | Valve | |
| Counter-Strike: Condition Zero | CSCZ | Valve GoldSrc | |
| Day of Defeat | DOD | Valve GoldSrc | |
| Minecraft | MINECRAFT | Proprietary | Bedrock edition provides a different response compared to the Java edition, query specifically for bedrock to get them, otherwise, only matching fields will be provided. |
| 7 Days To Die | SD2D | Valve | |
| ARK: Survival Evolved | ASE | Valve | |
| Unturned | UNTURNED | Valve | |
| The Forest | THEFOREST | Valve GoldSrc | Query port offset: 1. |
| Team Fortress Classic | TFC | Valve | |
| Sven Co-op | SCO | Valve GoldSrc | |
| Rust | RUST | Valve | |
| Counter-Strike | COUNTERSTRIKE | Valve GoldSrc | |
| Arma 2: Operation Arrowhead | A2OA | Valve | Query port offset: 1. |
| Arma 3 | ARMA3 | Valve | |
| Day of Infamy | DOI | Valve | |
| Half-Life Deathmatch: Source | HLDS | Valve | |
| Risk of Rain 2 | ROR2 | Valve | Query port offset: 1. |
| Battalion 1944 | BATTALION1944 | Valve | Query port offset: 3. It is strongly recommended to also query the rules, as it sends basic server info in them. |
| Black Mesa | BLACKMESA | Valve | |
| Project Zomboid | PROJECTZOMBOID | Valve | |
| Age of Chivalry | AOC | Valve | |
| Don't Starve Together | DST | Valve | Query port is 27016. |
| Colony Survival | COLONYSURVIVAL | Valve | |
| Onset | ONSET | Valve | Query port is 7776. |
| Codename CURE | CODENAMECURE | Valve | |
| Ballistic Overkill | BALLISTICOVERKILL | Valve | Query port is 27016. |
| BrainBread 2 | BRAINBREAD2 | Valve | |
| Avorion | AVORION | Valve | Query port is 27020. |
| Operation: Harsh Doorstop | OHD | Valve | Query port is 27005. |
| V Rising | VRISING | Valve | Query port is 27016. |
| Unreal Tournament | UNREALTOURNAMENT | GameSpy 1 | Query Port offset: 1. |
| Battlefield 1942 | B1942 | GameSpy 1 | Query port is 23000. |
| Serious Sam | SERIOUSSAM | GameSpy 1 | Query Port offset: 1. |
| Frontlines: Fuel of War | FFOW | Valve (*Altered) | Query Port offset: 2. |
| Crysis Wars | CRYSISWARS | GameSpy 3 | |
| Quake 2 | QUAKE2 | Quake 2 | |
| Quake 1 | QUAKE1 | Quake 1 | |
| Quake 3: Arena | QUAKE3 | Quake 3 | |
| Hell Let Loose | HLL | Valve Protocol | Query port is 26420. Note that on this port it might not send players data, as there might be another query port that does send players data. |
| Soldier of Fortune 2 | SOF2 | Quake 3 | |
| Halo: Combat Evolved | HCE | GameSpy 2 | |
| Just Cause 2: Multiplayer | JC2M | GameSpy 3 (*Altered) | |
| Warsow | WARSOW | Quake 3 | |
| Creativerse | CREATIVERSE | Valve | Query Port offset: 1. |
| Garry's Mod | GARRYSMOD | Valve | |
| Barotrauma | BAROTRAUMA | Valve | Query Port offset: 1. |
| Valheim | VALHEIM | Valve | Query Port offset: 1. Does not respond to the A2S rules. |
| The Front | THEFRONT | Valve | Responds with wrong values on `name` (gives out a SteamID instead of the server name) and `players_maximum` (always 200). |
| Conan Exiles | CONANEXILES | Valve | Does not respond to the players query. |
| Darkest Hour: Europe '44-'45 | DARKESTHOUR | Unreal2 | Query port offset: 1 |
| Devastation | DEVASTATION | Unreal2 | Query port offset: 1 |
| Killing Floor | KILLINGFLOOR | Unreal2 | Query port offset: 1 |
| Red Orchestra | REDORCHESTRA | Unreal2 | Query port offset: 1 |
| Unreal Tournament 2003 | UT2003 | Unreal2 | Query port offset: 1 |
| Unreal Tournament 2004 | UT2004 | Unreal2 | Query port offset: 1 |
| Post Scriptum | POSTSCRIPTUM | Valve | |
| Squad | SQUAD | Valve | |
| Savage 2 | SAVAGE2 | Proprietary | |
| Rising World | RISINGWORLD | Valve | Query port offset: -1 |
| ATLAS | ATLAS | Valve | Query port offset: 51800 |
| America's Army: Proving Grounds | AAPG | Valve | Query port: 27020. Does not respond to the rules query. |
| Base Defense | BASEDEFENSE | Valve | Query port: 27015. Does not respond to the rules query. |
| Zombie Panic: Source | ZPS | Valve | Query port: 27015. |
| Call Of Duty: Black Ops 3 | CODBO3 | Valve | Query port: 27017. |
| Counter-Strike 2 | COUNTERSTRIKE2 | Valve | |
| Double Action: Boogaloo | DAB | Valve | |
| Mordhau | MORDHAU | Valve | |
| Enshrouded | ENSHROUDED | Valve | |
| Myth of Empires | MOE | Valve | |
| Pirates, Vikings, and Knights II | PVAK2 | Valve | |
| PixARK | PIXARK | Valve | |
| Ark: Survival Ascended | ASA | Epic | Available on the 'tls' feature |
| Aliens vs. Predator 2010 | AVP | Valve | |
| Arma Reforger | ARMAREFORGER | Valve | |
| Nova-Life: Amboise | NLA | Valve | |
| Abiotic Factor | ABIOTICFACTOR | Valve | |
| Soulmask | SOULMASK | Valve | |
| Starbound | STARBOUND | Valve | |
| Minetest | MINETEST | Proprietary | Available on the 'tls', 'serde' and 'services' feature |
## Planned to add support:
_

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 CosminPerRam
Copyright (c) 2022 - 2026 GameDig Organization & Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

18
PROTOCOLS.md Normal file
View file

@ -0,0 +1,18 @@
A protocol is defined as proprietary if it is being used only for a single scope (or series, like Minecraft).
# Supported protocols:
| Name | For | Proprietary? | Documentation reference | Notes |
|---------------------------|-------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Valve Protocol | Games | No | [Server Queries](https://developer.valvesoftware.com/wiki/Server_queries) | In some cases, the players details query might contain some 0-length named players. Multi-packet decompression not tested. |
| Minecraft | Games | Yes | Java: [List Server Protocol](https://wiki.vg/Server_List_Ping) <br> Bedrock: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/minecraftbedrock.js) | |
| GameSpy | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy1.js) Two: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy2.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy3.js) | These protocols are not really standardized, gamedig tries to get the most common fields amongst its supported games, if there are parsing problems, use the `query_vars` function. |
| Quake | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake1.js) Two: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake2.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake3.js) | |
| Just Cause 2: Multiplayer | Games | Yes | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/jc2mp.js) |
| Unreal 2 | Games | No | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/unreal2.js) | Sometimes servers send strings that node-gamedig would treat as latin1 that are UTF-8 encoded, when this happens the remove color code breaks because latin1 decodes the colour sequences differently. Some games provide additional info at the end of the server info packet, this is not currently handled (see the node implementation). Some games use a bot player to denote the team names, this is not currently handled. |
| Savage 2 | Games | Yes | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/savage2.js) | |
| Epic | Games | No | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/epic.js) | Available only on the 'tls' feature. |
## Planned to add support:
_

119
README.md
View file

@ -1 +1,118 @@
# rust-gamedig
<h1 align="center">rust-GameDig</h1>
<h5 align="center">The fast library for querying game servers/services.</h5>
<div align="center">
<a href="https://github.com/gamedig/rust-gamedig/actions">
<img src="https://github.com/gamedig/rust-gamedig/actions/workflows/ci.yml/badge.svg" alt="CI">
</a>
<a href="https://crates.io/crates/gamedig">
<img src="https://img.shields.io/crates/v/gamedig.svg?color=orange" alt="Latest Version">
</a>
<a href="https://crates.io/crates/gamedig">
<img src="https://img.shields.io/crates/d/gamedig?color=purple" alt="Crates.io">
</a>
<a href="https://github.com/gamedig/node-gamedig">
<img src="https://raw.githubusercontent.com/gamedig/rust-gamedig/main/.github/badges/node.svg" alt="Node-GameDig Game Coverage">
</a>
<a href="https://deps.rs/crate/gamedig">
<img src="https://deps.rs/crate/gamedig/latest/status.svg" alt="Rust-GameDig Dependencies">
</a>
</div>
<h5 align="center">
This library brings what
<a href="https://github.com/gamedig/node-gamedig">
node-GameDig
</a>
does (and not only), to pure Rust!
</h5>
**Warning**: This project goes through frequent API breaking changes and hasn't been thoroughly tested.
## Community
Checkout the GameDig Community Discord Server [here](https://discord.gg/NVCMn3tnxH).
Note that it isn't be a replacement for GitHub issues, if you have found a problem
within the library or want to request a feature, it's better to do so here rather than
on Discord.
## Usage
Minimum Supported Rust Version is `1.85.1` and the code is cross-platform.
Pick a game/service/protocol (check the [GAMES](GAMES.md), [SERVICES](SERVICES.md) and [PROTOCOLS](PROTOCOLS.md) files
to see the currently supported ones), provide the ip and the port (be aware that some game servers use a separate port
for the info queries, the port can also be optional if the server is running the default ports) then query on it.
[Team Fortress 2](https://store.steampowered.com/app/440/Team_Fortress_2/) query example:
```rust
use gamedig::games::teamfortress2;
fn main() {
let response = teamfortress2::query(&"127.0.0.1".parse().unwrap(), None);
// None is the default port (which is 27015), could also be Some(27015)
match response { // Result type, must check what it is...
Err(error) => println!("Couldn't query, error: {}", error),
Ok(r) => println!("{:#?}", r)
}
}
```
Response (note that some games have a different structure):
```json5
{
protocol: 17,
name: "Team Fortress 2 Dedicated Server.",
map: "ctf_turbine",
game: "tf2",
appid: 440,
players_online: 0,
players_details: [],
players_maximum: 69,
players_bots: 0,
server_type: Dedicated,
has_password: false,
vac_secured: true,
version: "7638371",
port: Some(27015),
steam_id: Some(69753253289735296),
tv_port: None,
tv_name: None,
keywords: Some(
"alltalk,nocrits"
),
rules: [
"mp_autoteambalance"
:
"1",
"mp_maxrounds"
:
"5",
//....
]
}
```
Want to see more examples? Checkout the [examples](crates/lib/examples) folder.
## Command Line Interface
The library also has an [official CLI](https://crates.io/crates/gamedig_cli) that you can use, it has
MSRV of `1.85.1`.
## Documentation
The documentation is available at [docs.rs](https://docs.rs/gamedig/latest/gamedig/).
Curious about the history and what changed between versions?
Everything is in the changelogs file: [lib](crates/lib/CHANGELOG.md) and [cli](crates/lib/CHANGELOG.md).
## Contributing
If you want to see your favorite game/service being supported here, open an issue, and I'll prioritize it (or do a pull
request if you want to implement it yourself)!
Before contributing please read [CONTRIBUTING](CONTRIBUTING.md).

78
RESPONSES.md Normal file
View file

@ -0,0 +1,78 @@
Every protocol has its own response type(s), below is a listing of the overlapping fields on these responses.
If a cell is blank it doesn't exist, otherwise it contains the type of that data in the current column's response type.
In the case that a field that performs the same function exists in the current column's response type that name is
annotated in brackets.
# Response table
| Field | Generic | GameSpy(1) | GameSpy(2) | GameSpy(3) | Minecraft(Java) | Minecraft(Bedrock) | Valve | Quake | Unreal2 | Epic | Proprietary: FFOW | Proprietary: TheShip | Proprietary: JC2MP | Proprietary: Savage 2 | Proprietary: Minetest |
|----------------------|----------|------------|------------|------------|-----------------|--------------------|---------------|-----------|------------|----------|-------------------|----------------------|--------------------|-----------------------|-----------------------|
| name | `Option` | `String` | `String` | `String` | | `String` | `String` | `String` | `String` | `String` | `String` | `String` | `String` | `String` | `String` |
| description | `Option` | | | | `String` | | | | | | `String` | | `String` | | `String` |
| game_mode | `Option` | `String` | | `String` | | `Option` | `String` | | `String` | | `String` | `String` | | `String` | |
| game_version | `Option` | `String` | | `String` | `String` | | `String` | `String` | | `String` | `String` | `String` | `String` | | `String` |
| map | `Option` | `String` | `String` | `String` | | `Option` | `String` | `String` | `String` | `String` | `String` | `String` | | `String` | |
| players_maxmimum | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u32` | `u32` | `u8` | `u8` | `u32` | `u8` | `u32` |
| players_online | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u32` | `u32` | `u8` | `u8` | `u32` | `u8` | `u32` |
| players_bots | `Option` | | | | | | `u8` | | | | | `u8` | | | |
| has_password | `Option` | `bool` | `bool` | `bool` | | | `bool` | | | `bool` | `bool` | `bool` | `bool` | | `Option` |
| players_minimum | | `Option` | `Option` | `Option` | | | | | | | | | | `u8` | |
| players | | `Vec` | `Vec` | `Vec` | `Option>` | | `Option>` | `Vec ` | `Vec` | `Vec` | | `Vec` | `Vec` | | `Vec` |
| tournament | | `bool` | | `bool` | | | | | | | | | | | |
| unused_entries | | `Hashmap` | | `HashMap` | | | | `HashMap` | | | | | | | |
| teams | | | `Vec` | `Vec` | | | | | | | | | | | |
| protocol_version | | | | | `i32` | `String` | `u8` | | | | `u8` | `u8` | | `String` | |
| server_type | | | | | `Server` | `Server` | `Server` | | | | | `Server` | | | |
| rules | | | | | | | `Option>` | | `HashMap>` | | | `HashMap` | | | |
| environment_type | | | | | | | `Environment` | | | | `Environment` | | | | |
| vac_secured | | | | | | | `bool` | | | | `bool` | `bool` | | | |
| map_title | | `Option` | | | | | | | | | | | | | |
| admin_contact | | `Option` | | | | | | | | | | | | | |
| admin_name | | `Option` | | | | | | | | | | | | | |
| favicon | | | | | `Option` | | | | | | | | | | |
| previews_chat | | | | | `Option` | | | | | | | | | | |
| enforces_secure_chat | | | | | `Option` | | | | | | | | | | |
| edition | | | | | | `String` | | | | | | | | | |
| id | | | | | | `String` | | | `String` | | | | | | `String` |
| the_ship | | | | | | | `Option` | | | | | | | | |
| is_mod | | | | | | | `bool` | | | | | | | | |
| extra_data | | | | | | | `Option` | | | | | | | | |
| mod_data | | | | | | | `Option` | | | | | | | | |
| folder | | | | | | | `String` | | | | | | | | |
| appid | | | | | | | `u32` | | | | | | | | |
| active_mod | | | | | | | | | | | `String` | | | | |
| round | | | | | | | | | | | `u8` | | | | |
| rounds_maximum | | | | | | | | | | | `u8` | | | | |
| time_left | | | | | | | | | | | `u16` | | | | |
| port | | | | | | | | | `u32` | | | `Option` | | | `u32` |
| steam_id | | | | | | | | | | | | `Option` | | | |
| tv_port | | | | | | | | | | | | `Option` | | | |
| tv_name | | | | | | | | | | | | `Option` | | | |
| keywords | | | | | | | | | | | | `Option` | | | |
| mode | | | | | | | | | | | | `u8` | | | |
| witnesses | | | | | | | | | | | | `u8` | | | |
| duration | | | | | | | | | | | | `u8` | | | |
| query_port | | | | | | | | | `u32` | | | | | | |
| ip | | | | | | | | | `String` | | | | | | `String` |
| mutators | | | | | | | | | `HashSet` | | | | | | |
| next_map | | | | | | | | | | | | | | `String` | |
| location | | | | | | | | | | | | | | `String` | |
| level_minimum | | | | | | | | | | | | | | `String` | |
| time | | | | | | | | | | | | | | `String` | |
| creative | | | | | | | | | | | | | | | `Option` |
| damage | | | | | | | | | | | | | | | `bool` |
| game_time | | | | | | | | | | | | | | | `u32` |
| lag | | | | | | | | | | | | | | | `Option` |
| proto_max | | | | | | | | | | | | | | | `u16` |
| proto_min | | | | | | | | | | | | | | | `u16` |
| pvp | | | | | | | | | | | | | | | `bool` |
| uptime | | | | | | | | | | | | | | | `u32` |
| url | | | | | | | | | | | | | | | `Option` |
| update_time | | | | | | | | | | | | | | | `u32` |
| start | | | | | | | | | | | | | | | `u32` |
| clients_top | | | | | | | | | | | | | | | `u32` |
| updates | | | | | | | | | | | | | | | `u32` |
| pop_v | | | | | | | | | | | | | | | `f32` |
| geo_continent | | | | | | | | | | | | | | | `Option` |
| ping | | | | | | | | | | | | | | | `f32` |

10
SERVICES.md Normal file
View file

@ -0,0 +1,10 @@
# Supported services:
| Name | Documentation reference |
|------------------------|-------------------------------------------------------------------------------------------------------|
| Valve Master Server | [Master Server Query Protocol](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol) |
| MineTest Master Server | [Node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/minetest.js) |
## Planned to add support:
TeamSpeak

31
VERSIONS.md Normal file
View file

@ -0,0 +1,31 @@
# MSRV (Minimum Supported Rust Version)
Current: `1.85.1`
Places to update:
- `Cargo.toml`
- `README.md`
- `crates/lib/README.md`
- `.github/workflows/ci.yml`
- `.pre-commit-config.yaml`
# rustfmt version
Current: `1.8.0`
Places to update:
- `.rustfmt.toml`
- The nightly rust version
# The nightly rust version
The toolchain version used to run rustfmt in CI
Current: `nightly-2025-04-19`
Places to update:
- `.github/workflows/ci.yml`
- `.pre-commit-config.yaml`

View file

@ -0,0 +1,10 @@
[profile.release]
opt-level = 'z'
debug = false
rpath = true
lto = 'fat'
codegen-units = 1
strip = 'debuginfo'
[profile.release.package."*"]
opt-level = 'z'

76
crates/cli/CHANGELOG.md Normal file
View file

@ -0,0 +1,76 @@
Who knows what the future holds...
# X.Y.Z - DD/MM/YYYY
Nothing... yet.
# 0.5.0 - 22/02/2026
### Breaking Changes:
- MSRV has been updated to `1.85.1` to match the latest `gamedig` version.
### Changes:
- Updated dependencies
# 0.4.0 - 24/08/2025
### Breaking Changes:
- MSRV has been updated to `1.82.0` to match the latest `gamedig` version.
### Changes:
- Some minor clippy fixes
- Updated dependencies
# 0.3.0 - 23/04/2025
### Changes:
- CLI now uses `gamedig` v0.7.0 (To update, run `cargo install gamedig_cli`).
### Breaking Changes:
- MSRV has been updated to `1.81.0` to match the latest `gamedig` version.
# 0.2.1 - 05/12/2024
Dependencies:
- `gamedig`: `v0.6.0 -> v0.6.1`
# 0.2.0 - 26/11/2024
### Breaking Changes:
- Restructured the release flow to be more consistent (GitHub releases will no longer be available, use cargo instead).
- Changed crate name from `gamedig-cli` to `gamedig_cli` to align with recommended naming conventions.
- The CLI now requires a minimum Rust version of `1.74.1`.
# 0.1.1 - 15/07/2024
### Changes:
- Dependency updates (by @cainthebest)
- `gamedig`: `v0.5.0 -> v0.5.1`
- `clap`: `v4.1.11 -> v4.5.4`
- `quick-xml`: `v0.31.0 -> v0.36.0`
- `webbrowser`: `v0.8.12 -> v1.0.0`
# 0.1.0 - 15/03/2024
### Changes:
- Added the CLI (by @cainthebest).
- Added DNS lookup support (by @Douile).
- Added JSON output option (by @Douile).
- Added BSON output in hex or base64 (by @cainthebest).
- Added XML output option (by @cainthebest).
- Added ExtraRequestSettings as CLI arguments (by @Douile).
- Added TimeoutSettings as CLI argument (by @Douile).
- Added Comprehensive end-user documentation for the CLI interface (by @Douile & @cainthebest).
- Tweaked compile-time flags to allow for a more preformant binary (by @cainthebest).
- Added client for socket capture, dev tools are not included by default (by @Douile).
- Added license information to the CLI (by @cainthebest).
- Added source code information to the CLI (by @cainthebest).

52
crates/cli/Cargo.toml Normal file
View file

@ -0,0 +1,52 @@
[package]
name = "gamedig_cli"
authors = ["rust-GameDig contributors [https://github.com/gamedig/rust-gamedig/contributors]"]
description = "A command line interface for gamedig"
license = "MIT"
version = "0.5.0"
edition = "2021"
default-run = "gamedig_cli"
homepage = "https://gamedig.github.io/"
repository = "https://github.com/gamedig/rust-gamedig"
readme = "README.md"
keywords = ["server", "query", "game", "check", "status"]
rust-version = "1.85.1"
categories = ["command-line-interface"]
[features]
default = ["json", "bson", "xml", "browser"]
# Tools
packet_capture = ["gamedig/packet_capture"]
# Output formats
bson = ["dep:serde", "dep:bson", "dep:hex", "dep:base64", "gamedig/serde"]
json = ["dep:serde", "dep:serde_json", "gamedig/serde"]
xml = ["dep:serde", "dep:serde_json", "dep:quick-xml", "gamedig/serde"]
# Misc
browser = ["dep:webbrowser"]
[dependencies]
# Core Dependencies
thiserror = "2.0.18"
clap = { version = "4.5.60", default-features = false, features = ["derive"] }
gamedig = { version = "0.9.0", default-features = false, features = ["clap", "games", "game_defs"] }
# Feature Dependencies
# Serialization / Deserialization
serde = { version = "1", optional = true, default-features = false }
# BSON
bson = { version = "2.15", optional = true, default-features = false }
base64 = { version = "0.22", optional = true, default-features = false, features = ["std"] }
hex = { version = "0.4.3", optional = true, default-features = false }
# JSON
serde_json = { version = "1", optional = true, default-features = false }
# XML
quick-xml = { version = "0.39.2", optional = true, default-features = false }
# Browser
webbrowser = { version = "1.1.0", optional = true, default-features = false }

21
crates/cli/LICENSE.md Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 - 2026 GameDig Organization & Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

67
crates/cli/README.md Normal file
View file

@ -0,0 +1,67 @@
# Rust GameDig CLI
The official [rust-GameDig](https://crates.io/crates/gamedig) Command Line Interface.
[![CI](https://github.com/gamedig/rust-gamedig/actions/workflows/ci.yml/badge.svg)](https://github.com/gamedig/rust-gamedig/actions) [![License:MIT](https://img.shields.io/github/license/gamedig/rust-gamedig?color=blue)](https://github.com/gamedig/rust-gamedig/blob/main/LICENSE.md) [![node coverage](https://raw.githubusercontent.com/gamedig/rust-gamedig/main/.github/badges/node.svg)](https://github.com/gamedig/node-gamedig)
## Installation
You can install the CLI via `cargo`:
```sh
cargo install gamedig_cli
```
or
```sh
cargo install gamedig_cli --git https://github.com/gamedig/rust-gamedig.git
```
## Usage
Running `gamedig_cli` without any arguments will display the usage information. You can also use the `--help` (or `-h`) flag to see detailed usage instructions.
Here's also a quick rundown of a simple query with the `json-pretty` format:
Pick a game/service/protocol (check
the [GAMES](https://github.com/gamedig/rust-gamedig/blob/main/GAMES.md), [SERVICES](https://github.com/gamedig/rust-gamedig/blob/main/SERVICES.md)
and [PROTOCOLS](https://github.com/gamedig/rust-gamedig/blob/main/PROTOCOLS.md) files to see the currently supported
ones), provide the ip and the port (be aware that some game servers use a separate port for the info queries, the port
can also be optional if the server is running the default ports) then query on it.
[Team Fortress 2](https://store.steampowered.com/app/440/Team_Fortress_2/) query example:
```sh
gamedig_cli query -g teamfortress2 -i 127.0.0.1 -f json-pretty
```
What we are doing here:
- `-g` (or `--game`) specifies the game.
- `-i` (or `--ip`) target ip.
- `-f` (or `--format`) our preferred format.
Note: We haven't specified a port (via `-p` or `--port`), so the default one for the game will be used (`27015` in this
case).
Response (note that some games have a different structure):
```json
{
"name": "A cool server.",
"description": null,
"game_mode": "Team Fortress",
"game_version": "8690085",
"map": "cp_foundry",
"players_maximum": 24,
"players_online": 0,
"players_bots": 0,
"has_password": false,
"players": []
}
```
## Contributing
Please read [CONTRIBUTING](https://github.com/gamedig/rust-gamedig/blob/main/CONTRIBUTING.md).

31
crates/cli/src/error.rs Normal file
View file

@ -0,0 +1,31 @@
pub type Result<T> = std::result::Result<T, Error>;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("IO Error: {0}")]
Io(#[from] std::io::Error),
#[error("Clap Error: {0}")]
Clap(#[from] clap::Error),
#[error("Gamedig Error: {0}")]
Gamedig(#[from] gamedig::errors::GDError),
#[cfg(any(feature = "json", feature = "xml"))]
#[error("Serde Error: {0}")]
Serde(#[from] serde_json::Error),
#[cfg(feature = "bson")]
#[error("Bson Error: {0}")]
Bson(#[from] bson::ser::Error),
#[cfg(feature = "xml")]
#[error("Xml Error: {0}")]
Xml(#[from] quick_xml::Error),
#[error("Unknown Game: {0}")]
UnknownGame(String),
#[error("Invalid hostname: {0}")]
InvalidHostname(String),
}

487
crates/cli/src/main.rs Normal file
View file

@ -0,0 +1,487 @@
use std::net::{IpAddr, ToSocketAddrs};
use clap::{Parser, Subcommand, ValueEnum};
use gamedig::{
games::*,
protocols::types::{CommonResponse, ExtraRequestSettings, TimeoutSettings},
};
mod error;
use self::error::{Error, Result};
const GAMEDIG_HEADER: &str = r"
_____ _____ _ _____ _ _____
/ ____| | __ \(_) / ____| | |_ _|
| | __ __ _ _ __ ___ ___| | | |_ __ _ | | | | | |
| | |_ |/ _` | '_ ` _ \ / _ \ | | | |/ _` | | | | | | |
| |__| | (_| | | | | | | __/ |__| | | (_| | | |____| |____ _| |_
\_____|\__,_|_| |_| |_|\___|_____/|_|\__, | \_____|______|_____|
__/ |
|___/
A command line interface for querying game servers.
Copyright (C) 2022 - 2024 GameDig Organization & Contributors
Licensed under the MIT license
";
// NOTE: For some reason without setting long_about here the doc comment for
// ExtraRequestSettings gets set as the about for the CLI.
#[derive(Debug, Parser)]
#[command(author, version, about = GAMEDIG_HEADER, long_about = None)]
struct Cli {
#[command(subcommand)]
action: Action,
}
#[derive(Subcommand, Debug)]
enum Action {
/// Query game server information
Query {
/// Unique identifier of the game for which server information is being
/// queried.
#[arg(short, long)]
game: String,
/// Hostname or IP address of the server.
#[arg(short, long)]
ip: String,
/// Optional query port number for the server. If not provided the
/// default port for the game is used.
#[arg(short, long)]
port: Option<u16>,
/// Specifies the output format
#[arg(short, long, default_value = "debug", value_enum)]
format: OutputFormat,
/// Which response variant to use when outputting
#[arg(short, long, default_value = "generic")]
output_mode: OutputMode,
/// Optional file path for packet capture file writer
///
/// When set a PCAP file will be written to the location. This file can
/// be read with a tool like wireshark. The PCAP contains a log of the
/// TCP and UDP data sent/recieved by the gamedig library, it does not
/// contain an accurate representation of the real packets sent on the
/// wire as some information has to be hallucinated in order for it to
/// display nicely.
#[cfg(feature = "packet_capture")]
#[arg(short, long)]
capture: Option<std::path::PathBuf>,
/// Optional timeout settings for the server query
#[command(flatten, next_help_heading = "Timeouts")]
timeout_settings: Option<TimeoutSettings>,
/// Optional extra settings for the server query
#[command(flatten, next_help_heading = "Query options")]
extra_options: Option<ExtraRequestSettings>,
},
/// Check out the source code
Source,
/// Display the MIT License information
License,
}
#[derive(Clone, Debug, PartialEq, Eq, ValueEnum)]
enum OutputMode {
/// A generalised response that maps common fields from all game types to
/// the same name.
Generic,
/// The raw result returned from the protocol query, formatted similarly to
/// how the server returned it.
ProtocolSpecific,
}
#[derive(Clone, Debug, PartialEq, Eq, ValueEnum)]
enum OutputFormat {
/// Human readable structured output
Debug,
/// RFC 8259
#[cfg(feature = "json")]
JsonPretty,
/// RFC 8259
#[cfg(feature = "json")]
Json,
/// Parser tries to be mostly XML 1.1 (RFC 7303) compliant
#[cfg(feature = "xml")]
Xml,
/// RFC 4648 section 8
#[cfg(feature = "bson")]
BsonHex,
/// RFC 4648 section 4
#[cfg(feature = "bson")]
BsonBase64,
}
/// Attempt to find a game from the [library game definitions](GAMES) based on
/// its unique identifier.
///
/// # Arguments
/// * `game_id` - A string slice containing the unique game identifier.
///
/// # Returns
/// * Result<&'static [Game]> - On sucess returns a reference to the game
/// definition; on failure returns a [Error::UnknownGame] error.
fn find_game(game_id: &str) -> Result<&'static Game> {
// Attempt to retrieve the game from the predefined game list
GAMES
.get(game_id)
.ok_or_else(|| Error::UnknownGame(game_id.to_string()))
}
/// Resolve an IP address by either parsing an IP address or doing a DNS lookup.
/// In the case of DNS lookup update extra request options with the hostname.
///
/// # Arguments
/// * `host` - A string slice containing the IP address or hostname of a server
/// to resolve.
/// * `extra_options` - Mutable reference to extra options for the game query.
///
/// # Returns
/// * `Result<IpAddr>` - On sucess returns a resolved IP address; on failure
/// returns an [Error::InvalidHostname] error.
fn resolve_ip_or_domain<T: AsRef<str>>(host: T, extra_options: &mut Option<ExtraRequestSettings>) -> Result<IpAddr> {
let host_str = host.as_ref();
if let Ok(parsed_ip) = host_str.parse() {
Ok(parsed_ip)
} else {
set_hostname_if_missing(host_str, extra_options);
resolve_domain(host_str)
}
}
/// Resolve a domain name to one of its IP addresses (the first one returned).
///
/// # Arguments
/// * `domain` - A string slice containing the domain name to lookup.
///
/// # Returns
/// * `Result<IpAddr>` - On success, returns one of the resolved IP addresses;
/// on failure returns an [Error::InvalidHostname] error.
fn resolve_domain(domain: &str) -> Result<IpAddr> {
// Append a dummy port to perform socket address resolution and then extract the
// IP
Ok(format!("{domain}:0")
.to_socket_addrs()
.map_err(|_| Error::InvalidHostname(domain.to_string()))?
.next()
.ok_or_else(|| Error::InvalidHostname(domain.to_string()))?
.ip())
}
/// Sets the hostname on extra request settings if it is not already set.
///
/// # Arguments
/// * `host` - A string slice containing the hostname.
/// * `extra_options` - A mutable reference to optional [ExtraRequestSettings].
fn set_hostname_if_missing(host: &str, extra_options: &mut Option<ExtraRequestSettings>) {
if let Some(extra_options) = extra_options {
if extra_options.hostname.is_none() {
// If extra_options exists but hostname is None overwrite hostname in place
extra_options.hostname = Some(host.to_string())
}
} else {
// If extra_options is None create default settings with hostname
*extra_options = Some(ExtraRequestSettings::default().set_hostname(host.to_string()));
}
}
/// Output the result of a query to stdout.
///
/// # Arguments
/// * `args` - A reference to the command line options.
/// * `result` - A reference to the result of the query.
fn output_result<T: CommonResponse + ?Sized>(output_mode: OutputMode, format: OutputFormat, result: &T) {
match format {
OutputFormat::Debug => {
match output_mode {
OutputMode::Generic => output_result_debug(result.as_json()),
OutputMode::ProtocolSpecific => output_result_debug(result.as_original()),
};
}
#[cfg(feature = "json")]
OutputFormat::JsonPretty => {
let _ = match output_mode {
OutputMode::Generic => output_result_json_pretty(result.as_json()),
OutputMode::ProtocolSpecific => output_result_json_pretty(result.as_original()),
};
}
#[cfg(feature = "json")]
OutputFormat::Json => {
let _ = match output_mode {
OutputMode::Generic => output_result_json(result.as_json()),
OutputMode::ProtocolSpecific => output_result_json(result.as_original()),
};
}
#[cfg(feature = "xml")]
OutputFormat::Xml => {
let _ = match output_mode {
OutputMode::Generic => output_result_xml(result.as_json()),
OutputMode::ProtocolSpecific => output_result_xml(result.as_original()),
};
}
#[cfg(feature = "bson")]
OutputFormat::BsonHex => {
let _ = match output_mode {
OutputMode::Generic => output_result_bson_hex(result.as_json()),
OutputMode::ProtocolSpecific => output_result_bson_hex(result.as_original()),
};
}
#[cfg(feature = "bson")]
OutputFormat::BsonBase64 => {
let _ = match output_mode {
OutputMode::Generic => output_result_bson_base64(result.as_json()),
OutputMode::ProtocolSpecific => output_result_bson_base64(result.as_original()),
};
}
}
}
/// Output the result using debug formatting.
///
/// # Arguments
/// * `result` - A result that can be output using the debug formatter.
fn output_result_debug<R: std::fmt::Debug>(result: R) {
println!("{result:#?}");
}
/// Output the result as a JSON object.
///
/// # Arguments
/// * `result` - A serde serializable result.
#[cfg(feature = "json")]
fn output_result_json<T: serde::Serialize>(result: T) -> Result<()> {
println!("{}", serde_json::to_string(&result)?);
Ok(())
}
/// Output the result as a pretty printed JSON object.
///
/// # Arguments
/// * `result` - A serde serializable result.
#[cfg(feature = "json")]
fn output_result_json_pretty<T: serde::Serialize>(result: T) -> Result<()> {
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}
/// Output the result as an XML object.
/// # Arguments
/// * `result` - A serde serializable result.
#[cfg(feature = "xml")]
fn output_result_xml<T: serde::Serialize>(result: T) -> Result<()> {
use quick_xml::{
events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event},
Writer,
};
use serde_json::Value;
// Serialize the input `result` of generic type `T` into a JSON value.
// This step converts the Rust data structure into a JSON format,
// which will then be used to generate the corresponding XML.
let json = serde_json::to_value(result)?;
// Initialize the XML writer with a new, empty vector to store the XML data.
let mut writer = Writer::new(Vec::new());
// Write the XML 1.1 declaration
writer.write_event(Event::Decl(BytesDecl::new("1.1", Some("utf-8"), None)))?;
// Define a recursive function `json_to_xml` to convert the JSON value into XML
// format. The function takes a mutable reference to the XML writer, an
// optional key as a string slice, and a reference to the JSON value to be
// converted.
fn json_to_xml<W: std::io::Write>(writer: &mut Writer<W>, key: Option<&str>, value: &Value) -> Result<()> {
match value {
// If the JSON value is an object, iterate through its properties,
// creating XML elements with corresponding keys and values.
Value::Object(obj) => {
if let Some(key) = key {
// Start an XML element for the object.
writer.write_event(Event::Start(BytesStart::new(key)))?;
}
for (k, v) in obj {
// Recursively process each property of the object.
json_to_xml(writer, Some(k), v)?;
}
if let Some(key) = key {
// Close the XML element for the object.
writer.write_event(Event::End(BytesEnd::new(key)))?;
}
}
// If the JSON value is an array, iterate through its elements,
// creating XML elements for each item.
Value::Array(arr) => {
for v in arr {
// Use "item" as the default key for array elements without keys.
json_to_xml(writer, key.or(Some("item")), v)?;
}
}
// If the JSON value is null, create an empty XML element.
Value::Null => {
if let Some(key) = key {
writer.write_event(Event::Empty(BytesStart::new(key)))?;
}
}
// For all other JSON value types (String, Number, Bool),
// convert the value to a string and create an XML element with the text content.
// Note: We handle null strings here as well, as they are treated as a string type.
_ => {
if let Some(key) = key {
// Start the XML element with the given key.
writer.write_event(Event::Start(BytesStart::new(key)))?;
}
// Convert the JSON value to a string, trimming quotes for non-string values.
let text_string = match value {
Value::String(s) => s.to_string(),
_ => value.to_string().trim_matches('"').to_string(),
};
// Create a text node with the converted string value.
writer.write_event(Event::Text(BytesText::new(&text_string)))?;
if let Some(key) = key {
// Close the XML element.
writer.write_event(Event::End(BytesEnd::new(key)))?;
}
}
}
Ok(())
}
// Start the root XML element named "data".
writer.write_event(Event::Start(BytesStart::new("data")))?;
// Convert the top-level JSON value to XML.
json_to_xml(&mut writer, None, &json)?;
// Close the root XML element.
writer.write_event(Event::End(BytesEnd::new("data")))?;
// Convert the XML data stored in the writer to a UTF-8 string.
let xml_bytes = writer.into_inner();
let xml_string = String::from_utf8(xml_bytes).expect("Failed to convert XML bytes to UTF-8 string");
println!("{xml_string}");
Ok(())
}
/// Output the result as a BSON object encoded as a hex string.
///
/// # Arguments
/// * `result` - A serde serializable result.
#[cfg(feature = "bson")]
fn output_result_bson_hex<T: serde::Serialize>(result: T) -> Result<()> {
let bson = bson::to_bson(&result)?;
if let bson::Bson::Document(document) = bson {
let bytes = bson::to_vec(&document)?;
println!("{}", hex::encode(bytes));
Ok(())
} else {
panic!("Failed to convert result to BSON Hex (BSON_DOCUMENT_UNAVAILABLE)");
}
}
/// Output the result as a BSON object encoded as a base64 string.
///
/// # Arguments
/// * `result` - A serde serializable result.
#[cfg(feature = "bson")]
fn output_result_bson_base64<T: serde::Serialize>(result: T) -> Result<()> {
use base64::Engine;
let bson = bson::to_bson(&result)?;
if let bson::Bson::Document(document) = bson {
let bytes = bson::to_vec(&document)?;
println!("{}", base64::prelude::BASE64_STANDARD.encode(bytes));
Ok(())
} else {
panic!("Failed to convert result to BSON Base64 (BSON_DOCUMENT_UNAVAILABLE)");
}
}
fn main() -> Result<()> {
let args = Cli::parse();
match args.action {
Action::Query {
game,
ip,
port,
format,
output_mode,
#[cfg(feature = "packet_capture")]
capture,
timeout_settings,
extra_options,
} => {
// Process the query command
let game = find_game(&game)?;
let mut extra_options = extra_options;
let ip = resolve_ip_or_domain(&ip, &mut extra_options)?;
#[cfg(feature = "packet_capture")]
gamedig::capture::setup_capture(capture);
let result = query_with_timeout_and_extra_settings(game, &ip, port, timeout_settings, extra_options)?;
output_result(output_mode, format, result.as_ref());
}
Action::Source => {
println!("{GAMEDIG_HEADER}");
#[cfg(feature = "browser")]
{
// Directly offering to open the URL
println!("\nWould you like to open the GitHub repository in your default browser? [Y/n]");
let mut choice = String::new();
std::io::stdin().read_line(&mut choice).unwrap();
if choice.trim().eq_ignore_ascii_case("Y") {
if webbrowser::open("https://github.com/gamedig/rust-gamedig").is_ok() {
println!("Opening GitHub repository in default browser...");
} else {
println!("Failed to open GitHub repository in default browser.");
println!("Please use the following URL: https://github.com/gamedig/rust-gamedig");
}
} else {
println!("Not to worry, you can always open the repository manually");
println!("by visiting the following URL: https://github.com/gamedig/rust-gamedig");
}
}
#[cfg(not(feature = "browser"))]
{
println!("\nYou can find the source code for this project at the following URL:");
println!("https://github.com/gamedig/rust-gamedig");
}
println!("\nBe sure to leave a star if you like the project :)");
}
Action::License => {
// Bake the license into the binary
// so we don't have to ship it separately
println!("{}", include_str!("../LICENSE.md"));
}
}
Ok(())
}

View file

@ -0,0 +1,29 @@
[package]
name = "gamedig-id-tests"
version = "0.0.1"
edition = "2021"
authors = [
"rust-GameDig contributors [https://github.com/gamedig/rust-gamedig/contributors]",
"node-GameDig contributors [https://github.com/gamedig/node-gamedig/contributors]",
]
license = "MIT"
description = "Test if IDs match the gamedig rules"
homepage = "https://github.com/gamedig/rust-gamedig/CONTRIBUTING.md#naming"
repository = "https://github.com/gamedig/rust-gamedig"
readme = "README.md"
rust-version = "1.65.0"
[features]
cli = ["dep:serde_json", "dep:serde"]
default = ["cli"]
[[bin]]
name = "gamedig-id-tests"
required-features = ["cli"]
[dependencies]
number_to_words = "0.1"
roman_numeral = "0.1"
serde_json = { version = "1", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }

451
crates/id-tests/src/lib.rs Normal file
View file

@ -0,0 +1,451 @@
use std::collections::HashMap;
mod utils;
use utils::{extract_bracketed_suffix, split_on_switch_between_alpha_numeric};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IDRule {
IDsMustBeLowerCase,
NumbersAreTheirOwnWord,
IfFirstWordNumberNoDigits,
IfLastWordNumberMustBeAppended,
ConvertRomanNumeralsToArabic,
TwoWordsOrLessUseFullWords,
MoreThanTwoWordsMakeAcronym,
IfIDDuplicateSameGameAppendYearToNewer,
IfIDDuplicateSameGameAppendProtocol,
IfIDDuplicateNoAcronym,
IfModForQueriesProcessOnlyModName,
NoDuplicates,
}
#[derive(Clone, Debug)]
pub struct IDFail {
pub game_id: String,
pub game_name: String,
pub expected_id: String,
pub rule_stack: Vec<IDRule>,
}
impl IDFail {
fn new(game_id: String, game_name: String, expected_id: String, rule_stack: Vec<IDRule>) -> Self {
Self {
game_id,
game_name,
expected_id,
rule_stack,
}
}
}
/// Test a single game against the rules
pub fn test_game_name_rule(
seen_ids: &mut HashMap<String, Vec<String>>,
id: &str,
mut game: GameNameParsed,
is_mod_name: bool,
) -> Vec<IDFail> {
let mut wrong_ids = Vec::new();
let mut rule_stack = Vec::new();
if is_mod_name {
rule_stack.push(IDRule::IfModForQueriesProcessOnlyModName);
}
let mut suffix = String::new();
// A game's identification is a lowercase alphanumeric string will and be forged
// following these rules:
if id.to_lowercase().ne(id) {
wrong_ids.push(IDFail::new(
id.to_owned(),
game.name.to_owned(),
id.to_lowercase(),
vec![IDRule::IDsMustBeLowerCase],
));
}
// 5. Roman numbering will be converted to arabic numbering (XIV -> 14).
game.words = {
let mut is_first = true;
game.words
.into_iter()
.map(|w| {
// First word will never be a numeral
if is_first {
is_first = false;
w
} else if let Ok(number) = roman_numeral::RomanNumeral::from_string(&w) {
rule_stack.push(IDRule::ConvertRomanNumeralsToArabic);
number.get_u32().to_string()
} else {
w
}
})
.collect()
};
// 6. Unless numbers are at the end of a name, they will be considered words,
// but digits will always be used instead of the acronym (counter to #2)
// (Left 4 Dead -> l4d) unless they at the start position (7 Days to Die ->
// sdtd), if they are at the end (such as sequel number or the year), always
// append them (Team Fortress 2 -> teamfortress2, Unreal Tournament 2003 ->
// unrealtournament2003).
game.words = game
.words
.into_iter()
.flat_map(|w| {
let n = split_on_switch_between_alpha_numeric(&w);
if n.len() > 1 {
rule_stack.push(IDRule::NumbersAreTheirOwnWord);
}
n
})
.collect();
// If first word is number make text
if !game.words.is_empty() && game.words[0].chars().next().unwrap().is_ascii_digit() {
game.words[0] = number_to_words::number_to_words(game.words[0].parse::<f64>().unwrap(), false);
rule_stack.push(IDRule::IfFirstWordNumberNoDigits);
}
// If last word is number append full number
if let Some(last_word) = game.words.last() {
if last_word.chars().all(|c| c.is_ascii_digit()) {
suffix += &game.words.pop().unwrap();
rule_stack.push(IDRule::IfLastWordNumberMustBeAppended);
}
}
let main = if game.words.len() <= 2 {
// 1. Names composed of a maximum of two words (unless #4 applies) will result
// in an id where the words are concatenated (Dead Cells -> deadcells),
// acronyms in the name count as a single word (S.T.A.L.K.E.R. -> stalker).
rule_stack.push(IDRule::TwoWordsOrLessUseFullWords);
game.words
.iter()
.map(|w| w.trim_matches('-').to_owned())
.collect::<Vec<_>>()
.join("")
} else {
// 2. Names of more than two words shall be made into an acronym made of the
// initial letters (The Binding of Isaac -> tboi), hypenation composed words
// don't count as a single word, but of how many parts they are made of (Dino
// D-Day, 3 words, so ddd).
rule_stack.push(IDRule::MoreThanTwoWordsMakeAcronym);
game.words
.iter()
.map(|w| w.chars().next().unwrap())
.filter(|c| c.is_alphanumeric())
.collect()
};
let mut expected_id = format!("{main}{suffix}").to_lowercase();
if let Some(other_game_name_words) = seen_ids.get(&expected_id) {
let mut game_names_same = other_game_name_words.len() == game.words.len();
// Check all words in game name are the same
if game_names_same {
for (our_word, their_word) in game.words.iter().zip(other_game_name_words.iter()) {
if our_word.to_lowercase() != their_word.to_lowercase() {
game_names_same = false;
break;
}
}
}
if game_names_same {
if let Some(year) = game.year {
// 3. If a game has the exact name as a previously existing id's game (Star Wars
// Battlefront 2, the 2005 and 2017 one), append the release year to the
// newer id (2005 would be swbf2 (suppose we already have this one supported)
// and 2017 would be swbf22017).
rule_stack.push(IDRule::IfIDDuplicateSameGameAppendYearToNewer);
expected_id = format!("{expected_id}{year}").to_lowercase();
} else if let Some(protocol) = game.optional_parts.first() {
// 7. If a game supports multiple protocols, multiple entries will be done for
// said game where the edition/protocol name (first disposable in this order)
// will be appended to the game name (Minecraft is divided by 2 editions,
// Java and Bedrock which will be minecraftjava and minecraftbedrock
// respectively) and one more entry can be added by the base name of the game
// which queries in a group said supported protocols to make generic queries
// easier and disposable.
rule_stack.push(IDRule::IfIDDuplicateSameGameAppendProtocol);
// Parse the protocol as a game name so we can remove all non-valid characters
let protocol_parsed = extract_game_parts_from_name(protocol);
expected_id = format!("{}{}", expected_id, protocol_parsed.words.concat(),);
}
}
}
// 4. If a new id (Day of Dragons -> dod) results in an id that already exists
// (Day of Defeat -> dod), then the new name should ignore rule #2 (Day of
// Dragons -> dayofdragons).
if seen_ids.contains_key(&expected_id) {
rule_stack.push(IDRule::IfIDDuplicateNoAcronym);
let main = game
.words
.iter()
.map(|w| w.trim_matches('-').to_owned())
.collect::<Vec<_>>()
.join("");
expected_id = format!("{main}{suffix}").to_lowercase();
}
// 8. If its actually about a mod that adds the ability for queries to be
// performed, process only the mod name.
if !is_mod_name && id != expected_id {
if let Some((_, mod_game)) = game.name.split_once('-') {
let mut result = test_game_name_rule(seen_ids, id, extract_game_parts_from_name(mod_game), true);
if result.is_empty() {
return result;
} else {
wrong_ids.append(&mut result);
}
}
}
let duplicate = if seen_ids.insert(expected_id.clone(), game.words).is_some() {
rule_stack.push(IDRule::NoDuplicates);
true
} else {
false
};
// Check ID matches
if id != expected_id || duplicate {
wrong_ids.push(IDFail::new(
id.to_owned(),
game.name.to_owned(),
expected_id,
rule_stack,
));
}
wrong_ids
}
#[derive(Clone, Debug)]
pub struct GameNameParsed<'a> {
name: &'a str,
words: Vec<String>,
optional_parts: Vec<&'a str>,
year: Option<u16>,
}
pub fn extract_game_parts_from_name(game: &str) -> GameNameParsed<'_> {
// Separate game name into words
// NOTE: we have to leave "-" in to prevent hyphenated prefixes being parsed as
// numerals
let mut optional_game_name_parts = Vec::new();
let (game, paren) = extract_bracketed_suffix(game);
if let Some(paren) = paren {
optional_game_name_parts.push(paren);
}
let mut number_accumulator: Option<String> = None;
// Filter map necessary to move out words
#[allow(clippy::unnecessary_filter_map)]
let game_name_words: Vec<_> = game
// First split all text on space or dash
.split_inclusive(&[' ', '-'])
// Remove whitespace surrounding words (leave in dash because it is important information)
.map(str::trim)
// If a word is entirely surrounded in brackets move it to optional parts
.filter_map(|w| {
if w.starts_with('(') && w.ends_with(')') {
optional_game_name_parts.push(w);
None
} else {
Some(w)
}
})
// Remove all characters that aren't alphanumeric or dashses
.map(|w| {
w.replace(
|c: char| !c.is_ascii_digit() && !c.is_alphabetic() && c != '-',
"",
)
})
// Remove words that are empty (discounting strings that are just dashes)
.filter(|w| !w.trim_matches('-').is_empty())
// Combine numbers that are seperated by dashes
// e.g. 44-45 = 4445
// Panics if there is text after number with trailing dash (44-text)
.filter_map(|w| {
if number_accumulator.is_some() {
if let Some(maybe_number) = w.strip_suffix('-') {
if maybe_number.chars().all(|c| c.is_ascii_digit()) {
number_accumulator.as_mut().unwrap().push_str(maybe_number);
return None;
} else {
panic!("Text after number-");
}
} else if w.chars().all(|c| c.is_ascii_digit()) {
let mut accumulator = number_accumulator.as_ref().unwrap().clone();
number_accumulator = None;
accumulator.push_str(&w);
return Some(accumulator);
} else {
panic!("Text after number-");
}
} else if let Some(maybe_number) = w.strip_suffix('-') {
if maybe_number.chars().all(|c| c.is_ascii_digit()) {
number_accumulator = Some(maybe_number.to_string());
return None;
}
}
Some(w)
})
.collect();
let mut game_year: Option<u16> = None;
for optional_part in &optional_game_name_parts {
if let Some(game_year_text) = optional_part
.strip_prefix('(')
.and_then(|s| s.strip_suffix(')'))
{
if let Ok(year) = game_year_text.parse() {
game_year = Some(year);
break;
}
} else if let Ok(year) = optional_part.parse() {
game_year = Some(year);
break;
}
}
GameNameParsed {
name: game,
words: game_name_words,
optional_parts: optional_game_name_parts,
year: game_year,
}
}
/// Iterate game entries and validate the id matches current rules
pub fn test_game_name_rules<'a, I: Iterator<Item = (&'a str, &'a str)>>(games: I) -> Vec<IDFail> {
let mut wrong_ids = Vec::with_capacity(games.size_hint().0);
let mut seen_ids: HashMap<String, Vec<String>> = HashMap::new();
// We must sort games by year so that rule 3 is applied correctly
let mut sorted_games: Vec<_> = games
.map(|(id, game)| {
let game = extract_game_parts_from_name(game);
(id, game)
})
.collect();
sorted_games.sort_by(|(_, a_game), (_, b_game)| {
a_game
.year
.cmp(&b_game.year)
.then(a_game.name.len().cmp(&b_game.name.len()))
});
let game_count = sorted_games.len();
for (id, game) in sorted_games {
wrong_ids.append(&mut test_game_name_rule(&mut seen_ids, id, game, false))
}
if !wrong_ids.is_empty() {
for fail in &wrong_ids {
println!("{fail:#?}");
}
let percentage = (wrong_ids.len() * 100) / game_count;
println!(
"{} ({}%) IDs didn't match naming rules",
wrong_ids.len(),
percentage
);
}
wrong_ids
}
pub fn test_single_game_rule(id: &str, name: &str) -> Vec<IDFail> { test_game_name_rules(std::iter::once((id, name))) }
#[cfg(test)]
mod id_tests {
use super::{test_game_name_rules, test_single_game_rule};
#[test]
fn id_rule_one() {
assert!(test_single_game_rule("testgame", "Test Game").is_empty());
assert!(test_single_game_rule("testgame", "TestGame").is_empty());
assert!(test_single_game_rule("deadcells", "Dead Cells").is_empty());
assert!(test_single_game_rule("stalker", "S.T.A.L.K.E.R").is_empty());
}
#[test]
fn id_rule_two() {
assert!(test_single_game_rule("tgt", "Test Game Three").is_empty());
assert!(test_single_game_rule("tgt", "Test Game-Three").is_empty());
assert!(test_single_game_rule("tboi", "The Binding of Isaac").is_empty());
assert!(test_single_game_rule("ddd", "Dino D-Day").is_empty());
}
#[test]
fn id_rule_three() {
let games = vec![
("swb22017", "Star Wars Battlefront 2 (2017)"),
("swb2", "Star Wars Battlefront 2 (2015)"),
];
assert!(test_game_name_rules(games.into_iter()).is_empty());
}
#[test]
fn id_rule_four() {
let games = vec![("dod", "Day of Defeat"), ("dayofdragons", "Day of Dragons")];
assert!(test_game_name_rules(games.into_iter()).is_empty());
}
#[test]
fn id_rule_five() {
assert!(test_single_game_rule("gta14", "Grand Theft Auto XIV").is_empty());
}
#[test]
fn id_rule_six() {
assert!(test_single_game_rule("l4d", "Left 4 Dead").is_empty());
assert!(test_single_game_rule("sdtd", "7 Days to Die").is_empty());
assert!(test_single_game_rule("teamfortress2", "Team Fortress 2").is_empty());
assert!(test_single_game_rule("unrealtournament2003", "Unreal Tournament 2003").is_empty());
assert!(test_single_game_rule("dhe4445", "Darkest Hour: Europe '44-'45").is_empty());
}
#[test]
fn id_rule_seven() {
let games = vec![
("minecraft", "Minecraft"),
("minecraftjava", "Minecraft (java)"),
("minecraftbedrock", "Minecraft (bedrock)"),
];
assert!(test_game_name_rules(games.into_iter()).is_empty());
}
#[test]
fn id_rule_eight() {
assert!(test_single_game_rule("fivem", "Grand Theft Auto V - FiveM (2013)").is_empty());
assert!(test_single_game_rule("jc3m", "Just Cause 3 - Multiplayer").is_empty());
}
}

View file

@ -0,0 +1,32 @@
#![cfg(feature = "cli")]
use std::collections::HashMap;
/// Format for input games (the same as used in node-gamedig/lib/games.js).
type GamesInput = HashMap<String, Game>;
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
struct Game {
name: String,
}
use gamedig_id_tests::test_game_name_rules;
fn main() {
let games: GamesInput = std::env::args_os().nth(1).map_or_else(
|| serde_json::from_reader(std::io::stdin().lock()).unwrap(),
|file| {
let file = std::fs::OpenOptions::new().read(true).open(file).unwrap();
serde_json::from_reader(file).unwrap()
},
);
let failed = test_game_name_rules(
games
.iter()
.map(|(key, game)| (key.as_str(), game.name.as_str())),
);
assert!(failed.is_empty());
}

View file

@ -0,0 +1,66 @@
/// Split a str when characters swap between being digits and not digits.
pub fn split_on_switch_between_alpha_numeric(text: &str) -> Vec<String> {
if text.is_empty() {
return vec![];
}
let mut parts = Vec::with_capacity(text.len());
let mut current = Vec::with_capacity(text.len());
let mut iter = text.chars();
let c = iter.next().unwrap();
let mut last_was_numeric = c.is_ascii_digit();
current.push(c);
for c in iter {
if c.is_ascii_digit() == last_was_numeric {
current.push(c);
} else {
parts.push(current.iter().collect());
current.clear();
current.push(c);
last_was_numeric = !last_was_numeric;
}
}
parts.push(current.into_iter().collect());
parts
}
#[test]
fn split_correctly() {
assert_eq!(
split_on_switch_between_alpha_numeric("2D45A"),
&["2", "D", "45", "A"]
);
}
#[test]
fn split_symbol_broken_numbers() {
let game_name = super::extract_game_parts_from_name("Darkest Hour: Europe '44-'45");
assert_eq!(game_name.words, &["Darkest", "Hour", "Europe", "4445"]);
}
/// Extract parts at end of string enclosed in brackets.
pub fn extract_bracketed_suffix(text: &str) -> (&str, Option<&str>) {
if let Some(text) = text.strip_suffix(')') {
if let Some((text, extra)) = text.rsplit_once('(') {
return (text, Some(extra));
}
}
(text, None)
}
#[test]
fn extract_brackets_correctly() {
assert_eq!(
extract_bracketed_suffix("no brackets here"),
("no brackets here", None)
);
assert_eq!(
extract_bracketed_suffix("Game name (with protocol here)"),
("Game name ", Some("with protocol here"))
);
}

674
crates/lib/CHANGELOG.md Normal file
View file

@ -0,0 +1,674 @@
Who knows what the future holds...
# X.Y.Z - DD/MM/YYYY
# 0.9.0 22/02/2026
Breaking:
- MSRV is now `1.85.1` (was `1.82.0`), this is due to deps we rely on requiring a higher version
Games:
- Fixed minecraft java server side EncoderException error on query (by @paul-hansen).
Crate:
- Some minor clippy fixes
- Updated some dependencies
# 0.8.0 24/08/2025
Breaking:
- MSRV is now `1.82.0` (was `1.81.0`), this is due to deps we rely on requiring a higher version
Crate:
- Some minor clippy fixes
- Updated dependencies
# 0.7.0 - 23/04/2025
Breaking:
- MSRV is now `1.81.0` (was `1.71.1`), this is due to deps we rely on requiring a higher version
Games:
- Added `Arma 3` support (by @Perondas).
Crate:
- Some minor clippy fixes
# 0.6.1 - 05/12/2024
Games:
- Added `Starbound` support (by @Novaenia).
Protocols:
- Fixed enum cast error on valve when parsing uppercase envrionment and server type fields (by @Novaenia).
# 0.6.0 - 26/11/2024
Breaking:
- MSRV is now `1.71.1` (was `1.65.0`), this is due to deps we rely on requiring a higher version on linux builds (`1.65.0` is 2+ years old).
Games:
- [Minetest](https://www.minetest.net/) support (available on the `tls`, `serde` and `services` features) (#218 by
@CosminPerRam).
- Fixed the forest game failing when host has the client steam id (#232 by @paul-hansen).
# 0.5.2 - 20/10/2024
Games:
- [Soulmask](https://store.steampowered.com/app/2646460/Soulmask/) support (by @CosminPerRam).
Protocols:
- Fixed Epic (EOS) protocol to match ports on query (by @cainthebest).
Services:
- MineTest Master Server support (available only on the `tls` and `serde` feature) (by @CosminPerRam).
Crate:
- Performance improvements from clippy suggestions (by @CosminPerRam).
- Feature gate some variables so that they are not unused (by @cainthebest).
- Fixed a OOB panic that could occur when reading strings from the buffer (by @cainthebest).
- Updated `pnet_packet` from `0.34.0` to `0.35.0`.
# 0.5.1 - 12/05/2024
Games:
- [Mordhau](https://store.steampowered.com/app/629760/MORDHAU/) support.
- [Enshrouded](https://store.steampowered.com/app/1203620/Enshrouded/) support.
- [Myth of Empires](https://store.steampowered.com/app/1371580/Myth_of_Empires/) support.
- [Pirates, Vikings, and Knights II](https://store.steampowered.com/app/17570/Pirates_Vikings_and_Knights_II/) support.
- [PixARK](https://store.steampowered.com/app/593600/PixARK/) support.
- [Ark: Survival Ascended](https://store.steampowered.com/app/2399830/ARK_Survival_Ascended/) support, note: not yet in
the games definitions.
- [Aliens vs. Predator 2010](https://store.steampowered.com/app/10680/Aliens_vs_Predator/) support.
- [Arma Reforger](https://store.steampowered.com/app/1874880/Arma_Reforger/) support.
- [Nova-Life: Amboise](https://store.steampowered.com/app/885570/NovaLife_Amboise/) support.
- [Abiotic Factor](https://store.steampowered.com/app/427410/Abiotic_Factor/) support.
Protocols:
- Epic (EOS) support, available only on the `tls` feature.
Crate:
- Updated some dependencies: `crc32fast` to `1.4.0`, `clap` to `4.5.4` and `ureq` to `ureq`.
# 0.5.0 - 15/03/2024
### Changes:
Games:
- [Valheim](https://store.steampowered.com/app/892970/Valheim/) support.
- [The Front](https://store.steampowered.com/app/2285150/The_Front/) support.
- [Conan Exiles](https://store.steampowered.com/app/440900/Conan_Exiles/) support.
- [Post Scriptum](https://store.steampowered.com/app/736220/Post_Scriptum/) support.
- [Squad](https://store.steampowered.com/app/393380/Squad/) support.
- [Savage 2](https://savage2.net/) support.
- [Rising World](https://store.steampowered.com/app/324080/Rising_World/) support.
- [ATLAS](https://store.steampowered.com/app/834910/ATLAS/) support.
- [America's Army: Proving Grounds](https://store.steampowered.com/app/203290/Americas_Army_Proving_Grounds/) support.
- [Base Defense](https://store.steampowered.com/app/632730/Base_Defense/) support.
- [Zombie Panic: Source](https://store.steampowered.com/app/17500/Zombie_Panic_Source/) support.
- Added a valve protocol query example.
- Made all of Just Cause 2: Multiplayer Response and Player fields public.
- [Mindustry](https://mindustrygame.github.io/) support.
- [Eco](https://store.steampowered.com/app/382310/Eco/) support.
- [Call Of Duty: Black Ops 3](https://store.steampowered.com/agecheck/app/311210/) support.
- [Counter-Strike 2](https://store.steampowered.com/app/730/CounterStrike_2/) support.
- [Double Action: Boogaloo](https://store.steampowered.com/app/317360/Double_Action_Boogaloo/) support.
Crate:
- Changed the serde feature to only enable serde derive for some types: serde and serde_json is now a dependecy by
default.
Protocols:
- Added the unreal2 protocol and its associated games: Darkest Hour, Devastation, Killing Floor, Red Orchestra, Unreal
Tournament 2003, Unreal Tournament 2004 (by @Douile).
- Added HTTPClient to allow use of HTTP(S) (and JSON) APIs (by @CosminPerRam & @Douile).
Crate:
- Added a `packet_capture` feature to capture the raw packets sent and received by the socket (by @Douile).
- Added packet emulation and socket retrevial using the `packet_capture` feature (by @Douile).
- Added PCAP writing support to the `packet_capture` feature (by @Douile & @cainthebest).
- Refactored socket to use a custom implementation of socket for packet capture when using the `packet_capture`
feature (by @Douile).
CLI:
- Added a CLI (by @cainthebest).
- Added DNS lookup support (by @Douile).
- Added JSON output option (by @Douile).
- Added BSON output in hex or base64 (by @cainthebest).
- Added XML output option (by @cainthebest).
- Added ExtraRequestSettings as CLI arguments (by @Douile).
- Added TimeoutSettings as CLI argument (by @Douile).
- Added Comprehensive end-user documentation for the CLI interface (by @Douile & @cainthebest).
- Tweaked compile-time flags to allow for a more preformant binary (by @cainthebest).
- Added client for socket capture, dev tools are not included by default (by @Douile).
- Added license information to the CLI (by @cainthebest).
- Added source code information to the CLI (by @cainthebest).
### Breaking:
Game:
- Changed identifications of the following games as they weren't properly expecting the naming rules:
-
- Left 4 Dead: `left4dead` -> `l4d`.
-
- 7 Days to Die: `7d2d` in definitions and `sd2d` in game declaration -> `sdtd`.
-
- Quake 3 Arena: `quake3arena` -> `q3a`.
-
- Unreal tournament 2003: `ut2003` -> `unrealtournament2003`
-
- Unreal tournament 2004: `ut2004` -> `unrealtournament2004`
-
- Darkest Hour: Europe '44-'45: `darkesthour` -> `dhe4445`
- Minecraft:
-
- Legacy 1.5 and 1.3 were renamed to 1.4 and beta 1.8 respectively to show the lowest version they support, this
change includes Structs, Enum and game id renames, also removed the "v" from the game definition name.
-
- Moved the Minecraft protocol implementation in the games folder as its proprietary.
Protocols:
- Valve: Removed `SteamApp` due to it not being really useful at all, replaced all instances with `Engine`.
Query:
- Added a connection timeout to TimeoutSettings (at the moment this only applies to TCP)
- Sockets are now expected to apply timeout settings in new()
# 0.4.1 - 13/10/2023
### Changes:
Game:
- Added [Barotrauma](https://store.steampowered.com/app/602960/Barotrauma/) support.
Crate:
- Added `Send` and `Sync` on `Error::source` to fix some async issues.
Protocols:
- Minecraft Java: Add derives to `RequestSettings` and add `new_just_hostname` that creates new settings just by
specifying
the hostname, `protocol_version` defaults to -1.
Games:
- Organised game modules into protocols (when protocol used by other games),
you can now access a game by its name or by its protocol name:
- `use gamedig::games::teamfortress2;`
- `use gamedig::games::valve::teamfortress2;`
Generics:
- Added standard derives to `ProprietaryProtocol`, `CommonResponseJson`, `CommonPlayerJson`, `TimeoutSettings` and
`ExtraRequestSettings`.
### Breaking...
None, yaay!
# 0.4.0 - 07/10/2023
### Changes:
Games:
- [Creativerse](https://store.steampowered.com/app/280790/Creativerse/) support.
Protocols:
- Quake 2: Fixed a bug where the version tag wouldn't always be present.
- The Ship: Removed instances of using `unwrap` without handling the panics.
Crate:
- Updated [byteorder](https://crates.io/crates/byteorder) dependency from 1.4 to 1.5.
- Rich errors, capturing backtrace is done on `RUST_BACKTRACE=1`. (by @Douile)
- Applied some nursery Clippy lints.
- The `retries` field was added to `TimeoutSettings` that specifies the number of times to retry a failed request (
request being individual send, receive sequence, some protocols can include multiple requests in a single query). (by
@Douile)
- By default `retries` is set to `0`, meaning no retries will be attempted
Generics:
- Added `ExtraRequestSettings` containing all possible extra request settings. (by @Douile)
- Added `query_with_timeout_and_extra_settings()` to allow generic queries with extra settings. (by @Douile)
### Breaking...
Crate:
- The enum used for errors, `GDError` has been renamed to `GDErrorKind`.
- `GDError` is now a struct that holds its kind, the source and a backtrace.
- The `Socket::apply_timeout` method now borrows `TimeoutSettings` (`&Option<TimeoutSettings>`)
- To make this easier to work with a new method was added
to `TimeoutSettings`: `TimeoutSettings::get_read_and_write_or_defaults` this takes a borrowed
optional `TimeoutSettings` and returns the contained read and write durations or the default read and write
durations.
Generics:
- Renamed `CommonResponseJson`'s `game` field (and the function) to `game_mode`.
- Changed `players_maximum` and `players_online` (and their functions) types from `u64` to `u32`.
- Changed `score` type (and the function) of player from `u32` to `i32`.
Games:
- Rename some game definitions and implementations to follow a stable ID naming system.
Protocols:
- Valve:
1. Renamed `protocol` to `protocol_version`.
2. Renamed `version` to `game_version`.
3. Renamed `game` to `game_mode`.
4. Fixed `player`'s `score` field being `u32` when it needed to be `i32`, as specified in the protocol.
5. Added the field `check_app_id` to `GatherSettings` which controls if the app id specified to the request and
reported by the server are the same, errors if not, enabled by default. (by @Douile)
6. Valve: Renamed SteamApp enum variants to match new definition names
- GameSpy (1, 2, 3):
1. Renamed `version` to `game_version`.
2. Changed `players_maximum` and `players_online` (and their functions) types from `usize` to `u32`.
- GameSpy 1:
1. Renamed the player's `frags` to `score` and type from `u32` to `i32`.
2. Made `Option` the following response fields `team`, `face`, `skin`, `mesh` and `secret` to fix missing fields
issues. (by @Douile)
- Quake (1, 2):
1. Renamed `game_type` to `game_mode`.
2. Changed `version` type from `String`to `Option<String>`.
- Minecraft Java
1. Renamed `version_protocol` to `protocol_version`.
2. Renamed `version_name` to `game_version`.
3. Renamed `players_sample` to `players`.
4. Added an optional parameter, `RequestSettings`, which contains fields that are used when creating the handshake
packet (this solves some servers not responding to the query). (by @Douile)
5. Legacy versions naming has been changed to represent up to what version they can query, `LegacyBV1_8` (Beta 1.8 to
1.3) -> `LegacyV1_3` and `LegacyV1_4` (1.4 to 1.5) -> `LegacyV1_5` (and their enums accordingly).
- Minecraft Bedrock
1. Renamed `version_protocol` to `protocol_version`.
- Minecraft:
1. Added an optional parameter, `request_settings` parameter to `query`.
- The Ship:
1. Renamed `protocol` to `protocol_version`.
2. Renamed `max_players` to `players_maximum` and changed its type from `u64` to `u32`.
3. Renamed `bots` to `players_bots`. and changed its type from `u64` to `u32`.
4. Renamed `players` to `players_online`.
5. Renamed `players_details` to `players`.
6. Renamed `game` to `game_mode`.
7. Added field `game_version`.
8. Changed `players_bots` type from `Option<u64>` to `Option<u32>`.
9. Changed `score` type of player from `u32` to `i32`.
- Frontlines: Fuel of War:
1. Renamed `game_mode` to `game`.
2. Renamed `version` to `game_version`.
3. Renamed `protocol` to `protocol_version`.
4. Renamed `game` to `game_mode`.
5. Changed `players_maximum` and `players_minimum` types from `usize` to `u32`.
- Just Cause 2: Multiplayer:
1. Renamed `version` to `game_version`.
2. Changed `players_maximum` and `players_minimum` types from `usize` to `u32`.
# 0.3.0 - 18/07/2023
### Changes:
Protocols:
- GameSpy 2 support.
- Quake 2: Added Optional address field to Player.
Generic query:
- Added generic queries (by [@Douile](https://github.com/Douile)) which come with a common struct for the response
fields.
- The supported games list is available programmatically.
Games:
- [Halo: Combat Evolved](https://en.wikipedia.org/wiki/Halo:_Combat_Evolved) support.
- [Just Cause 2: Multiplayer](https://store.steampowered.com/app/259080/Just_Cause_2_Multiplayer_Mod/) support.
- [Warsow](https://warsow.net/) support.
Internal:
- Buffer reader rewrite, resulting in more data checks and better code quality (
thanks [@cainthebest](https://github.com/cainthebest)).
- Better CI to never break accidentally MSRV again (thanks [@Douile](https://github.com/Douile)).
### Breaking...
Protocols:
- Quake 2: Renamed the players `frags` field to `score` to be more inline with the other protocols.
Crate:
- `no_games` and `no_services` have been changed to `games` and `services`, this better represents that they are present
by default (by [@Douile](https://github.com/Douile)).
- Fixed crate's `rust-version`, it is now `1.60.0` (was `1.56.1`)
# 0.2.3 - 02/06/2023
### Changes:
Protocols:
- Valve:
1. Added standard and serde derives to `GatheringSettings`.
- Quake 1, 2 and 3 support.
Games:
- [Quake 2](https://store.steampowered.com/app/2320/Quake_II/) support.
- [Quake 1](https://store.steampowered.com/app/2310/Quake/) support.
- [Quake 3: Arena](https://store.steampowered.com/app/2200/Quake_III_Arena/) support.
- [Hell Let Loose](https://store.steampowered.com/app/686810/Hell_Let_Loose/) support.
- [Soldier of Fortune 2](https://www.gog.com/en/game/soldier_of_fortune_ii_double_helix_gold_edition) support.
### Breaking:
- Every function that used `&str` for the address has been changed to `&IpAddr` (
thanks [@Douile](https://github.com/Douile) for the re-re-write).
- Protocols now use `&SocketAddr` instead of `address: &str, port: u16`.
Services:
- Valve Master Query:
1. Removed Filter and SearchFilters lifetimes and changed `&'a str` to `String` and `&'a [&'a str]` to `Vec<String>`
# 0.2.2 - 01/05/2023
### Changes:
Crate:
- General optimizations thanks to [cargo clippy](https://github.com/rust-lang/rust-clippy)
and [@cainthebest](https://github.com/cainthebest).
- Added feature `serde` which enables json serialization/deserialization for all types (
by [@cainthebest](https://github.com/cainthebest)).
- Documentation improvements.
Protocols:
- GameSpy 1: Add key `admin` as a possible variable for `admin_name`.
- GameSpy 3 support.
Games:
- [Serious Sam](https://www.gog.com/game/serious_sam_the_first_encounter) support.
- [Frontlines: Fuel of War](https://store.steampowered.com/app/9460/Frontlines_Fuel_of_War/) support.
- [Crysis Wars](https://steamcommunity.com/app/17340) support.
Services:
- [Valve Master Server Query](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol) support.
- Added feature `no_services` which disables the supported services.
### Breaking:
Protocols:
- Valve: Request type enums have been renamed from all caps to starting-only uppercase, ex: `INFO` to `Info`
- GameSpy 1: `players_minimum` is now an `Option<u8>` instead of an `u8`
- GameSpy 1: Is now under `protocols::gamespy::one` instead of `protocols::gamespy`
# 0.2.1 - 03/03/2023
### Changes:
Crate:
- Added feature `no_games` which disables the supported games (useful when only the
protocols/services are needed, also saves storage space).
Games:
- [V Rising](https://store.steampowered.com/app/1604030/V_Rising/) support.
- [Unreal Tournament](https://en.wikipedia.org/wiki/Unreal_Tournament) support.
- [Battlefield 1942](https://www.ea.com/games/battlefield/battlefield-1942) support.
Protocols:
- Valve:
1. Reversed (from `0.1.0`) "Players with no name are no more added to the `players_details` field.", also added a note
in the [protocols](PROTOCOLS.md) file regarding this.
2. Fixed querying while multiple challenge responses might happen.
- GameSpy 1 support.
### Breaking:
None.
# 0.2.0 - 18/02/2023
### Changes:
Games:
- [Don't Starve Together](https://store.steampowered.com/app/322330/Dont_Starve_Together/) support.
- [Colony Survival](https://store.steampowered.com/app/366090/Colony_Survival/) support.
- [Onset](https://store.steampowered.com/app/1105810/Onset/) support.
- [Codename CURE](https://store.steampowered.com/app/355180/Codename_CURE/) support.
- [Ballistic Overkill](https://store.steampowered.com/app/296300/Ballistic_Overkill/) support.
- [BrainBread 2](https://store.steampowered.com/app/346330/BrainBread_2/) support.
- [Avorion](https://store.steampowered.com/app/445220/Avorion/) support.
- [Operation: Harsh Doorstop](https://store.steampowered.com/app/736590/Operation_Harsh_Doorstop/) support.
Protocols:
- Valve:
1. `appid` is now a field in the `Response` struct.
### Breaking:
Protocols:
- Valve:
due to some games being able to host a server from within the game AND from a dedicated server,
if you were to query one of them, the query would fail for the other one, as the `SteamID` enum
for that game could specify only one id.
1. `SteamID` is now `SteamApp`, was an u32 enum, and now it's a simple enum.
2. `App` is now `Engine`, the `Source` enum's structure has been changed from `Option<u32>` to
`Option<u32, Option<u32>>`, where the first parameter is the game app id and the second is
the dedicated server app id (if there is one).
# 0.1.0 - 17/01/2023
### Changes:
Games:
- [Risk of Rain 2](https://store.steampowered.com/app/632360/Risk_of_Rain_2/) support.
- [Battalion 1944](https://store.steampowered.com/app/489940/BATTALION_Legacy/) support.
- [Black Mesa](https://store.steampowered.com/app/362890/Black_Mesa/) support.
- [Project Zomboid](https://store.steampowered.com/app/108600/Project_Zomboid/) support.
- [Age of Chivalry](https://store.steampowered.com/app/17510/Age_of_Chivalry/) support.
Protocols:
- Valve: Players with no name are no more added to the `players_details` field.
- Valve: Split packets are now appending in the correct order.
Crate:
- `MSRV` is now `1.56.1` (was `1.58.1`)
### Breaking:
Protocols:
- Valve: The rules field is now a `HashMap<String, String>` instead of a `Vec<ServerRule>` (where the `ServerRule`
structure had a name and a value fields).
- Valve: Structs that contained the `players`, `max_players` and `bots` fields have been renamed
to `players_online`, `players_maximum` and `players_bots` respectively.
- Minecraft: Structs that contained the `online_players`, `max_players` and `sample_players` fields have been renamed
to `players_online`, `players_maximum` and `players_sample` respectively.
- Minecraft: The Java query response struct named `Response` has been renamed to `JavaResponse`.
Errors:
- Besides the `BadGame` error, now no other errors returns details about what happened (as it was quite pointless).
Crate:
- `package.metadata.msrv` has been replaced with `package.rust-version`
# 0.0.7 - 03/01/2023
### Changes:
[Minecraft](https://www.minecraft.com) bedrock edition support.
Fix Minecraft legacy v1.6 max/online players count being reversed.
Added `query_legacy_specific` method to the Minecraft protocol.
### Breaking:
Removed `query_specific` from the mc protocol in favor of `query_java`, `query_legacy` and `query_legacy_specific`.
Some public functions that are meant to be used only internally were made private.
# 0.0.6 - 28/11/2022
[Minecraft](https://www.minecraft.com) support (bedrock not supported yet).
[7 Days To Die](https://store.steampowered.com/app/251570/7_Days_to_Die/) support.
[ARK: Survival Evolved](https://store.steampowered.com/app/346110/ARK_Survival_Evolved/) support.
[Unturned](https://store.steampowered.com/app/304930/Unturned/) support.
[The Forest](https://store.steampowered.com/app/242760/The_Forest/) support.
[Team Fortress Classic](https://store.steampowered.com/app/20/Team_Fortress_Classic/) support.
[Sven Co-op](https://store.steampowered.com/app/225840/Sven_Coop/) support.
[Rust](https://store.steampowered.com/app/252490/Rust/) support.
[Counter-Strike](https://store.steampowered.com/app/10/CounterStrike/) support.
[Arma 2: Operation Arrowhead](https://store.steampowered.com/app/33930/Arma_2_Operation_Arrowhead/) support.
[Day of Infamy](https://store.steampowered.com/app/447820/Day_of_Infamy/) support.
[Half-Life Deathmatch: Source](https://store.steampowered.com/app/360/HalfLife_Deathmatch_Source/) support.
Successfully tested `Alien Swarm` and `Insurgency: Modern Infantry Combat`.
Restored rules response for `Counter-Strike: Global Offensive` (note: for a full player list response, the
cvar `host_players_show` must be set to `2`).
Increased Valve Protocol `PACKET_SIZE` from 1400 to 6144 (because some games send larger packets than the specified
protocol size).
Removed DNS resolving as it was not needed.
Valve Protocol minor optimizations.
# 0.0.5 - 15/11/2022
Added `SocketBind` error, regarding failing to bind a socket.
Socket custom timeout capability (with an error if provided durations are zero).
Because of this, a parameter similar to GatherSettings has been added on the Valve Protocol Query.
Support for GoldSrc split packets and obsolete A2S_INFO response.
Changed the Valve Protocol app parameter to represent the engine responses.
It is now an enum of:
- `Source(Option<u32>)` - A Source response with optionally, the id (if the id is present and the response id is not the
same, the query fails), if it isn't provided, find it.
- `GoldSrc(bool)` - A GoldSrc response with the option to enforce the obsolete A2S_INFO response.
Fixed Source multi-packet response crash due to when a certain app with a certain protocol doesn't have the Size
field.
Reduced Valve Protocol `PACKET_SIZE` to be as specified from 2048 to 1400.
[Counter-Strike: Condition Zero](https://store.steampowered.com/app/80/CounterStrike_Condition_Zero/) implementation.
[Day of Defeat](https://store.steampowered.com/app/30/Day_of_Defeat/) implementation.
Games besides CSGO and TS now have the same response structure.
# 0.0.4 - 23/10/2022
Queries now support DNS resolve.
Changed uses a bit, example: from `use gamedig::valve::ValveProtocol::query`
to `use gamedig::protocols::valve::query`.
Changed Valve Protocol Query parameters to (ip, port, app, gather_settings), changes include:
- the app is now optional, being None means to anonymously query the server.
- gather_settings is now also an optional, being None means all query settings.
Valve Protocol now supports querying anonymous apps (see previous lines).
Better bad game error.
[Alien Swarm](https://store.steampowered.com/app/630/Alien_Swarm/) implementation (not tested).
[Alien Swarm: Reactive Drop](https://store.steampowered.com/app/563560/Alien_Swarm_Reactive_Drop/) implementation.
[Insurgency](https://store.steampowered.com/app/222880/Insurgency/) implementation.
[Insurgency: Sandstorm](https://store.steampowered.com/app/581320/Insurgency_Sandstorm/) implementation.
[Insurgency: Modern Infantry Combat](https://store.steampowered.com/app/17700/INSURGENCY_Modern_Infantry_Combat/)
implementation (not tested).
# 0.0.3 - 22/10/2022
Valve protocol now properly supports multi-packet responses (compressed ones not tested).
CSGO, TF2 and TS now have independent Responses, if you want a generic one, query the protocol.
[Counter Strike: Source](https://store.steampowered.com/app/240/CounterStrike_Source/) implementation (if protocol is 7,
queries with multi-packet responses will crash).
[Day of Defeat: Source](https://store.steampowered.com/app/300/Day_of_Defeat_Source/) implementation.
[Garry's Mod](https://store.steampowered.com/app/4000/Garrys_Mod/) implementation.
[Half-Life 2 Deathmatch](https://store.steampowered.com/app/320/HalfLife_2_Deathmatch/) implementation.
[Left 4 Dead](https://store.steampowered.com/app/500/Left_4_Dead/) implementation.
[Left 4 Dead 2](https://store.steampowered.com/app/550/Left_4_Dead_2/) implementation.
# 0.0.2 - 20/10/2022
Further implementation of the Valve protocol (PLAYERS and RULES queries).
[Counter Strike: Global Offensive](https://store.steampowered.com/app/730/CounterStrike_Global_Offensive/)
implementation.
[The Ship](https://developer.valvesoftware.com/wiki/The_Ship) implementation.
The library now has error handling.
# 0.0.1 - 16/10/2022
The first usable version of the crate, yay!
It brings:
Initial implementation of the [Valve server query protocol](https://developer.valvesoftware.com/wiki/Server_queries).
Initial [Team Fortress 2](https://en.wikipedia.org/wiki/Team_Fortress_2) support.
# 0.0.0 - 15/10/2022
The first *markdown*, the crate is unusable as it doesn't contain anything helpful.

78
crates/lib/Cargo.toml Normal file
View file

@ -0,0 +1,78 @@
[package]
name = "gamedig"
version = "0.9.0"
edition = "2021"
authors = [
"rust-GameDig contributors [https://github.com/gamedig/rust-gamedig/contributors]",
"node-GameDig contributors [https://github.com/gamedig/node-gamedig/contributors]",
]
license = "MIT"
description = "Query game servers and not only."
homepage = "https://gamedig.github.io/"
documentation = "https://docs.rs/gamedig/latest/gamedig/"
repository = "https://github.com/gamedig/rust-gamedig"
readme = "README.md"
keywords = ["server", "query", "game", "check", "status"]
rust-version = "1.85.1"
categories = ["parser-implementations", "parsing", "network-programming", "encoding"]
[features]
default = ["games", "services", "game_defs"]
# Enable query functions for specific games
games = []
# Enable game definitions for use with the generic query functions
game_defs = ["dep:phf", "games"]
# Enable service querying
services = []
# Enable serde derivations for our types
serde = []
# Enable clap derivations for our types
clap = ["dep:clap"]
packet_capture = ["dep:pcap-file", "dep:pnet_packet", "dep:lazy_static"]
# Enable TLS for HTTP Client
tls = ["ureq/tls"]
[dependencies]
url = "2.5.8"
byteorder = "1.5.0"
bzip2-rs = "0.1.2"
crc32fast = "1.5.0"
base64 = "0.22.1"
encoding_rs = "0.8.35"
serde_json = { version = "1.0.149" }
serde = { version = "1.0.228", features = ["derive"] }
ureq = { version = "2.12.1", default-features = false, features = ["gzip", "json"] }
phf = { version = "0.13.1", optional = true, features = ["macros"] }
clap = { version = "4.5.60", optional = true, features = ["derive"] }
pcap-file = { version = "2.0.0", optional = true }
pnet_packet = { version = "0.35.0", optional = true }
lazy_static = { version = "1.5.0", optional = true }
[dev-dependencies]
gamedig-id-tests = { path = "../id-tests", default-features = false }
# Examples
[[example]]
name = "minecraft"
required-features = ["games"]
[[example]]
name = "teamfortress2"
required-features = ["games"]
[[example]]
name = "valve_master_server_query"
required-features = ["services"]
[[example]]
name = "test_eco"
required-features = ["games"]
[[example]]
name = "generic"
required-features = ["games", "game_defs"]

114
crates/lib/README.md Normal file
View file

@ -0,0 +1,114 @@
<h1 align="center">rust-GameDig</h1>
<h5 align="center">The fast library for querying game servers/services.</h5>
<div align="center">
<a href="https://github.com/gamedig/rust-gamedig/actions">
<img src="https://github.com/gamedig/rust-gamedig/actions/workflows/ci.yml/badge.svg" alt="CI">
</a>
<a href="https://crates.io/crates/gamedig">
<img src="https://img.shields.io/crates/v/gamedig.svg?color=yellow" alt="Latest Version">
</a>
<a href="https://crates.io/crates/gamedig">
<img src="https://img.shields.io/crates/d/gamedig?color=purple" alt="Crates.io">
</a>
<a href="https://github.com/gamedig/node-gamedig">
<img src="https://raw.githubusercontent.com/gamedig/rust-gamedig/main/.github/badges/node.svg" alt="Node-GameDig Game Coverage">
</a>
</div>
<h5 align="center">
This library brings what
<a href="https://github.com/gamedig/node-gamedig">
node-GameDig
</a>
does (and not only), to pure Rust!
</h5>
**Warning**: This project goes through frequent API breaking changes and hasn't been thoroughly tested.
## Community
Checkout the GameDig Community Discord Server [here](https://discord.gg/NVCMn3tnxH).
Note that it isn't be a replacement for GitHub issues, if you have found a problem
within the library or want to request a feature, it's better to do so here rather than
on Discord.
## Usage
Minimum Supported Rust Version is `1.85.1` and the code is cross-platform.
Pick a game/service/protocol (check
the [GAMES](https://github.com/gamedig/rust-gamedig/blob/main/GAMES.md), [SERVICES](https://github.com/gamedig/rust-gamedig/blob/main/SERVICES.md)
and [PROTOCOLS](https://github.com/gamedig/rust-gamedig/blob/main/PROTOCOLS.md) files to see the currently supported
ones), provide the ip and the port (be aware that some game servers use a separate port for the info queries, the port
can also be optional if the server is running the default ports) then query on it.
[Team Fortress 2](https://store.steampowered.com/app/440/Team_Fortress_2/) query example:
```rust
use gamedig::games::teamfortress2;
fn main() {
let response = teamfortress2::query(&"127.0.0.1".parse().unwrap(), None);
// None is the default port (which is 27015), could also be Some(27015)
match response { // Result type, must check what it is...
Err(error) => println!("Couldn't query, error: {}", error),
Ok(r) => println!("{:#?}", r)
}
}
```
Response (note that some games have a different structure):
```json5
{
protocol: 17,
name: "Team Fortress 2 Dedicated Server.",
map: "ctf_turbine",
game: "tf2",
appid: 440,
players_online: 0,
players_details: [],
players_maximum: 69,
players_bots: 0,
server_type: Dedicated,
has_password: false,
vac_secured: true,
version: "7638371",
port: Some(27015),
steam_id: Some(69753253289735296),
tv_port: None,
tv_name: None,
keywords: Some(
"alltalk,nocrits"
),
rules: [
"mp_autoteambalance"
:
"1",
"mp_maxrounds"
:
"5",
//....
]
}
```
Want to see more examples? Checkout
the [examples](https://github.com/gamedig/rust-gamedig/tree/main/crates/lib/examples) folder.
## Documentation
The documentation is available at [docs.rs](https://docs.rs/gamedig/latest/gamedig/).
Curious about the history and what changed between versions? Everything is in
the [CHANGELOG](https://github.com/gamedig/rust-gamedig/blob/main/crates/lib/CHANGELOG.md) file.
## Contributing
If you want to see your favorite game/service being supported here, open an issue, and I'll prioritize it (or do a pull
request if you want to implement it yourself)!
Before contributing please read [CONTRIBUTING](https://github.com/gamedig/rust-gamedig/blob/main/CONTRIBUTING.md).

View file

@ -0,0 +1,129 @@
use gamedig::{
protocols::types::CommonResponse,
query_with_timeout_and_extra_settings,
ExtraRequestSettings,
GDResult,
Game,
TimeoutSettings,
GAMES,
};
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
/// Make a query given the name of a game
/// The `game` argument is taken from the [GAMES](gamedig::GAMES) map.
fn generic_query(
game: &Game,
addr: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
extra_settings: Option<ExtraRequestSettings>,
) -> GDResult<Box<dyn CommonResponse>> {
println!("Querying {:#?} with game {:#?}.", addr, game);
let response = query_with_timeout_and_extra_settings(game, addr, port, timeout_settings, extra_settings)?;
println!("Response: {:#?}", response.as_json());
let common = response.as_original();
println!("Common response: {:#?}", common);
Ok(response)
}
fn main() {
let mut args = std::env::args().skip(1);
// Handle arguments
if let Some(game_name) = args.next() {
let hostname = args.next().expect("Must provide an address");
// Use to_socket_addrs to resolve hostname to IP
let addr: SocketAddr = format!("{}:0", hostname)
.to_socket_addrs()
.unwrap()
.next()
.expect("Could not lookup host");
let port: Option<u16> = args.next().map(|s| s.parse().unwrap());
let timeout_settings = TimeoutSettings::new(
TimeoutSettings::default().get_read(),
TimeoutSettings::default().get_write(),
TimeoutSettings::default().get_connect(),
2,
)
.unwrap();
let game = GAMES
.get(&game_name)
.expect("Game doesn't exist, run without arguments to see a list of games");
let extra_settings = game
.request_settings
.clone()
.set_hostname(hostname.to_string())
.set_check_app_id(false);
generic_query(
game,
&addr.ip(),
port,
Some(timeout_settings),
Some(extra_settings),
)
.unwrap();
} else {
// Without arguments print a list of games
for (name, game) in GAMES.entries() {
println!("{}\t{}", name, game.name);
}
}
}
#[cfg(test)]
mod test {
use gamedig::{protocols::types::TimeoutSettings, GAMES};
use std::{
net::{IpAddr, Ipv4Addr},
time::Duration,
};
use super::generic_query;
const ADDR: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);
fn test_game(game_name: &str) {
let timeout_settings = Some(
TimeoutSettings::new(
Some(Duration::from_nanos(1)),
Some(Duration::from_nanos(1)),
Some(Duration::from_nanos(1)),
0,
)
.unwrap(),
);
let game = GAMES
.get(game_name)
.expect("Game doesn't exist, run without arguments to see a list of games");
assert!(generic_query(game, &ADDR, None, timeout_settings, None).is_err());
}
#[test]
fn battlefield1942() { test_game("battlefield1942"); }
#[test]
fn minecraft() { test_game("minecraft"); }
#[test]
fn teamfortress2() { test_game("teamfortress2"); }
#[test]
fn quake2() { test_game("quake2"); }
#[test]
fn all_games() {
for game_name in GAMES.keys() {
test_game(game_name);
}
}
}

View file

@ -0,0 +1,31 @@
use gamedig::minecraft;
use gamedig::minecraft::types::RequestSettings;
fn main() {
// or Some(<port>), None is the default protocol port (which is 25565 for java
// and 19132 for bedrock)
let response = minecraft::query(&"127.0.0.1".parse().unwrap(), None);
// This will fail if no server is available locally!
match response {
Err(error) => println!("Couldn't query, error: {}", error),
Ok(r) => println!("{:#?}", r),
}
// This is an example to query a server with a hostname to be specified in the
// packet. Passing -1 on the protocol_version means anything, note that
// an invalid value here might result in server not responding.
let response = minecraft::query_java(
&"209.222.114.62".parse().unwrap(),
Some(25565),
Some(RequestSettings {
hostname: "mc.hypixel.net".to_string(),
protocol_version: -1,
}),
);
match response {
Err(error) => println!("Couldn't query, error: {}", error),
Ok(r) => println!("{:#?}", r),
}
}

View file

@ -0,0 +1,12 @@
use gamedig::games::teamfortress2;
fn main() {
let response = teamfortress2::query(&"127.0.0.1".parse().unwrap(), None);
// or Some(27015), None is the default protocol port (which is 27015)
match response {
// Result type, must check what it is...
Err(error) => println!("Couldn't query, error: {}", error),
Ok(r) => println!("{:#?}", r),
}
}

View file

@ -0,0 +1,10 @@
use gamedig::games::eco;
use std::net::IpAddr;
use std::str::FromStr;
fn main() {
let ip = IpAddr::from_str("142.132.154.69").unwrap();
let port = 31111;
let r = eco::query(&ip, Some(port));
println!("{:#?}", r);
}

View file

@ -0,0 +1,14 @@
use gamedig::valve_master_server::{query, Filter, Region, SearchFilters};
fn main() {
let search_filters = SearchFilters::new()
.insert(Filter::RunsAppID(440))
.insert(Filter::CanBeEmpty(false))
.insert(Filter::CanBeFull(false))
.insert(Filter::CanHavePassword(false))
.insert(Filter::IsSecured(true))
.insert(Filter::HasTags(vec!["minecraft".to_string()]));
let ips = query(Region::Europe, Some(search_filters)).unwrap();
println!("Servers: {:?} \n Amount: {}", ips, ips.len());
}

View file

@ -0,0 +1,36 @@
use gamedig::protocols::types::GatherToggle;
use gamedig::protocols::valve;
use gamedig::protocols::valve::{Engine, GatheringSettings};
use gamedig::TimeoutSettings;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::time::Duration;
fn main() {
let address = &SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 27015);
let engine = Engine::Source(None); // We don't specify a steam app id, let the query try to find it.
let gather_settings = GatheringSettings {
players: GatherToggle::Enforce, // We want to query for players
rules: GatherToggle::Skip, // We don't want to query for rules
check_app_id: false, // Loosen up the query a bit by not checking app id
};
let read_timeout = Duration::from_secs(2);
let write_timeout = Duration::from_secs(3);
let connect_timeout = Duration::from_secs(4);
let retries = 1; // does another request if the first one fails.
let timeout_settings = TimeoutSettings::new(
Some(read_timeout),
Some(write_timeout),
Some(connect_timeout),
retries,
)
.unwrap();
let response = valve::query(
address,
engine,
Some(gather_settings),
Some(timeout_settings),
);
println!("{response:#?}");
}

612
crates/lib/src/buffer.rs Normal file
View file

@ -0,0 +1,612 @@
use crate::GDErrorKind::PacketBad;
use crate::GDErrorKind::PacketUnderflow;
use crate::GDResult;
use byteorder::{BigEndian, ByteOrder, LittleEndian};
use std::{convert::TryInto, marker::PhantomData};
/// A struct representing a buffer with a specific byte order.
///
/// It's comprised of a byte slice that it reads from, a cursor to keep track of
/// the current position within the byte slice, and a `PhantomData` marker to
/// bind it to a specific byte order (BigEndian or LittleEndian).
///
/// The byte order is defined by the `B: ByteOrder` generic parameter.
pub struct Buffer<'a, B: ByteOrder> {
/// The byte slice that the buffer reads from.
data: &'a [u8],
/// The cursor marking our current position in the buffer.
cursor: usize,
/// A phantom field used to bind the `Buffer` to a specific `ByteOrder`.
_marker: PhantomData<B>,
}
impl<'a, B: ByteOrder> Buffer<'a, B> {
/// Creates and returns a new `Buffer` with the given data.
///
/// The cursor is set to the start of the buffer (position 0) upon
/// initialization.
///
/// # Arguments
///
/// * `data` - A byte slice that the buffer will read from.
pub const fn new(data: &'a [u8]) -> Self {
Self {
data,
cursor: 0,
_marker: PhantomData,
}
}
pub const fn current_position(&self) -> usize { self.cursor }
/// Returns the length of the remaining bytes from the current cursor
/// position.
pub const fn remaining_length(&self) -> usize { self.data.len() - self.cursor }
/// Returns the length of the buffer data.
pub const fn data_length(&self) -> usize { self.data.len() }
// TODO: Look into this to make it take ownership of data, not borrowing it
// There are many instances where we transform this to a vector.
/// Returns the remaining bytes that have not been read.
pub fn remaining_bytes(&self) -> &[u8] { &self.data[self.cursor ..] }
/// Moves the cursor forward or backward by a specified offset.
///
/// # Arguments
///
/// * `offset` - The amount to move the cursor. Use a negative value to move
/// backwards.
///
/// # Errors
///
/// Returns a `BufferError` if the attempted move would position the cursor
/// out of bounds.
pub fn move_cursor(&mut self, offset: isize) -> GDResult<()> {
// Compute the new cursor position by adding the offset to the current cursor
// position. The checked_add method is used for safe addition,
// preventing overflow and underflow.
let new_cursor = (self.cursor as isize).checked_add(offset);
match new_cursor {
// If the addition was not successful (i.e., it resulted in an overflow or underflow),
// return an error indicating that the cursor is out of bounds.
None => Err(PacketBad.into()),
// If the new cursor position is either less than zero (i.e., before the start of the buffer)
// or greater than the remaining length of the buffer (i.e., past the end of the buffer),
// return an error indicating that the cursor is out of bounds.
Some(x) if x < 0 || x as usize > self.data_length() => Err(PacketBad.into()),
// If the new cursor position is within the bounds of the buffer, update the cursor
// position and return Ok.
Some(x) => {
self.cursor = x as usize;
Ok(())
}
}
}
/// Reads a value of type `T` from the buffer, and advances the cursor by
/// the size of `T`.
///
/// # Type Parameters
///
/// * `T` - The type of value to be read from the buffer. This type must
/// implement the `BufferRead` trait with the same byte order as the
/// buffer.
///
/// # Errors
///
/// Returns a `BufferError` if there is not enough data remaining in the
/// buffer to read a value of type `T`.
pub fn read<T: Sized + BufferRead<B>>(&mut self) -> GDResult<T> {
// Get the size of `T` in bytes.
let size = std::mem::size_of::<T>();
// Calculate remaining length of the buffer.
let remaining = self.remaining_length();
// If the size of `T` is larger than the remaining length, return an error
// because we don't have enough data left to read.
if size > remaining {
return Err(PacketUnderflow.context(format!(
"Size requested {size} was larger than remaining bytes {remaining}"
)));
}
// Slice the data array from the current cursor position for `size` amount of
// bytes.
let bytes = &self.data[self.cursor .. self.cursor + size];
// Move the cursor forward by `size`.
self.cursor += size;
// Use the `read_from_buffer` function of the `BufferRead` implementation for
// `T` to convert the bytes into an instance of `T`.
T::read_from_buffer(bytes)
}
/// Reads a string from the buffer using a specified `StringDecoder`, until
/// an optional delimiter.
///
/// # Type Parameters
///
/// * `D` - The type of string decoder to use. This type must implement the
/// `StringDecoder` trait with the same byte order as the buffer.
///
/// # Arguments
///
/// * `until` - An optional delimiter. If provided, the method will read
/// until this delimiter is encountered. If not provided, the method will
/// read until the default delimiter of the decoder.
///
/// # Errors
///
/// Returns a `BufferError` if there is an error decoding the string.
pub fn read_string<D: StringDecoder>(&mut self, until: Option<D::Delimiter>) -> GDResult<String> {
// Check if the cursor is out of bounds.
if self.cursor > self.data_length() {
return Err(PacketUnderflow.context(format!(
"Cursor position {} is out of bounds when reading string. Buffer length: {}",
self.cursor,
self.data_length()
)));
}
// Slice the data array from the current cursor position to the end.
let data_slice = &self.data[self.cursor ..];
// Use the provided delimiter if one was given, or default to the
// delimiter specified by the StringDecoder.
let delimiter = until.unwrap_or(D::DELIMITER);
// Invoke the decode_string function of the provided StringDecoder,
// passing in the remaining data slice, the mutable reference to the
// cursor, and the delimiter.
let result = D::decode_string(data_slice, &mut self.cursor, delimiter)?;
// If decoding was successful, return the decoded string. The cursor
// position has been updated within the decode_string call to reflect
// the new position after reading.
Ok(result)
}
}
/// A trait that provides an interface to switch endianness.
///
/// The trait `SwitchEndian` is used for types that have a specific
/// byte order (endianness) and can switch to another byte order.
/// The type of the switched endianness is determined by the associated
/// type `Output`.
///
/// The associated type `Output` must implement the `ByteOrder` trait.
pub trait SwitchEndian {
type Output: ByteOrder;
}
/// An implementation of `SwitchEndian` for `LittleEndian`.
///
/// The switched endianness type is `BigEndian`.
impl SwitchEndian for LittleEndian {
type Output = BigEndian;
}
/// An implementation of `SwitchEndian` for `BigEndian`.
///
/// The switched endianness type is `LittleEndian`.
impl SwitchEndian for BigEndian {
type Output = LittleEndian;
}
impl<'a, B: SwitchEndian + ByteOrder> Buffer<'a, B> {
/// Switches the byte order of a chunk in the buffer.
///
/// This method consumes the buffer and returns a new buffer
/// with a chunk of the original buffer's data, starting from the
/// original cursor position and of the given size, where the byte
/// order is switched according to the implementation
/// of `SwitchEndian` for `B`.
///
/// Note: The method also advances the cursor of the original buffer
/// by `size`.
///
/// # Parameters
///
/// * `size`: The size of the chunk to be taken from the original buffer.
pub fn switch_endian_chunk(&mut self, size: usize) -> GDResult<Buffer<'a, B::Output>> {
let old_cursor = self.cursor;
self.move_cursor(size as isize)?;
Ok(Buffer {
data: &self.data[old_cursor .. old_cursor + size],
cursor: 0,
_marker: PhantomData,
})
}
}
/// A trait defining a protocol for reading values of a certain type from a
/// buffer.
///
/// Implementors of this trait provide a method for reading their type from a
/// byte buffer with a specific byte order.
pub trait BufferRead<B: ByteOrder>: Sized {
fn read_from_buffer(data: &[u8]) -> GDResult<Self>;
}
/// Macro to implement the `BufferRead` trait for byte types.
///
/// This macro generates an implementation of the `BufferRead` trait for a
/// specified byte type. The implementation will read a single byte from the
/// buffer and convert it to the target type using the provided map function.
///
/// # Arguments
///
/// * `$type` - The target type to implement `BufferRead` for.
/// * `$map_func` - The function to map a byte to the target type.
macro_rules! impl_buffer_read_byte {
($type:ty, $map_func:expr) => {
impl<B: ByteOrder> BufferRead<B> for $type {
fn read_from_buffer(data: &[u8]) -> GDResult<Self> {
// Use the `first` method to get the first byte from the data array.
data.first()
// Apply the $map_func function to convert the raw byte to the $type.
.map($map_func)
// If the data array is empty (and thus `first` returns None),
// `ok_or_else` will return a BufferError.
.ok_or_else(|| PacketBad.into())
}
}
};
}
/// Macro to implement the `BufferRead` trait for multi-byte types.
///
/// This macro generates an implementation of the `BufferRead` trait for a
/// specified multi-byte type. The implementation will read the appropriate
/// number of bytes from the buffer and convert them to the target type using
/// the provided read function.
///
/// # Arguments
///
/// * `$type` - The target type to implement `BufferRead` for.
/// * `$read_func` - The function to read the bytes into the target type.
macro_rules! impl_buffer_read {
($type:ty, $read_func:ident) => {
impl<B: ByteOrder> BufferRead<B> for $type {
fn read_from_buffer(data: &[u8]) -> GDResult<Self> {
// Convert the byte slice into an array of the appropriate type.
let array = data.try_into().map_err(|e| {
// If conversion fails, return an error indicating the required and provided
// lengths.
PacketBad.context(e)
})?;
// Use the provided function to read the data from the array into the given
// type.
Ok(B::$read_func(array))
}
}
};
}
impl_buffer_read_byte!(u8, |&b| b);
impl_buffer_read_byte!(i8, |&b| b as i8);
impl_buffer_read!(u16, read_u16);
impl_buffer_read!(i16, read_i16);
impl_buffer_read!(u32, read_u32);
impl_buffer_read!(i32, read_i32);
impl_buffer_read!(u64, read_u64);
impl_buffer_read!(i64, read_i64);
impl_buffer_read!(f32, read_f32);
impl_buffer_read!(f64, read_f64);
/// A trait defining a protocol for decoding strings from a buffer.
///
/// This trait should be implemented by types that can decode strings from a
/// byte buffer with a specific byte order and delimiter.
pub trait StringDecoder {
/// The type of the delimiter used by the decoder.
type Delimiter: AsRef<[u8]>;
/// The default delimiter used by the decoder.
const DELIMITER: Self::Delimiter;
/// Decodes a string from the provided byte slice, and updates the cursor
/// position accordingly.
///
/// # Arguments
///
/// * `data` - The byte slice to decode the string from.
/// * `cursor` - The current position in the byte slice.
/// * `delimiter` - The delimiter to use for decoding the string.
///
/// # Errors
///
/// Returns a `BufferError` if there is an error decoding the string.
fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult<String>;
}
/// A decoder for UTF-8 encoded strings.
///
/// This decoder uses a single null byte (`0x00`) as the default delimiter.
pub struct Utf8Decoder;
impl StringDecoder for Utf8Decoder {
type Delimiter = [u8; 1];
const DELIMITER: Self::Delimiter = [0x00];
/// Decodes a UTF-8 string from the given data, updating the cursor position
/// accordingly.
fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult<String> {
// Find the position of the delimiter in the data. If the delimiter is not
// found, the length of the data is returned.
let position = data
// Create an iterator over the data.
.iter()
// Find the position of the delimiter
.position(|&b| b == delimiter.as_ref()[0])
// If the delimiter is not found, use the whole data slice.
.unwrap_or(data.len());
// Convert the data until the found position into a UTF-8 string.
let result = std::str::from_utf8(
// Take a slice of data until the position.
&data[.. position]
)
// If the data cannot be converted into a UTF-8 string, return an error
.map_err(|e| PacketBad.context(e))?
// Convert the resulting &str into a String
.to_owned();
// Update the cursor position
// The +1 is to skip the delimiter
*cursor += position + 1;
Ok(result)
}
}
/// A decoder for UTF-8 encoded strings prefixed by a single byte denoting the
/// string's length.
///
/// This decoder uses a single null byte (`0x00`) as the default delimiter.
pub struct Utf8LengthPrefixedDecoder;
impl StringDecoder for Utf8LengthPrefixedDecoder {
type Delimiter = [u8; 1];
const DELIMITER: Self::Delimiter = [0x00];
/// Decodes a UTF-8 string from the given data, updating the cursor position
/// accordingly.
fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult<String> {
// Find the maximum length of the string
let length = *data
.first()
.ok_or_else(|| PacketBad.context("Length of string not found"))?;
// Find the position of the delimiter in the data. If the delimiter is not
// found, the length is returned.
let position = data
// Create an iterator over the data.
.iter()
.skip(1)
.take(length as usize)
// Find the position of the delimiter
.position(|&b| b == delimiter.as_ref()[0])
// If the delimiter is not found, use the whole data slice.
.unwrap_or(length as usize);
// Convert the data until the found position into a UTF-8 string.
let result = std::str::from_utf8(
// Take a slice of data until the position.
&data[1 .. position + 1]
)
// If the data cannot be converted into a UTF-8 string, return an error
.map_err(|e| PacketBad.context(e))?
// Convert the resulting &str into a String
.to_owned();
// Update the cursor position
// The +1 is to skip t length
*cursor += position + 1;
Ok(result)
}
}
/// A decoder for UTF-16 encoded strings.
///
/// This decoder uses a pair of null bytes (`0x00, 0x00`) as the default
/// delimiter.
///
/// # Type Parameters
///
/// * `B` - The byte order to use when decoding the string.
pub struct Utf16Decoder<B: ByteOrder> {
_marker: PhantomData<B>,
}
impl<B: ByteOrder> StringDecoder for Utf16Decoder<B> {
type Delimiter = [u8; 2];
const DELIMITER: Self::Delimiter = [0x00, 0x00];
/// Decodes a UTF-16 string from the given data, updating the cursor
/// position accordingly.
fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult<String> {
// Try to find the position of the delimiter in the data
let position = data
// Split the data into 2-byte chunks (as UTF-16 uses 2 bytes per character)
.chunks_exact(2)
// Find the position of the delimiter
.position(|chunk| chunk == delimiter.as_ref())
// If the delimiter is not found, use the whole data, otherwise use the position of the delimiter
.map_or(data.len(), |pos| pos * 2);
// Create a buffer of u16 values to hold the decoded characters
let mut paired_buf: Vec<u16> = vec![0; position / 2];
// Decode the data into the buffer
B::read_u16_into(&data[.. position], &mut paired_buf);
// Convert the buffer of u16 values into a String
let result = String::from_utf16(&paired_buf).map_err(|e| PacketBad.context(e))?;
// Update the cursor position
// The +2 accounts for the delimiter
*cursor += position + 2;
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use byteorder::BigEndian;
#[test]
fn test_new_buffer() {
let data: &[u8] = &[1, 2, 3, 4];
let buffer = Buffer::<LittleEndian>::new(data);
assert_eq!(buffer.data, data);
assert_eq!(buffer.cursor, 0);
}
#[test]
fn test_remaining_length() {
let data: &[u8] = &[1, 2, 3, 4];
let mut buffer = Buffer::<LittleEndian>::new(data);
assert_eq!(buffer.remaining_length(), 4);
buffer.cursor = 2;
assert_eq!(buffer.remaining_length(), 2);
}
#[test]
fn test_move_cursor() {
let data: &[u8] = &[1, 2, 3, 4];
let mut buffer = Buffer::<LittleEndian>::new(data);
// Test moving forward
assert!(buffer.move_cursor(2).is_ok());
assert_eq!(buffer.cursor, 2);
// Test moving backward
assert!(buffer.move_cursor(-1).is_ok());
assert_eq!(buffer.cursor, 1);
// Test moving beyond data limits
assert!(buffer.move_cursor(5).is_err());
assert!(buffer.move_cursor(-2).is_err());
}
#[test]
fn test_switch_endian_chunk_le_be() {
let data = [0x01, 0x02, 0x03, 0x04];
let mut buffer = Buffer::<LittleEndian>::new(&data[..]);
let switched_buffer = buffer.switch_endian_chunk(2).unwrap();
assert_eq!(switched_buffer.data, [0x01, 0x02]);
assert_eq!(switched_buffer.cursor, 0);
assert_eq!(buffer.remaining_bytes(), [0x03, 0x04]);
assert_eq!(buffer.cursor, 2);
}
#[test]
fn test_switch_endian_chunk_be_le() {
let data = [0x01, 0x02, 0x03, 0x04];
let mut buffer = Buffer::<BigEndian>::new(&data[..]);
let switched_buffer = buffer.switch_endian_chunk(2).unwrap();
assert_eq!(switched_buffer.data, [0x01, 0x02]);
assert_eq!(switched_buffer.cursor, 0);
assert_eq!(buffer.remaining_bytes(), [0x03, 0x04]);
assert_eq!(buffer.cursor, 2);
}
#[test]
fn test_buffer_read_u8() {
let data: &[u8] = &[1, 2, 3, 4];
let mut buffer = Buffer::<LittleEndian>::new(data);
let result: Result<u8, _> = buffer.read();
assert_eq!(result.unwrap(), 1);
assert_eq!(buffer.cursor, 1);
}
#[test]
fn test_buffer_read_u16() {
let data: &[u8] = &[1, 2, 3, 4];
let mut buffer = Buffer::<LittleEndian>::new(data);
let result: Result<u16, _> = buffer.read();
assert_eq!(result.unwrap(), 0x0201);
assert_eq!(buffer.cursor, 2);
}
#[test]
fn test_buffer_read_u16_big_endian() {
let data: &[u8] = &[1, 2, 3, 4];
let mut buffer = Buffer::<BigEndian>::new(data);
let result: Result<u16, _> = buffer.read();
assert_eq!(result.unwrap(), 0x0102);
assert_eq!(buffer.cursor, 2);
}
#[test]
fn test_decode_string_utf8() {
let data: &[u8] = b"Hello\0World\0";
let mut cursor = 0;
let delimiter = [0x00];
let result = Utf8Decoder::decode_string(data, &mut cursor, delimiter);
assert_eq!(result.unwrap(), "Hello");
assert_eq!(cursor, 6);
}
#[test]
fn test_decode_string_utf16_le() {
let data: &[u8] = &[0x48, 0x00, 0x65, 0x00, 0x00, 0x00];
let mut cursor = 0;
let delimiter = [0x00, 0x00];
let result = Utf16Decoder::<LittleEndian>::decode_string(data, &mut cursor, delimiter);
assert_eq!(result.unwrap(), "He");
assert_eq!(cursor, 6);
}
#[test]
fn test_decode_string_utf16_be() {
let data: &[u8] = &[0x00, 0x48, 0x00, 0x65, 0x00, 0x00];
let mut cursor = 0;
let delimiter = [0x00, 0x00];
let result = Utf16Decoder::<BigEndian>::decode_string(data, &mut cursor, delimiter);
assert_eq!(result.unwrap(), "He");
assert_eq!(cursor, 6);
}
#[test]
fn test_buffer_underflow_error() {
let data: &[u8] = &[1, 2];
let mut buffer = Buffer::<LittleEndian>::new(data);
let result: Result<u32, _> = buffer.read();
assert_eq!(
result.unwrap_err(),
crate::GDErrorKind::PacketUnderflow.into()
);
}
}

View file

@ -0,0 +1,39 @@
pub(crate) mod packet;
mod pcap;
pub(crate) mod socket;
pub(crate) mod writer;
use self::{pcap::Pcap, writer::Writer};
use pcap_file::pcapng::{blocks::interface_description::InterfaceDescriptionBlock, PcapNgBlock, PcapNgWriter};
use std::path::PathBuf;
pub fn setup_capture(file_path: Option<PathBuf>) {
if let Some(file_path) = file_path {
let file = std::fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(file_path.with_extension("pcap"))
.unwrap();
let mut pcap_writer = PcapNgWriter::new(file).unwrap();
// Write headers
let _ = pcap_writer.write_block(
&InterfaceDescriptionBlock {
linktype: pcap_file::DataLink::ETHERNET,
snaplen: 0xFFFF,
options: vec![],
}
.into_block(),
);
let writer = Box::new(Pcap::new(pcap_writer));
attach(writer)
}
}
/// Attaches a writer to the capture module.
///
/// # Errors
/// Returns an Error if the writer is already set.
fn attach(writer: Box<dyn Writer + Send + Sync>) { crate::capture::socket::set_writer(writer); }

View file

@ -0,0 +1,203 @@
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
/// Size of a standard network packet.
pub(crate) const PACKET_SIZE: usize = 5012;
/// Size of an Ethernet header.
pub(crate) const HEADER_SIZE_ETHERNET: usize = 14;
/// Size of an IPv4 header.
pub(crate) const HEADER_SIZE_IP4: usize = 20;
/// Size of an IPv6 header.
pub(crate) const HEADER_SIZE_IP6: usize = 40;
/// Size of a UDP header.
pub(crate) const HEADER_SIZE_UDP: usize = 4;
/// Represents the direction of a network packet.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum Direction {
/// Packet is outgoing (sent by us).
Send,
/// Packet is incoming (received by us).
Receive,
}
/// Defines the protocol of a network packet.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum Protocol {
/// Transmission Control Protocol.
Tcp,
/// User Datagram Protocol.
Udp,
}
/// Trait for handling different types of IP addresses (IPv4, IPv6).
pub(crate) trait IpAddress: Sized {
/// Creates an instance from a standard `IpAddr`, returning `None` if the
/// types are incompatible.
fn from_std(ip: IpAddr) -> Option<Self>;
}
/// Represents a captured network packet with metadata.
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct CapturePacket<'a> {
/// Direction of the packet (Send/Receive).
pub(crate) direction: Direction,
/// Protocol of the packet (Tcp/UDP).
pub(crate) protocol: Protocol,
/// Remote socket address.
pub(crate) remote_address: &'a SocketAddr,
/// Local socket address.
pub(crate) local_address: &'a SocketAddr,
}
impl CapturePacket<'_> {
/// Retrieves the local and remote ports based on the packet's direction.
///
/// Returns:
/// - (u16, u16): Tuple of (source port, destination port).
pub(super) fn ports_by_direction(&self) -> (u16, u16) {
let (local, remote) = (self.local_address.port(), self.remote_address.port());
self.direction.order(local, remote)
}
/// Retrieves the local and remote IP addresses.
///
/// Returns:
/// - (IpAddr, IpAddr): Tuple of (local IP, remote IP).
pub(super) fn ip_addr(&self) -> (IpAddr, IpAddr) {
let (local, remote) = (self.local_address.ip(), self.remote_address.ip());
(local, remote)
}
/// Retrieves IP addresses of a specific type (IPv4 or IPv6) based on the
/// packet's direction.
///
/// Panics if the IP type of the addresses does not match the requested
/// type.
///
/// Returns:
/// - (T, T): Tuple of (source IP, destination IP) of the specified type in
/// order.
pub(super) fn ipvt_by_direction<T: IpAddress>(&self) -> (T, T) {
let (local, remote) = (
T::from_std(self.local_address.ip()).expect("Incorrect IP type for local address"),
T::from_std(self.remote_address.ip()).expect("Incorrect IP type for remote address"),
);
self.direction.order(local, remote)
}
}
impl Direction {
/// Orders two elements (source and destination) based on the packet's
/// direction.
///
/// Returns:
/// - (T, T): Ordered tuple (source, destination).
pub(self) const fn order<T>(&self, source: T, remote: T) -> (T, T) {
match self {
Direction::Send => (source, remote),
Direction::Receive => (remote, source),
}
}
}
/// Implements the `IpAddress` trait for `Ipv4Addr`.
impl IpAddress for Ipv4Addr {
/// Creates an `Ipv4Addr` from a standard `IpAddr`, if it's IPv4.
fn from_std(ip: IpAddr) -> Option<Self> {
match ip {
IpAddr::V4(ipv4) => Some(ipv4),
_ => None,
}
}
}
/// Implements the `IpAddress` trait for `Ipv6Addr`.
impl IpAddress for Ipv6Addr {
/// Creates an `Ipv6Addr` from a standard `IpAddr`, if it's IPv6.
fn from_std(ip: IpAddr) -> Option<Self> {
match ip {
IpAddr::V6(ipv6) => Some(ipv6),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
// Helper function to create a SocketAddr from a string
fn socket_addr(addr: &str) -> SocketAddr { SocketAddr::from_str(addr).unwrap() }
#[test]
fn test_ports_by_direction() {
let packet_send = CapturePacket {
direction: Direction::Send,
protocol: Protocol::Tcp,
local_address: &socket_addr("127.0.0.1:8080"),
remote_address: &socket_addr("192.168.1.1:80"),
};
let packet_receive = CapturePacket {
direction: Direction::Receive,
protocol: Protocol::Tcp,
local_address: &socket_addr("127.0.0.1:8080"),
remote_address: &socket_addr("192.168.1.1:80"),
};
assert_eq!(packet_send.ports_by_direction(), (8080, 80));
assert_eq!(packet_receive.ports_by_direction(), (80, 8080));
}
#[test]
fn test_ip_addr() {
let packet_send = CapturePacket {
direction: Direction::Send,
protocol: Protocol::Tcp,
local_address: &socket_addr("127.0.0.1:8080"),
remote_address: &socket_addr("192.168.1.1:80"),
};
let packet_receive = CapturePacket {
direction: Direction::Receive,
protocol: Protocol::Tcp,
local_address: &socket_addr("127.0.0.1:8080"),
remote_address: &socket_addr("192.168.1.1:80"),
};
assert_eq!(
packet_send.ip_addr(),
(
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))
)
);
assert_eq!(
packet_receive.ip_addr(),
(
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))
)
);
}
#[test]
fn test_ip_by_direction_type_specific() {
let packet = CapturePacket {
direction: Direction::Send,
protocol: Protocol::Tcp,
local_address: &socket_addr("127.0.0.1:8080"),
remote_address: &socket_addr("192.168.1.1:80"),
};
let ipv4_result: Result<(Ipv4Addr, Ipv4Addr), _> =
std::panic::catch_unwind(|| packet.ipvt_by_direction::<Ipv4Addr>());
assert!(ipv4_result.is_ok());
let ipv6_result: Result<(Ipv6Addr, Ipv6Addr), _> =
std::panic::catch_unwind(|| packet.ipvt_by_direction::<Ipv6Addr>());
assert!(ipv6_result.is_err());
}
}

View file

@ -0,0 +1,383 @@
use pcap_file::pcapng::{blocks::enhanced_packet::EnhancedPacketOption, PcapNgBlock, PcapNgWriter};
use pnet_packet::{
ethernet::{EtherType, MutableEthernetPacket},
ip::{IpNextHeaderProtocol, IpNextHeaderProtocols},
ipv4::MutableIpv4Packet,
ipv6::MutableIpv6Packet,
tcp::{MutableTcpPacket, TcpFlags},
udp::MutableUdpPacket,
PacketSize,
};
use std::{io::Write, net::IpAddr, time::Instant};
use super::packet::{
CapturePacket,
Direction,
Protocol,
HEADER_SIZE_ETHERNET,
HEADER_SIZE_IP4,
HEADER_SIZE_IP6,
HEADER_SIZE_UDP,
PACKET_SIZE,
};
const BUFFER_SIZE: usize = PACKET_SIZE - HEADER_SIZE_IP6 - HEADER_SIZE_ETHERNET;
pub(crate) struct Pcap<W: Write> {
writer: PcapNgWriter<W>,
pub(crate) state: State,
}
pub(crate) struct State {
pub(crate) start_time: Instant,
pub(crate) send_seq: u32,
pub(crate) rec_seq: u32,
pub(crate) has_sent_handshake: bool,
pub(crate) stream_count: u32,
}
impl<W: Write> Pcap<W> {
pub(crate) fn new(writer: PcapNgWriter<W>) -> Self {
Self {
writer,
state: State::default(),
}
}
pub(crate) fn write_transport_packet(&mut self, info: &CapturePacket, payload: &[u8]) {
let mut buffer_array: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
let buf: &mut [u8] = &mut buffer_array[..];
let (source_port, dest_port) = info.ports_by_direction();
match info.protocol {
Protocol::Tcp => {
let buf_size = {
let mut tcp = MutableTcpPacket::new(buf).unwrap();
tcp.set_source(source_port);
tcp.set_destination(dest_port);
tcp.set_payload(payload);
tcp.set_data_offset(5);
tcp.set_window(43440);
match info.direction {
Direction::Send => {
tcp.set_sequence(self.state.send_seq);
tcp.set_acknowledgement(self.state.rec_seq);
self.state.send_seq = self.state.send_seq.wrapping_add(payload.len() as u32);
}
Direction::Receive => {
tcp.set_sequence(self.state.rec_seq);
tcp.set_acknowledgement(self.state.send_seq);
self.state.rec_seq = self.state.rec_seq.wrapping_add(payload.len() as u32);
}
}
tcp.set_flags(TcpFlags::PSH | TcpFlags::ACK);
tcp.packet_size()
};
self.write_transport_payload(
info,
IpNextHeaderProtocols::Tcp,
&buf[.. buf_size + payload.len()],
vec![],
);
let mut info = info.clone();
let buf_size = {
let mut tcp = MutableTcpPacket::new(buf).unwrap();
tcp.set_source(dest_port);
tcp.set_destination(source_port);
tcp.set_data_offset(5);
tcp.set_window(43440);
match &info.direction {
Direction::Send => {
tcp.set_sequence(self.state.rec_seq);
tcp.set_acknowledgement(self.state.send_seq);
info.direction = Direction::Receive;
}
Direction::Receive => {
tcp.set_sequence(self.state.send_seq);
tcp.set_acknowledgement(self.state.rec_seq);
info.direction = Direction::Send;
}
}
tcp.set_flags(TcpFlags::ACK);
tcp.packet_size()
};
self.write_transport_payload(
&info,
IpNextHeaderProtocols::Tcp,
&buf[.. buf_size],
vec![EnhancedPacketOption::Comment("Generated TCP ACK".into())],
);
}
Protocol::Udp => {
let buf_size = {
let mut udp = MutableUdpPacket::new(buf).unwrap();
udp.set_source(source_port);
udp.set_destination(dest_port);
udp.set_length((payload.len() + HEADER_SIZE_UDP) as u16);
udp.set_payload(payload);
udp.packet_size()
};
self.write_transport_payload(
info,
IpNextHeaderProtocols::Udp,
&buf[.. buf_size + payload.len()],
vec![],
);
}
}
}
/// Encode a network layer (IP) packet with a payload.
fn encode_ip_packet(
&self,
buf: &mut [u8],
info: &CapturePacket,
protocol: IpNextHeaderProtocol,
payload: &[u8],
) -> (usize, EtherType) {
match info.ip_addr() {
(IpAddr::V4(_), IpAddr::V4(_)) => {
let (source, destination) = info.ipvt_by_direction();
let header_size = HEADER_SIZE_IP4 + (32 / 8);
let mut ip = MutableIpv4Packet::new(buf).unwrap();
ip.set_version(4);
ip.set_total_length((payload.len() + header_size) as u16);
ip.set_next_level_protocol(protocol);
// https://en.wikipedia.org/wiki/Internet_Protocol_version_4#Total_Length
ip.set_header_length((header_size / 4) as u8);
ip.set_source(source);
ip.set_destination(destination);
ip.set_payload(payload);
ip.set_ttl(64);
ip.set_flags(pnet_packet::ipv4::Ipv4Flags::DontFragment);
let mut options_writer =
pnet_packet::ipv4::MutableIpv4OptionPacket::new(ip.get_options_raw_mut()).unwrap();
options_writer.set_copied(1);
options_writer.set_class(0);
options_writer.set_number(pnet_packet::ipv4::Ipv4OptionNumbers::SID);
options_writer.set_length(&[4]);
options_writer.set_data(&(self.state.stream_count as u16).to_be_bytes());
ip.set_checksum(pnet_packet::ipv4::checksum(&ip.to_immutable()));
(ip.packet_size(), pnet_packet::ethernet::EtherTypes::Ipv4)
}
(IpAddr::V6(_), IpAddr::V6(_)) => {
let (source, destination) = info.ipvt_by_direction();
let mut ip = MutableIpv6Packet::new(buf).unwrap();
ip.set_version(6);
ip.set_payload_length(payload.len() as u16);
ip.set_next_header(protocol);
ip.set_source(source);
ip.set_destination(destination);
ip.set_hop_limit(64);
ip.set_payload(payload);
ip.set_flow_label(self.state.stream_count);
(ip.packet_size(), pnet_packet::ethernet::EtherTypes::Ipv6)
}
_ => unreachable!(),
}
}
/// Encode a physical layer (ethernet) packet with a payload.
fn encode_ethernet_packet(
&self,
buf: &mut [u8],
ethertype: pnet_packet::ethernet::EtherType,
payload: &[u8],
) -> usize {
let mut ethernet = MutableEthernetPacket::new(buf).unwrap();
ethernet.set_ethertype(ethertype);
ethernet.set_payload(payload);
ethernet.packet_size()
}
/// Write a TCP handshake.
pub(crate) fn write_tcp_handshake(&mut self, info: &CapturePacket) {
let (source_port, dest_port) = (info.local_address.port(), info.remote_address.port());
let mut info = info.clone();
info.direction = Direction::Send;
let mut buffer_array: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
let buf: &mut [u8] = &mut buffer_array[..];
// Add a generated comment to all packets
let options = vec![
pcap_file::pcapng::blocks::enhanced_packet::EnhancedPacketOption::Comment("Generated TCP handshake".into()),
];
// SYN
let buf_size = {
let mut tcp = MutableTcpPacket::new(buf).unwrap();
self.state.send_seq = 500;
tcp.set_sequence(self.state.send_seq);
tcp.set_flags(TcpFlags::SYN);
tcp.set_source(source_port);
tcp.set_destination(dest_port);
tcp.set_window(43440);
tcp.set_data_offset(5);
tcp.packet_size()
};
self.write_transport_payload(
&info,
IpNextHeaderProtocols::Tcp,
&buf[.. buf_size],
options.clone(),
);
// SYN + ACK
info.direction = Direction::Receive;
let buf_size = {
let mut tcp = MutableTcpPacket::new(buf).unwrap();
self.state.send_seq = self.state.send_seq.wrapping_add(1);
tcp.set_acknowledgement(self.state.send_seq);
self.state.rec_seq = 1000;
tcp.set_sequence(self.state.rec_seq);
tcp.set_flags(TcpFlags::SYN | TcpFlags::ACK);
tcp.set_source(dest_port);
tcp.set_destination(source_port);
tcp.set_window(43440);
tcp.set_data_offset(5);
tcp.packet_size()
};
self.write_transport_payload(
&info,
IpNextHeaderProtocols::Tcp,
&buf[.. buf_size],
options.clone(),
);
// ACK
info.direction = Direction::Send;
let buf_size = {
let mut tcp = MutableTcpPacket::new(buf).unwrap();
tcp.set_sequence(self.state.send_seq);
self.state.rec_seq = self.state.rec_seq.wrapping_add(1);
tcp.set_acknowledgement(self.state.rec_seq);
tcp.set_flags(TcpFlags::ACK);
tcp.set_source(source_port);
tcp.set_destination(dest_port);
tcp.set_window(43440);
tcp.set_data_offset(5);
tcp.packet_size()
};
self.write_transport_payload(
&info,
IpNextHeaderProtocols::Tcp,
&buf[.. buf_size],
options,
);
self.state.has_sent_handshake = true;
}
pub(crate) fn send_tcp_fin(&mut self, info: &CapturePacket) {
let mut buffer_array: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
let buf: &mut [u8] = &mut buffer_array[..];
let (source_port, dest_port) = info.ports_by_direction();
let buf_size = {
let mut tcp = MutableTcpPacket::new(buf).unwrap();
tcp.set_source(source_port);
tcp.set_destination(dest_port);
tcp.set_data_offset(5);
tcp.set_window(43440);
match info.direction {
Direction::Send => {
tcp.set_sequence(self.state.send_seq);
tcp.set_acknowledgement(self.state.rec_seq);
}
Direction::Receive => {
tcp.set_sequence(self.state.rec_seq);
tcp.set_acknowledgement(self.state.send_seq);
}
}
tcp.set_flags(TcpFlags::FIN | TcpFlags::ACK);
tcp.packet_size()
};
self.write_transport_payload(
info,
IpNextHeaderProtocols::Tcp,
&buf[.. buf_size],
vec![EnhancedPacketOption::Comment("Generated TCP FIN".into())],
);
// Update sequence number
match info.direction {
Direction::Send => {
self.state.send_seq = self.state.send_seq.wrapping_add(1);
}
Direction::Receive => {
self.state.rec_seq = self.state.rec_seq.wrapping_add(1);
}
}
}
fn write_transport_payload(
&mut self,
info: &CapturePacket,
protocol: IpNextHeaderProtocol,
payload: &[u8],
options: Vec<pcap_file::pcapng::blocks::enhanced_packet::EnhancedPacketOption>,
) {
let mut network_packet = vec![0; PACKET_SIZE - HEADER_SIZE_ETHERNET];
let (network_size, ethertype) = self.encode_ip_packet(&mut network_packet, info, protocol, payload);
let network_size = network_size + payload.len();
network_packet.truncate(network_size);
let mut physical_packet = vec![0; PACKET_SIZE];
let physical_size =
self.encode_ethernet_packet(&mut physical_packet, ethertype, &network_packet) + network_size;
physical_packet.truncate(physical_size);
self.writer
.write_block(
&pcap_file::pcapng::blocks::enhanced_packet::EnhancedPacketBlock {
original_len: physical_size as u32,
data: physical_packet.into(),
interface_id: 0,
timestamp: self.state.start_time.elapsed(),
options,
}
.into_block(),
)
.unwrap();
}
}
impl Default for State {
fn default() -> Self {
Self {
start_time: Instant::now(),
send_seq: 0,
rec_seq: 0,
has_sent_handshake: false,
stream_count: 0,
}
}
}

View file

@ -0,0 +1,214 @@
use std::{marker::PhantomData, net::SocketAddr};
use crate::{
capture::{
packet::CapturePacket,
packet::{Direction, Protocol},
writer::{Writer, CAPTURE_WRITER},
},
protocols::types::TimeoutSettings,
socket::{Socket, TcpSocketImpl, UdpSocketImpl},
GDResult,
};
/// Sets a global capture writer for handling all packet data.
///
/// # Panics
/// Panics if a capture writer is already set.
///
/// # Arguments
/// * `writer` - A boxed writer that implements the `Writer` trait.
pub(crate) fn set_writer(writer: Box<dyn Writer + Send + Sync>) {
let mut lock = CAPTURE_WRITER.lock().unwrap();
if lock.is_some() {
panic!("Capture writer already set");
}
*lock = Some(writer);
}
/// A trait representing a provider of a network protocol.
pub(crate) trait ProtocolProvider {
/// Returns the protocol used by the provider.
fn protocol() -> Protocol;
}
/// Represents the TCP protocol provider.
pub(crate) struct ProtocolTCP;
impl ProtocolProvider for ProtocolTCP {
fn protocol() -> Protocol { Protocol::Tcp }
}
/// Represents the UDP protocol provider.
pub(crate) struct ProtocolUDP;
impl ProtocolProvider for ProtocolUDP {
fn protocol() -> Protocol { Protocol::Udp }
}
/// A socket wrapper that allows capturing packets.
///
/// # Type parameters
/// * `I` - The inner socket type.
/// * `P` - The protocol provider.
#[derive(Clone, Debug)]
pub(crate) struct WrappedCaptureSocket<I: Socket, P: ProtocolProvider> {
inner: I,
remote_address: SocketAddr,
_protocol: PhantomData<P>,
}
impl<I: Socket, P: ProtocolProvider> Socket for WrappedCaptureSocket<I, P> {
/// Creates a new wrapped socket for capturing packets.
///
/// Initializes a new socket of type `I`, wrapping it to enable packet
/// capturing. Capturing is protocol-specific, as indicated by
/// the `ProtocolProvider`.
///
/// # Arguments
/// * `address` - The address to connect the socket to.
/// * `timeout_settings` - Optional timeout settings for the socket.
///
/// # Returns
/// A `GDResult` containing either the wrapped socket or an error.
fn new(address: &SocketAddr, timeout_settings: &Option<TimeoutSettings>) -> GDResult<Self>
where Self: Sized {
let v = Self {
inner: I::new(address, timeout_settings)?,
remote_address: *address,
_protocol: PhantomData,
};
let info = CapturePacket {
direction: Direction::Send,
protocol: P::protocol(),
remote_address: address,
local_address: &v.local_addr().unwrap(),
};
if let Some(writer) = CAPTURE_WRITER.lock().unwrap().as_mut() {
writer.new_connect(&info)?;
}
Ok(v)
}
/// Sends data over the socket and captures the packet.
///
/// The method sends data using the inner socket and captures the sent
/// packet if a capture writer is set.
///
/// # Arguments
/// * `data` - Data to be sent.
///
/// # Returns
/// A result indicating success or error in sending data.
fn send(&mut self, data: &[u8]) -> GDResult<()> {
let info = CapturePacket {
direction: Direction::Send,
protocol: P::protocol(),
remote_address: &self.remote_address,
local_address: &self.local_addr().unwrap(),
};
if let Some(writer) = CAPTURE_WRITER.lock().unwrap().as_mut() {
writer.write(&info, data)?;
}
self.inner.send(data)
}
/// Receives data from the socket and captures the packet.
///
/// The method receives data using the inner socket and captures the
/// incoming packet if a capture writer is set.
///
/// # Arguments
/// * `size` - Optional size of data to receive.
///
/// # Returns
/// A result containing received data or an error.
fn receive(&mut self, size: Option<usize>) -> crate::GDResult<Vec<u8>> {
let data = self.inner.receive(size)?;
let info = CapturePacket {
direction: Direction::Receive,
protocol: P::protocol(),
remote_address: &self.remote_address,
local_address: &self.local_addr().unwrap(),
};
if let Some(writer) = CAPTURE_WRITER.lock().unwrap().as_mut() {
writer.write(&info, &data)?;
}
Ok(data)
}
/// Applies timeout settings to the wrapped socket.
///
/// Delegates the operation to the inner socket implementation.
///
/// # Arguments
/// * `timeout_settings` - Optional timeout settings to apply.
///
/// # Returns
/// A result indicating success or error in applying timeouts.
fn apply_timeout(
&self,
timeout_settings: &Option<crate::protocols::types::TimeoutSettings>,
) -> crate::GDResult<()> {
self.inner.apply_timeout(timeout_settings)
}
/// Returns the remote port of the wrapped socket.
///
/// Delegates the operation to the inner socket implementation.
///
/// # Returns
/// The remote port number.
fn port(&self) -> u16 { self.inner.port() }
/// Returns the local SocketAddr of the wrapped socket.
///
/// Delegates the operation to the inner socket implementation.
///
/// # Returns
/// The local SocketAddr.
fn local_addr(&self) -> std::io::Result<SocketAddr> { self.inner.local_addr() }
}
// this seems a bad way to do this, but its safe
impl<I: Socket, P: ProtocolProvider> Drop for WrappedCaptureSocket<I, P> {
fn drop(&mut self) {
// Construct the CapturePacket info
let info = CapturePacket {
direction: Direction::Send,
protocol: P::protocol(),
remote_address: &self.remote_address,
local_address: &self
.local_addr()
.unwrap_or_else(|_| SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), 0)),
};
// If a capture writer is set, close the connection and capture the packet.
if let Some(writer) = CAPTURE_WRITER.lock().unwrap().as_mut() {
let _ = writer.close_connection(&info);
}
}
}
/// A specialized `WrappedCaptureSocket` for UDP, using `UdpSocketImpl` as
/// the inner socket and `ProtocolUDP` as the protocol provider.
///
/// This type captures and processes UDP packets, wrapping around standard
/// UDP socket functionalities with additional packet capture
/// capabilities.
pub(crate) type CapturedUdpSocket = WrappedCaptureSocket<UdpSocketImpl, ProtocolUDP>;
/// A specialized `WrappedCaptureSocket` for TCP, using `TcpSocketImpl` as
/// the inner socket and `ProtocolTCP` as the protocol provider.
///
/// This type captures and processes TCP packets, wrapping around standard
/// TCP socket functionalities with additional packet capture
/// capabilities.
pub(crate) type CapturedTcpSocket = WrappedCaptureSocket<TcpSocketImpl, ProtocolTCP>;

View file

@ -0,0 +1,86 @@
use std::{io::Write, sync::Mutex};
use super::{
packet::{CapturePacket, Protocol},
pcap::Pcap,
};
use crate::GDResult;
use lazy_static::lazy_static;
lazy_static! {
/// A globally accessible, lazily-initialized static writer instance.
/// This writer is intended for capturing and recording network packets.
/// The writer is wrapped in a Mutex to ensure thread-safe access and modification.
pub(crate) static ref CAPTURE_WRITER: Mutex<Option<Box<dyn Writer + Send + Sync>>> = Mutex::new(None);
}
/// Trait defining the functionality for a writer that handles network packet
/// captures. This trait includes methods for writing packet data, handling new
/// connections, and closing connections.
pub(crate) trait Writer {
/// Writes a given packet's data to an underlying storage or stream.
///
/// # Arguments
/// * `packet` - Reference to the packet being captured.
/// * `data` - The raw byte data associated with the packet.
///
/// # Returns
/// A `GDResult` indicating the success or failure of the write operation.
fn write(&mut self, packet: &CapturePacket, data: &[u8]) -> GDResult<()>;
/// Handles the creation of a new connection, potentially logging or
/// initializing resources.
///
/// # Arguments
/// * `packet` - Reference to the packet indicating a new connection.
///
/// # Returns
/// A `GDResult` indicating the success or failure of handling the new
/// connection.
fn new_connect(&mut self, packet: &CapturePacket) -> GDResult<()>;
/// Closes a connection, handling any necessary cleanup or finalization.
///
/// # Arguments
/// * `packet` - Reference to the packet indicating the closure of a
/// connection.
///
/// # Returns
/// A `GDResult` indicating the success or failure of the connection closure
/// operation.
fn close_connection(&mut self, packet: &CapturePacket) -> GDResult<()>;
}
/// Implementation of the `Writer` trait for the `Pcap` struct.
/// This implementation enables writing, connection handling, and closure
/// specific to PCAP (Packet Capture) format.
impl<W: Write> Writer for Pcap<W> {
fn write(&mut self, info: &CapturePacket, data: &[u8]) -> GDResult<()> {
self.write_transport_packet(info, data);
Ok(())
}
fn new_connect(&mut self, packet: &CapturePacket) -> GDResult<()> {
match packet.protocol {
Protocol::Tcp => {
self.write_tcp_handshake(packet);
}
Protocol::Udp => {}
}
self.state.stream_count = self.state.stream_count.wrapping_add(1);
Ok(())
}
fn close_connection(&mut self, packet: &CapturePacket) -> GDResult<()> {
match packet.protocol {
Protocol::Tcp => {
self.send_tcp_fin(packet);
}
Protocol::Udp => {}
}
Ok(())
}
}

View file

@ -0,0 +1,143 @@
use crate::GDErrorKind;
use std::error::Error;
use std::fmt::Formatter;
use std::{backtrace, fmt};
pub(crate) type ErrorSource = Box<dyn Error + 'static + Send + Sync>;
/// The GameDig error type.
///
/// Can be created in three ways (all of which will implicitly generate a
/// backtrace):
///
/// Directly from an [error kind](GDErrorKind) (without a
/// source).
///
/// ```
/// use gamedig::{GDError, GDErrorKind};
/// let _: GDError = GDErrorKind::PacketBad.into();
/// ```
///
/// [From an error kind with a source](GDErrorKind::context) (any
/// type that implements `Into<Box<dyn std::error::Error + 'static>>`).
///
/// ```
/// use gamedig::{GDError, GDErrorKind};
/// let _: GDError = GDErrorKind::PacketBad.context("Reason the packet was bad");
/// ```
///
/// Using the [new helper](GDError::new).
///
/// ```
/// use gamedig::{GDError, GDErrorKind};
/// let _: GDError = GDError::new(GDErrorKind::PacketBad, Some("Reason the packet was bad".into()));
/// ```
pub struct GDError {
pub kind: GDErrorKind,
pub source: Option<ErrorSource>,
pub backtrace: Option<backtrace::Backtrace>,
}
impl From<GDErrorKind> for GDError {
fn from(value: GDErrorKind) -> Self {
let backtrace = Some(backtrace::Backtrace::capture());
Self {
kind: value,
source: None,
backtrace,
}
}
}
impl PartialEq for GDError {
fn eq(&self, other: &Self) -> bool { self.kind == other.kind }
}
impl Error for GDError {
fn source(&self) -> Option<&(dyn Error + 'static)> { self.source.as_ref().map(|err| Box::as_ref(err) as _) }
}
impl fmt::Debug for GDError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
writeln!(f, "GDError{{ kind={:?}", self.kind)?;
if let Some(source) = &self.source {
writeln!(f, " source={source:?}")?;
}
if let Some(backtrace) = &self.backtrace {
let bt = format!("{backtrace:#?}");
writeln!(f, " backtrace={}", bt.replace('\n', "\n "))?;
}
writeln!(f, "}}")?;
Ok(())
}
}
impl fmt::Display for GDError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{self:?}") }
}
impl GDError {
/// Create a new error (with automatic backtrace)
pub fn new(kind: GDErrorKind, source: Option<ErrorSource>) -> Self {
let backtrace = Some(backtrace::Backtrace::capture());
Self {
kind,
source,
backtrace,
}
}
/// Create a new error using any type that can be converted to an error
pub fn from_error<E: Into<ErrorSource>>(kind: GDErrorKind, source: E) -> Self {
Self::new(kind, Some(source.into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
// test error trait GDError
#[test]
fn test_error_trait() {
let source: Result<u32, _> = "nan".parse();
let source_err = source.unwrap_err();
let error_with_context = GDErrorKind::TypeParse.context(source_err.clone());
assert!(error_with_context.source().is_some());
assert_eq!(
format!("{}", error_with_context.source().unwrap()),
format!("{source_err}")
);
let error_without_context: GDError = GDErrorKind::TypeParse.into();
assert!(error_without_context.source().is_none());
}
// Test creating GDError with GDError::new
#[test]
fn test_create_new() {
let error_from_new = GDError::new(GDErrorKind::InvalidInput, None);
assert!(error_from_new.backtrace.is_some());
assert_eq!(error_from_new.kind, GDErrorKind::InvalidInput);
assert!(error_from_new.source.is_none());
}
// Test creating GDError with GDErrorKind::context
#[test]
fn test_create_context() {
let error_from_context = GDErrorKind::InvalidInput.context("test");
assert!(error_from_context.backtrace.is_some());
assert_eq!(error_from_context.kind, GDErrorKind::InvalidInput);
assert!(error_from_context.source.is_some());
}
// Test creating GDError with From<GDErrorKind> for GDError
#[test]
fn test_create_into() {
let error_from_into: GDError = GDErrorKind::InvalidInput.into();
assert!(error_from_into.backtrace.is_some());
assert_eq!(error_from_into.kind, GDErrorKind::InvalidInput);
assert!(error_from_into.source.is_none());
}
}

View file

@ -0,0 +1,75 @@
use crate::error::ErrorSource;
use crate::GDError;
/// All GameDig Error kinds.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GDErrorKind {
/// The received packet was bigger than the buffer size.
PacketOverflow,
/// The received packet was shorter than the expected one.
PacketUnderflow,
/// The received packet is badly formatted.
PacketBad,
/// Couldn't send the packet.
PacketSend,
/// Couldn't receieve data when it was expected.
PacketReceive,
/// Couldn't decompress data.
Decompress,
/// Couldn't create a socket connection.
SocketConnect,
/// Couldn't bind a socket.
SocketBind,
/// Invalid input to the library.
InvalidInput,
/// The server response indicated that it is a different game than the game
/// queried.
BadGame,
/// Couldn't automatically query (none of the attempted protocols were
/// successful).
AutoQuery,
/// A protocol-defined expected format was not met.
ProtocolFormat,
/// Couldn't cast a value to an enum.
UnknownEnumCast,
/// Couldn't parse a json string.
JsonParse,
/// Couldn't parse a value.
TypeParse,
/// Couldn't find the host specified.
HostLookup,
}
impl GDErrorKind {
/// Convert error kind into a full error with a source (and implicit
/// backtrace)
///
/// ```
/// use gamedig::{GDErrorKind, GDResult};
/// let _: GDResult<u32> = "thing".parse().map_err(|e| GDErrorKind::TypeParse.context(e));
/// ```
pub fn context<E: Into<ErrorSource>>(self, source: E) -> GDError { GDError::from_error(self, source) }
}
#[cfg(test)]
mod tests {
use super::*;
// Testing cloning the GDErrorKind type
#[test]
fn test_cloning() {
let error = GDErrorKind::BadGame;
let cloned_error = error.clone();
assert_eq!(error, cloned_error);
}
// test display GDError
#[test]
fn test_display() {
let err = GDErrorKind::BadGame.context("Rust is not a game");
assert_eq!(
format!("{err}"),
"GDError{ kind=BadGame\n source=\"Rust is not a game\"\n backtrace=<disabled>\n}\n"
);
}
}

View file

@ -0,0 +1,12 @@
//! Every GameDig errors.
/// The Error with backtrace.
pub mod error;
/// All defined Error kinds.
pub mod kind;
/// `GDResult`, a shorthand of `Result<T, GDError>`.
pub mod result;
pub use error::*;
pub use kind::*;
pub use result::*;

View file

@ -0,0 +1,24 @@
use crate::GDError;
/// `Result` of `T` and `GDError`.
pub type GDResult<T> = Result<T, GDError>;
#[cfg(test)]
mod tests {
use super::*;
use crate::GDErrorKind;
// Testing Ok variant of the GDResult type
#[test]
fn test_gdresult_ok() {
let result: GDResult<u32> = Ok(42);
assert_eq!(result, Ok(42));
}
// Testing Err variant of the GDResult type
#[test]
fn test_gdresult_err() {
let result: GDResult<u32> = Err(GDErrorKind::InvalidInput.into());
assert!(result.is_err());
}
}

View file

@ -0,0 +1,47 @@
use crate::protocols::valve::Engine;
use crate::{
protocols::valve::{self, game},
GDErrorKind::TypeParse,
GDResult,
};
use std::net::{IpAddr, SocketAddr};
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<game::Response> {
let mut valve_response = valve::query(
&SocketAddr::new(*address, port.unwrap_or(7780)),
Engine::new(489_940),
None,
None,
)?;
if let Some(rules) = &mut valve_response.rules {
if let Some(bat_max_players) = rules.get("bat_max_players_i") {
valve_response.info.players_maximum = bat_max_players.parse().map_err(|e| TypeParse.context(e))?;
rules.remove("bat_max_players_i");
}
if let Some(bat_player_count) = rules.get("bat_player_count_s") {
valve_response.info.players_online = bat_player_count.parse().map_err(|e| TypeParse.context(e))?;
rules.remove("bat_player_count_s");
}
if let Some(bat_has_password) = rules.get("bat_has_password_s") {
valve_response.info.has_password = bat_has_password == "Y";
rules.remove("bat_has_password_s");
}
if let Some(bat_name) = rules.get("bat_name_s") {
valve_response.info.name.clone_from(bat_name);
rules.remove("bat_name_s");
}
if let Some(bat_gamemode) = rules.get("bat_gamemode_s") {
valve_response.info.game_mode.clone_from(bat_gamemode);
rules.remove("bat_gamemode_s");
}
rules.remove("bat_map_s");
}
Ok(game::Response::new_from_valve_response(valve_response))
}

View file

@ -0,0 +1,160 @@
//! Static definitions of currently supported games
use crate::games::minecraft::types::{LegacyGroup, Server};
use crate::protocols::{gamespy::GameSpyVersion, quake::QuakeVersion, valve::Engine, Protocol};
use crate::Game;
use crate::protocols::types::{GatherToggle, ProprietaryProtocol};
use crate::protocols::valve::GatheringSettings;
use phf::{phf_map, Map};
macro_rules! game {
($name: literal, $default_port: expr, $protocol: expr) => {
game!(
$name,
$default_port,
$protocol,
GatheringSettings::default().into_extra()
)
};
($name: literal, $default_port: expr, $protocol: expr, $extra_request_settings: expr) => {
Game {
name: $name,
default_port: $default_port,
protocol: $protocol,
request_settings: $extra_request_settings,
}
};
}
/// Map of all currently supported games
pub static GAMES: Map<&'static str, Game> = phf_map! {
// Query with all minecraft protocols
"minecraft" => game!("Minecraft", 25565, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(None))),
// Query with specific minecraft protocols
"minecraftbedrock" => game!("Minecraft (bedrock)", 19132, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Bedrock)))),
"minecraftpocket" => game!("Minecraft (pocket)", 19132, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Bedrock)))),
"minecraftjava" => game!("Minecraft (java)", 25565, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Java)))),
"minecraftlegacy16" => game!("Minecraft (legacy 1.6)", 25565, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_6))))),
"minecraftlegacy14" => game!("Minecraft (legacy 1.4)", 25565, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_4))))),
"minecraftlegacyb18" => game!("Minecraft (legacy b1.8)", 25565, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Legacy(LegacyGroup::VB1_8))))),
"aapg" => game!("America's Army: Proving Grounds", 27020, Protocol::Valve(Engine::new(203_290)), GatheringSettings {
players: GatherToggle::Enforce,
rules: GatherToggle::Skip,
check_app_id: true,
}.into_extra()),
"abioticfactor" => game!("Abiotic Factor", 27015, Protocol::Valve(Engine::new(427_410))),
"alienswarm" => game!("Alien Swarm", 27015, Protocol::Valve(Engine::new(630))),
"aoc" => game!("Age of Chivalry", 27015, Protocol::Valve(Engine::new(17510))),
"a2oa" => game!("ARMA 2: Operation Arrowhead", 2304, Protocol::Valve(Engine::new(33930))),
"arma3" => game!("ARMA 3", 2303, Protocol::Valve(Engine::new(107_410))),
"ase" => game!("ARK: Survival Evolved", 27015, Protocol::Valve(Engine::new(346_110))),
"asrd" => game!("Alien Swarm: Reactive Drop", 2304, Protocol::Valve(Engine::new(563_560))),
"armareforger" => game!("Arma Reforger", 17777, Protocol::Valve(Engine::new(1_874_880)), GatheringSettings {
players: GatherToggle::Enforce,
rules: GatherToggle::Enforce,
check_app_id: false,
}.into_extra()),
"atlas" => game!("ATLAS", 57561, Protocol::Valve(Engine::new(834_910))),
"avorion" => game!("Avorion", 27020, Protocol::Valve(Engine::new(445_220))),
"avp2010" => game!("Aliens vs. Predator 2010", 27015, Protocol::Valve(Engine::new(10_680))),
"barotrauma" => game!("Barotrauma", 27016, Protocol::Valve(Engine::new(602_960))),
"basedefense" => game!("Base Defense", 27015, Protocol::Valve(Engine::new(632_730)), GatheringSettings {
players: GatherToggle::Enforce,
rules: GatherToggle::Skip,
check_app_id: true,
}.into_extra()),
"battalion1944" => game!("Battalion 1944", 7780, Protocol::Valve(Engine::new(489_940))),
"brainbread2" => game!("BrainBread 2", 27015, Protocol::Valve(Engine::new(346_330))),
"battlefield1942" => game!("Battlefield 1942", 23000, Protocol::Gamespy(GameSpyVersion::One)),
"blackmesa" => game!("Black Mesa", 27015, Protocol::Valve(Engine::new(362_890))),
"ballisticoverkill" => game!("Ballistic Overkill", 27016, Protocol::Valve(Engine::new(296_300))),
"codbo3" => game!("Call Of Duty: Black Ops 3", 27017, Protocol::Valve(Engine::new(311_210))),
"codenamecure" => game!("Codename CURE", 27015, Protocol::Valve(Engine::new(355_180))),
"colonysurvival" => game!("Colony Survival", 27004, Protocol::Valve(Engine::new(366_090))),
"conanexiles" => game!("Conan Exiles", 27015, Protocol::Valve(Engine::new(440_900)), GatheringSettings {
players: GatherToggle::Skip,
rules: GatherToggle::Enforce,
check_app_id: true,
}.into_extra()),
"counterstrike" => game!("Counter-Strike", 27015, Protocol::Valve(Engine::new_gold_src(false))),
"counterstrike2" => game!("Counter-Strike 2", 27015, Protocol::Valve(Engine::new(730))),
"cscz" => game!("Counter Strike: Condition Zero", 27015, Protocol::Valve(Engine::new_gold_src(false))),
"csgo" => game!("Counter-Strike: Global Offensive", 27015, Protocol::Valve(Engine::new(730))),
"css" => game!("Counter-Strike: Source", 27015, Protocol::Valve(Engine::new(240))),
"creativerse" => game!("Creativerse", 26901, Protocol::Valve(Engine::new(280_790))),
"crysiswars" => game!("Crysis Wars", 64100, Protocol::Gamespy(GameSpyVersion::Three)),
"dab" => game!("Double Action: Boogaloo", 27015, Protocol::Valve(Engine::new(317_360))),
"dod" => game!("Day of Defeat", 27015, Protocol::Valve(Engine::new_gold_src(false))),
"dods" => game!("Day of Defeat: Source", 27015, Protocol::Valve(Engine::new(300))),
"doi" => game!("Day of Infamy", 27015, Protocol::Valve(Engine::new(447_820))),
"dst" => game!("Don't Starve Together", 27016, Protocol::Valve(Engine::new(322_320))),
"enshrouded" => game!("Enshrouded", 15637, Protocol::Valve(Engine::new(1_203_620))),
"ffow" => game!("Frontlines: Fuel of War", 5478, Protocol::PROPRIETARY(ProprietaryProtocol::FFOW)),
"garrysmod" => game!("Garry's Mod", 27016, Protocol::Valve(Engine::new(4000))),
"hl2d" => game!("Half-Life 2 Deathmatch", 27015, Protocol::Valve(Engine::new(320))),
"hce" => game!("Halo: Combat Evolved", 2302, Protocol::Gamespy(GameSpyVersion::Two)),
"hlds" => game!("Half-Life Deathmatch: Source", 27015, Protocol::Valve(Engine::new(360))),
"hll" => game!("Hell Let Loose", 26420, Protocol::Valve(Engine::new(686_810))),
"insurgency" => game!("Insurgency", 27015, Protocol::Valve(Engine::new(222_880))),
"imic" => game!("Insurgency: Modern Infantry Combat", 27015, Protocol::Valve(Engine::new(17700))),
"insurgencysandstorm" => game!("Insurgency: Sandstorm", 27131, Protocol::Valve(Engine::new(581_320))),
"l4d" => game!("Left 4 Dead", 27015, Protocol::Valve(Engine::new(500))),
"l4d2" => game!("Left 4 Dead 2", 27015, Protocol::Valve(Engine::new(550))),
"ohd" => game!("Operation: Harsh Doorstop", 27005, Protocol::Valve(Engine::new_with_dedicated(736_590, 950_900))),
"onset" => game!("Onset", 7776, Protocol::Valve(Engine::new(1_105_810))),
"pixark" => game!("PixARK", 27015, Protocol::Valve(Engine::new(593_600))),
"postscriptum" => game!("Post Scriptum", 10037, Protocol::Valve(Engine::new(736_220))),
"projectzomboid" => game!("Project Zomboid", 16261, Protocol::Valve(Engine::new(108_600))),
"pvak2" => game!("Pirates, Vikings, and Knights II", 27015, Protocol::Valve(Engine::new(17_570))),
"quake1" => game!("Quake 1", 27500, Protocol::Quake(QuakeVersion::One)),
"quake2" => game!("Quake 2", 27910, Protocol::Quake(QuakeVersion::Two)),
"q3a" => game!("Quake 3 Arena", 27960, Protocol::Quake(QuakeVersion::Three)),
"risingworld" => game!("Rising World", 4254, Protocol::Valve(Engine::new(324_080)), GatheringSettings {
players: GatherToggle::Enforce,
rules: GatherToggle::Skip,
check_app_id: true,
}.into_extra()),
"ror2" => game!("Risk of Rain 2", 27016, Protocol::Valve(Engine::new(632_360))),
"rust" => game!("Rust", 27015, Protocol::Valve(Engine::new(252_490))),
"savage2" => game!("Savage 2", 11235, Protocol::PROPRIETARY(ProprietaryProtocol::Savage2)),
"sco" => game!("Sven Co-op", 27015, Protocol::Valve(Engine::new_gold_src(false))),
"sdtd" => game!("7 Days to Die", 26900, Protocol::Valve(Engine::new(251_570))),
"sof2" => game!("Soldier of Fortune 2", 20100, Protocol::Quake(QuakeVersion::Three)),
"soulmask" => game!("Soulmask", 27015, Protocol::Valve(Engine::new(2_646_460))),
"serioussam" => game!("Serious Sam", 25601, Protocol::Gamespy(GameSpyVersion::One)),
"squad" => game!("Squad", 27165, Protocol::Valve(Engine::new(393_380))),
"starbound" => game!("Starbound", 21025, Protocol::Valve(Engine::new(211_820)), GatheringSettings {
players: GatherToggle::Enforce,
rules: GatherToggle::Enforce,
check_app_id: false,
}.into_extra()),
"theforest" => game!("The Forest", 27016, Protocol::Valve(Engine::new_with_dedicated(242_760, 556_450))),
"thefront" => game!("The Front", 27015, Protocol::Valve(Engine::new(2_285_150))),
"teamfortress2" => game!("Team Fortress 2", 27015, Protocol::Valve(Engine::new(440))),
"tfc" => game!("Team Fortress Classic", 27015, Protocol::Valve(Engine::new_gold_src(false))),
"theship" => game!("The Ship", 27015, Protocol::PROPRIETARY(ProprietaryProtocol::TheShip)),
"unturned" => game!("Unturned", 27015, Protocol::Valve(Engine::new(304_930))),
"unrealtournament" => game!("Unreal Tournament", 7778, Protocol::Gamespy(GameSpyVersion::One)),
"valheim" => game!("Valheim", 2457, Protocol::Valve(Engine::new(892_970)), GatheringSettings {
players: GatherToggle::Enforce,
rules: GatherToggle::Skip,
check_app_id: true,
}.into_extra()),
"vrising" => game!("V Rising", 27016, Protocol::Valve(Engine::new(1_604_030))),
"jc2m" => game!("Just Cause 2: Multiplayer", 7777, Protocol::PROPRIETARY(ProprietaryProtocol::JC2M)),
"warsow" => game!("Warsow", 44400, Protocol::Quake(QuakeVersion::Three)),
"dhe4445" => game!("Darkest Hour: Europe '44-'45 (2008)", 7758, Protocol::Unreal2),
"devastation" => game!("Devastation (2003)", 7778, Protocol::Unreal2),
"killingfloor" => game!("Killing Floor", 7708, Protocol::Unreal2),
"redorchestra" => game!("Red Orchestra", 7759, Protocol::Unreal2),
"unrealtournament2003" => game!("Unreal Tournament 2003", 7758, Protocol::Unreal2),
"unrealtournament2004" => game!("Unreal Tournament 2004", 7778, Protocol::Unreal2),
"eco" => game!("Eco", 3000, Protocol::PROPRIETARY(ProprietaryProtocol::Eco)),
"zps" => game!("Zombie Panic: Source", 27015, Protocol::Valve(Engine::new(17_500))),
"moe" => game!("Myth Of Empires", 12888, Protocol::Valve(Engine::new(1_371_580))),
"mordhau" => game!("Mordhau", 27015, Protocol::Valve(Engine::new(629_760))),
"mindustry" => game!("Mindustry", crate::games::mindustry::DEFAULT_PORT, Protocol::PROPRIETARY(ProprietaryProtocol::Mindustry)),
"nla" => game!("Nova-Life: Amboise", 27015, Protocol::Valve(Engine::new(885_570))),
};

View file

@ -0,0 +1,8 @@
/// The implementation.
/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/ffow.js)
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;

View file

@ -0,0 +1,37 @@
use crate::eco::{EcoRequestSettings, Response, Root};
use crate::http::HttpClient;
use crate::{GDResult, TimeoutSettings};
use std::net::{IpAddr, SocketAddr};
/// Query an eco server.
#[inline]
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, &None) }
/// Query an eco server.
#[inline]
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: &Option<TimeoutSettings>,
) -> GDResult<Response> {
query_with_timeout_and_extra_settings(address, port, timeout_settings, None)
}
/// Query an eco server.
pub fn query_with_timeout_and_extra_settings(
address: &IpAddr,
port: Option<u16>,
timeout_settings: &Option<TimeoutSettings>,
extra_settings: Option<EcoRequestSettings>,
) -> GDResult<Response> {
let address = &SocketAddr::new(*address, port.unwrap_or(3001));
let mut client = HttpClient::new(
address,
timeout_settings,
extra_settings.unwrap_or_default().into(),
)?;
let response = client.get_json::<Root>("/frontpage", None)?;
Ok(response.into())
}

View file

@ -0,0 +1,241 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::http::{HttpProtocol, HttpSettings};
use crate::protocols::types::{CommonPlayer, CommonResponse};
use crate::ExtraRequestSettings;
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Root {
#[serde(rename = "Info")]
pub info: Info,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Info {
#[serde(rename = "External")]
pub external: bool,
#[serde(rename = "GamePort")]
pub game_port: u32,
#[serde(rename = "WebPort")]
pub web_port: u32,
#[serde(rename = "IsLAN")]
pub is_lan: bool,
#[serde(rename = "Description")]
pub description: String,
#[serde(rename = "DetailedDescription")]
pub detailed_description: String,
#[serde(rename = "Category")]
pub category: String,
#[serde(rename = "OnlinePlayers")]
pub online_players: u32,
#[serde(rename = "TotalPlayers")]
pub total_players: u32,
#[serde(rename = "OnlinePlayersNames")]
pub online_players_names: Vec<String>,
#[serde(rename = "AdminOnline")]
pub admin_online: bool,
#[serde(rename = "TimeSinceStart")]
pub time_since_start: f64,
#[serde(rename = "TimeLeft")]
pub time_left: f64,
#[serde(rename = "Animals")]
pub animals: u32,
#[serde(rename = "Plants")]
pub plants: u32,
#[serde(rename = "Laws")]
pub laws: u32,
#[serde(rename = "WorldSize")]
pub world_size: String,
#[serde(rename = "Version")]
pub version: String,
#[serde(rename = "EconomyDesc")]
pub economy_desc: String,
#[serde(rename = "SkillSpecializationSetting")]
pub skill_specialization_setting: String,
#[serde(rename = "Language")]
pub language: String,
#[serde(rename = "HasPassword")]
pub has_password: bool,
#[serde(rename = "HasMeteor")]
pub has_meteor: bool,
#[serde(rename = "DistributionStationItems")]
pub distribution_station_items: String,
#[serde(rename = "Playtimes")]
pub playtimes: String,
#[serde(rename = "DiscordAddress")]
pub discord_address: String,
#[serde(rename = "IsPaused")]
pub is_paused: bool,
#[serde(rename = "ActiveAndOnlinePlayers")]
pub active_and_online_players: u32,
#[serde(rename = "PeakActivePlayers")]
pub peak_active_players: u32,
#[serde(rename = "MaxActivePlayers")]
pub max_active_players: u32,
#[serde(rename = "ShelfLifeMultiplier")]
pub shelf_life_multiplier: f64,
#[serde(rename = "ExhaustionAfterHours")]
pub exhaustion_after_hours: f64,
#[serde(rename = "IsLimitingHours")]
pub is_limiting_hours: bool,
#[serde(rename = "ServerAchievementsDict")]
pub server_achievements_dict: HashMap<String, String>,
#[serde(rename = "RelayAddress")]
pub relay_address: String,
#[serde(rename = "Access")]
pub access: String,
#[serde(rename = "JoinUrl")]
pub join_url: String,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
pub name: String,
}
impl CommonPlayer for Player {
fn as_original(&self) -> crate::protocols::types::GenericPlayer<'_> {
crate::protocols::types::GenericPlayer::Eco(self)
}
fn name(&self) -> &str { &self.name }
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct Response {
pub external: bool,
pub port: u32,
pub query_port: u32,
pub is_lan: bool,
pub description: String, // this and other fields require some text filtering
pub description_detailed: String,
pub description_economy: String,
pub category: String,
pub players_online: u32,
pub players_maximum: u32,
pub players: Vec<Player>,
pub admin_online: bool,
pub time_since_start: f64,
pub time_left: f64,
pub animals: u32,
pub plants: u32,
pub laws: u32,
pub world_size: String,
pub game_version: String,
pub skill_specialization_setting: String,
pub language: String,
pub has_password: bool,
pub has_meteor: bool,
pub distribution_station_items: String,
pub playtimes: String,
pub discord_address: String,
pub is_paused: bool,
pub active_and_online_players: u32,
pub peak_active_players: u32,
pub max_active_players: u32,
pub shelf_life_multiplier: f64,
pub exhaustion_after_hours: f64,
pub is_limiting_hours: bool,
pub server_achievements_dict: HashMap<String, String>,
pub relay_address: String,
pub access: String,
pub connect: String,
}
impl From<Root> for Response {
fn from(root: Root) -> Self {
let value = root.info;
Self {
external: value.external,
port: value.game_port,
query_port: value.web_port,
is_lan: value.is_lan,
description: value.description,
description_detailed: value.detailed_description,
description_economy: value.economy_desc,
category: value.category,
players_online: value.online_players,
players_maximum: value.total_players,
players: value
.online_players_names
.iter()
.map(|player| {
Player {
name: player.clone(),
}
})
.collect(),
admin_online: value.admin_online,
time_since_start: value.time_since_start,
time_left: value.time_left,
animals: value.animals,
plants: value.plants,
laws: value.laws,
world_size: value.world_size,
game_version: value.version,
skill_specialization_setting: value.skill_specialization_setting,
language: value.language,
has_password: value.has_password,
has_meteor: value.has_meteor,
distribution_station_items: value.distribution_station_items,
playtimes: value.playtimes,
discord_address: value.discord_address,
is_paused: value.is_paused,
active_and_online_players: value.active_and_online_players,
peak_active_players: value.peak_active_players,
max_active_players: value.max_active_players,
shelf_life_multiplier: value.shelf_life_multiplier,
exhaustion_after_hours: value.exhaustion_after_hours,
is_limiting_hours: value.is_limiting_hours,
server_achievements_dict: value.server_achievements_dict,
relay_address: value.relay_address,
access: value.access,
connect: value.join_url,
}
}
}
impl CommonResponse for Response {
fn as_original(&self) -> crate::protocols::GenericResponse<'_> { crate::protocols::GenericResponse::Eco(self) }
fn players_online(&self) -> u32 { self.players_online }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn description(&self) -> Option<&str> { Some(&self.description) }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> { Some(self.players.iter().map(|p| p as _).collect()) }
}
/// Extra request settings for eco queries.
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct EcoRequestSettings {
hostname: Option<String>,
}
impl From<ExtraRequestSettings> for EcoRequestSettings {
fn from(value: ExtraRequestSettings) -> Self {
Self {
hostname: value.hostname,
}
}
}
impl From<EcoRequestSettings> for HttpSettings<String> {
fn from(value: EcoRequestSettings) -> Self {
Self {
protocol: HttpProtocol::Http,
hostname: value.hostname,
headers: Vec::with_capacity(0),
}
}
}

View file

@ -0,0 +1,15 @@
//! Unreal2 game query modules
use crate::protocols::epic::game_query_mod;
game_query_mod!(
asa,
"Ark: Survival Ascended",
7777,
Credentials {
deployment: "ad9a8feffb3b4b2ca315546f038c3ae2",
id: "xyza7891muomRmynIIHaJB9COBKkwj6n",
secret: "PP5UGxysEieNfSrEicaD1N2Bb3TdXuD7xHYcsdUHZ7s",
auth_by_external: false,
}
);

View file

@ -0,0 +1,8 @@
/// The implementation.
/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/ffow.js)
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;

View file

@ -0,0 +1,66 @@
use crate::buffer::{Buffer, Utf8Decoder};
use crate::games::ffow::types::Response;
use crate::protocols::types::TimeoutSettings;
use crate::protocols::valve::{Engine, Environment, Server, ValveProtocol};
use crate::GDResult;
use byteorder::LittleEndian;
use std::net::{IpAddr, SocketAddr};
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let mut client = ValveProtocol::new(
&SocketAddr::new(*address, port.unwrap_or(5478)),
timeout_settings,
)?;
let data = client.get_request_data(
&Engine::GoldSrc(true),
0,
0x46,
String::from("LSQ").into_bytes(),
)?;
let mut buffer = Buffer::<LittleEndian>::new(&data);
let protocol_version = buffer.read::<u8>()?;
let name = buffer.read_string::<Utf8Decoder>(None)?;
let map = buffer.read_string::<Utf8Decoder>(None)?;
let active_mod = buffer.read_string::<Utf8Decoder>(None)?;
let game_mode = buffer.read_string::<Utf8Decoder>(None)?;
let description = buffer.read_string::<Utf8Decoder>(None)?;
let game_version = buffer.read_string::<Utf8Decoder>(None)?;
buffer.move_cursor(2)?;
let players_online = buffer.read::<u8>()?;
let players_maximum = buffer.read::<u8>()?;
let server_type = Server::from_gldsrc(buffer.read::<u8>()?)?;
let environment_type = Environment::from_gldsrc(buffer.read::<u8>()?)?;
let has_password = buffer.read::<u8>()? == 1;
let vac_secured = buffer.read::<u8>()? == 1;
buffer.move_cursor(1)?; // average fps
let round = buffer.read::<u8>()?;
let rounds_maximum = buffer.read::<u8>()?;
let time_left = buffer.read::<u16>()?;
Ok(Response {
protocol_version,
name,
active_mod,
game_mode,
game_version,
description,
map,
players_online,
players_maximum,
server_type,
environment_type,
has_password,
vac_secured,
round,
rounds_maximum,
time_left,
})
}

View file

@ -0,0 +1,56 @@
use crate::protocols::types::CommonResponse;
use crate::protocols::valve::{Environment, Server};
use crate::protocols::GenericResponse;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// The query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Response {
/// Protocol used by the server.
pub protocol_version: u8,
/// Name of the server.
pub name: String,
/// Map name.
pub active_mod: String,
/// Running game mode.
pub game_mode: String,
/// The version that the server is running on.
pub game_version: String,
/// Description of the server.
pub description: String,
/// Current map.
pub map: String,
/// Number of players on the server.
pub players_online: u8,
/// Maximum number of players the server reports it can hold.
pub players_maximum: u8,
/// Dedicated, NonDedicated or SourceTV
pub server_type: Server,
/// The Operating System that the server is on.
pub environment_type: Environment,
/// Indicates whether the server requires a password.
pub has_password: bool,
/// Indicates whether the server uses VAC.
pub vac_secured: bool,
/// Current round index.
pub round: u8,
/// Maximum amount of rounds.
pub rounds_maximum: u8,
/// Time left for the current round in seconds.
pub time_left: u16,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse<'_> { GenericResponse::FFOW(self) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
fn description(&self) -> Option<&str> { Some(&self.description) }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn players_maximum(&self) -> u32 { self.players_maximum.into() }
fn players_online(&self) -> u32 { self.players_online.into() }
}

View file

@ -0,0 +1,9 @@
//! Gamespy game query modules
use crate::protocols::gamespy::game_query_mod;
game_query_mod!(battlefield1942, "Battlefield 1942", one, 23000);
game_query_mod!(crysiswars, "Crysis Wars", three, 64100);
game_query_mod!(hce, "Halo: Combat Evolved", two, 2302);
game_query_mod!(serioussam, "Serious Sam", one, 25601);
game_query_mod!(unrealtournament, "Unreal Tournament", one, 7778);

View file

@ -0,0 +1,8 @@
/// The implementation.
/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/jc2mp.js)
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;

View file

@ -0,0 +1,75 @@
use crate::buffer::{Buffer, Utf8Decoder};
use crate::jc2m::{Player, Response};
use crate::protocols::gamespy::common::has_password;
use crate::protocols::gamespy::three::{data_to_map, GameSpy3};
use crate::protocols::types::TimeoutSettings;
use crate::GDErrorKind::{PacketBad, TypeParse};
use crate::GDResult;
use byteorder::BigEndian;
use std::net::{IpAddr, SocketAddr};
fn parse_players_and_teams(packet: &[u8]) -> GDResult<Vec<Player>> {
let mut buf = Buffer::<BigEndian>::new(packet);
let count = buf.read::<u16>()?;
let mut players = Vec::with_capacity(count as usize);
while buf.remaining_length() != 0 {
players.push(Player {
name: buf.read_string::<Utf8Decoder>(None)?,
steam_id: buf.read_string::<Utf8Decoder>(None)?,
ping: buf.read::<u16>()?,
});
}
Ok(players)
}
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let mut client = GameSpy3::new_custom(
&SocketAddr::new(*address, port.unwrap_or(7777)),
timeout_settings,
[0xFF, 0xFF, 0xFF, 0x02],
true,
)?;
let packets = client.get_server_packets()?;
let data = packets
.first()
.ok_or_else(|| PacketBad.context("First packet missing"))?;
let (mut server_vars, remaining_data) = data_to_map(data)?;
let players = parse_players_and_teams(&remaining_data)?;
let players_maximum = server_vars
.remove("maxplayers")
.ok_or_else(|| PacketBad.context("Server variables missing maxplayers"))?
.parse()
.map_err(|e| TypeParse.context(e))?;
let players_online = match server_vars.remove("numplayers") {
None => players.len(),
Some(v) => {
let reported_players = v.parse().map_err(|e| TypeParse.context(e))?;
match reported_players < players.len() {
true => players.len(),
false => reported_players,
}
}
} as u32;
Ok(Response {
game_version: server_vars.remove("version").ok_or(PacketBad)?,
description: server_vars.remove("description").ok_or(PacketBad)?,
name: server_vars.remove("hostname").ok_or(PacketBad)?,
has_password: has_password(&mut server_vars)?,
players,
players_maximum,
players_online,
})
}

View file

@ -0,0 +1,50 @@
use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer};
use crate::protocols::GenericResponse;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
pub name: String,
pub steam_id: String,
pub ping: u16,
}
impl CommonPlayer for Player {
fn as_original(&self) -> GenericPlayer<'_> { GenericPlayer::JCMP2(self) }
fn name(&self) -> &str { &self.name }
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Response {
pub game_version: String,
pub description: String,
pub name: String,
pub has_password: bool,
pub players: Vec<Player>,
pub players_maximum: u32,
pub players_online: u32,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse<'_> { GenericResponse::JC2M(self) }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn description(&self) -> Option<&str> { Some(&self.description) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
Some(
self.players
.iter()
.map(|p| p as &dyn CommonPlayer)
.collect(),
)
}
}

View file

@ -0,0 +1,25 @@
//! Mindustry game ping (v146)
//!
//! [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/ArcNetProvider.java#L225-L259)
use std::{net::IpAddr, net::SocketAddr};
use crate::{GDResult, TimeoutSettings};
use self::types::ServerData;
pub mod types;
pub mod protocol;
/// Default mindustry server port
///
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/Vars.java#L141-L142)
pub const DEFAULT_PORT: u16 = 6567;
/// Query a mindustry server.
pub fn query(ip: &IpAddr, port: Option<u16>, timeout_settings: &Option<TimeoutSettings>) -> GDResult<ServerData> {
let address = SocketAddr::new(*ip, port.unwrap_or(DEFAULT_PORT));
protocol::query_with_retries(&address, timeout_settings)
}

View file

@ -0,0 +1,58 @@
use std::net::SocketAddr;
use crate::{
buffer::{self, Buffer},
socket::{Socket, UdpSocket},
utils,
GDResult,
TimeoutSettings,
};
use super::types::ServerData;
/// Mindustry max datagram packet size.
pub const MAX_BUFFER_SIZE: usize = 500;
/// Send a ping packet.
///
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/ArcNetProvider.java#L248)
pub(crate) fn send_ping(socket: &mut UdpSocket) -> GDResult<()> { socket.send(&[-2i8 as u8, 1i8 as u8]) }
/// Parse server data.
///
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/NetworkIO.java#L122-L135)
pub fn parse_server_data<B: byteorder::ByteOrder, D: buffer::StringDecoder>(
buffer: &mut Buffer<B>,
) -> GDResult<ServerData> {
Ok(ServerData {
host: buffer.read_string::<D>(None)?,
map: buffer.read_string::<D>(None)?,
players: buffer.read()?,
wave: buffer.read()?,
version: buffer.read()?,
version_type: buffer.read_string::<D>(None)?,
gamemode: buffer.read::<u8>()?.try_into()?,
player_limit: buffer.read()?,
description: buffer.read_string::<D>(None)?,
mode_name: buffer.read_string::<D>(None).ok(),
})
}
/// Query a Mindustry server (without retries).
pub fn query(address: &SocketAddr, timeout_settings: &Option<TimeoutSettings>) -> GDResult<ServerData> {
let mut socket = UdpSocket::new(address, timeout_settings)?;
send_ping(&mut socket)?;
let socket_data = socket.receive(Some(MAX_BUFFER_SIZE))?;
let mut buffer = Buffer::new(&socket_data);
parse_server_data::<byteorder::BigEndian, buffer::Utf8LengthPrefixedDecoder>(&mut buffer)
}
/// Query a Mindustry server.
pub fn query_with_retries(address: &SocketAddr, timeout_settings: &Option<TimeoutSettings>) -> GDResult<ServerData> {
let retries = TimeoutSettings::get_retries_or_default(timeout_settings);
utils::retry_on_timeout(retries, || query(address, timeout_settings))
}

View file

@ -0,0 +1,107 @@
use crate::{
protocols::types::{CommonResponse, GenericResponse},
GDErrorKind,
};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// Mindustry sever data
///
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/NetworkIO.java#L122-L135)
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ServerData {
pub host: String,
pub map: String,
pub players: i32,
pub wave: i32,
pub version: i32,
pub version_type: String,
pub gamemode: GameMode,
pub player_limit: i32,
pub description: String,
pub mode_name: Option<String>,
}
/// Mindustry game mode
///
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/game/Gamemode.java)
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum GameMode {
Survival,
Sandbox,
Attack,
PVP,
Editor,
}
impl TryFrom<u8> for GameMode {
type Error = GDErrorKind;
fn try_from(value: u8) -> Result<Self, Self::Error> {
use GameMode::*;
Ok(match value {
0 => Survival,
1 => Sandbox,
2 => Attack,
3 => PVP,
4 => Editor,
_ => return Err(GDErrorKind::TypeParse),
})
}
}
impl GameMode {
const fn as_str(&self) -> &'static str {
match self {
Self::Survival => "survival",
Self::Sandbox => "sandbox",
Self::Attack => "attack",
Self::PVP => "pvp",
Self::Editor => "editor",
}
}
}
impl CommonResponse for ServerData {
fn as_original(&self) -> GenericResponse<'_> { GenericResponse::Mindustry(self) }
fn players_online(&self) -> u32 { self.players.try_into().unwrap_or(0) }
fn players_maximum(&self) -> u32 { self.player_limit.try_into().unwrap_or(0) }
fn game_mode(&self) -> Option<&str> { Some(self.gamemode.as_str()) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn description(&self) -> Option<&str> { Some(&self.description) }
}
#[cfg(test)]
mod test {
use crate::protocols::types::CommonResponse;
use super::ServerData;
#[test]
fn common_impl() {
let data = ServerData {
host: String::from("host"),
map: String::from("map"),
players: 5,
wave: 2,
version: 142,
version_type: String::from("steam"),
gamemode: super::GameMode::PVP,
player_limit: 20,
description: String::from("description"),
mode_name: Some(String::from("campaign")),
};
let common: &dyn CommonResponse = &data;
assert_eq!(common.players_online(), 5);
assert_eq!(common.players_maximum(), 20);
assert_eq!(common.game_mode(), Some("pvp"));
assert_eq!(common.map(), Some("map"));
assert_eq!(common.description(), Some("description"));
}
}

View file

@ -0,0 +1,69 @@
/// The implementation.
/// Reference: [Server List Ping](https://wiki.vg/Server_List_Ping)
pub mod protocol;
/// All types used by the implementation.
pub mod types;
#[allow(unused_imports)]
pub use protocol::*;
pub use types::*;
use crate::{GDErrorKind, GDResult};
use std::net::{IpAddr, SocketAddr};
/// Query with all the protocol variants one by one (Java -> Bedrock -> Legacy
/// (1.6 -> 1.4 -> Beta 1.8)).
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<JavaResponse> {
if let Ok(response) = query_java(address, port, None) {
return Ok(response);
}
if let Ok(response) = query_bedrock(address, port) {
return Ok(JavaResponse::from_bedrock_response(response));
}
if let Ok(response) = query_legacy(address, port) {
return Ok(response);
}
Err(GDErrorKind::AutoQuery.into())
}
/// Query a Java Server.
pub fn query_java(
address: &IpAddr,
port: Option<u16>,
request_settings: Option<RequestSettings>,
) -> GDResult<JavaResponse> {
protocol::query_java(
&SocketAddr::new(*address, port_or_java_default(port)),
None,
request_settings,
)
}
/// Query a (Java) Legacy Server (1.6 -> 1.4 -> Beta 1.8).
pub fn query_legacy(address: &IpAddr, port: Option<u16>) -> GDResult<JavaResponse> {
protocol::query_legacy(&SocketAddr::new(*address, port_or_java_default(port)), None)
}
/// Query a specific (Java) Legacy Server.
pub fn query_legacy_specific(group: LegacyGroup, address: &IpAddr, port: Option<u16>) -> GDResult<JavaResponse> {
protocol::query_legacy_specific(
group,
&SocketAddr::new(*address, port_or_java_default(port)),
None,
)
}
/// Query a Bedrock Server.
pub fn query_bedrock(address: &IpAddr, port: Option<u16>) -> GDResult<BedrockResponse> {
protocol::query_bedrock(
&SocketAddr::new(*address, port_or_bedrock_default(port)),
None,
)
}
fn port_or_java_default(port: Option<u16>) -> u16 { port.unwrap_or(25565) }
fn port_or_bedrock_default(port: Option<u16>) -> u16 { port.unwrap_or(19132) }

View file

@ -0,0 +1,111 @@
// This file has code that has been documented by the NodeJS GameDig library
// (MIT) from https://github.com/gamedig/node-gamedig/blob/master/protocols/minecraftbedrock.js
use crate::{
buffer::{Buffer, Utf8Decoder},
games::minecraft::{BedrockResponse, GameMode, Server},
protocols::types::TimeoutSettings,
socket::{Socket, UdpSocket},
utils::{error_by_expected_size, retry_on_timeout},
GDErrorKind::{PacketBad, TypeParse},
GDResult,
};
use std::net::SocketAddr;
use byteorder::LittleEndian;
pub struct Bedrock {
socket: UdpSocket,
retry_count: usize,
}
impl Bedrock {
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = UdpSocket::new(address, &timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
Ok(Self {
socket,
retry_count,
})
}
fn send_status_request(&mut self) -> GDResult<()> {
self.socket.send(&[
0x01, // Message ID: ID_UNCONNECTED_PING
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, // Nonce / timestamp
0x00, 0xff, 0xff, 0x00, 0xfe, 0xfe, 0xfe, 0xfe, 0xfd, 0xfd, 0xfd, 0xfd, 0x12, 0x34, // Magic
0x56, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Client GUID
])?;
Ok(())
}
/// Send a status request, and parse the response.
/// This function will retry fetch on timeouts.
fn get_info(&mut self) -> GDResult<BedrockResponse> {
retry_on_timeout(self.retry_count, move || self.get_info_impl())
}
/// Send a status request, and parse the response (without retry logic).
fn get_info_impl(&mut self) -> GDResult<BedrockResponse> {
self.send_status_request()?;
let received = self.socket.receive(None)?;
let mut buffer = Buffer::<LittleEndian>::new(&received);
if buffer.read::<u8>()? != 0x1c {
return Err(PacketBad.context("Expected 0x1c"));
}
// Checking for our nonce directly from a u64 (as the nonce is 8 bytes).
if buffer.read::<u64>()? != 9_833_440_827_789_222_417 {
return Err(PacketBad.context("Invalid nonce"));
}
// These 8 bytes are identical to the serverId string we receive in decimal
// below
buffer.move_cursor(8)?;
// Verifying the magic value (as we need 16 bytes, cast to two u64 values)
if buffer.read::<u64>()? != 18_374_403_896_610_127_616 {
return Err(PacketBad.context("Invalid magic"));
}
if buffer.read::<u64>()? != 8_671_175_388_723_805_693 {
return Err(PacketBad.context("Invalid magic"));
}
let remaining_length = buffer.switch_endian_chunk(2)?.read::<u16>()? as usize;
error_by_expected_size(remaining_length, buffer.remaining_length())?;
let binding = buffer.read_string::<Utf8Decoder>(None)?;
let status: Vec<&str> = binding.split(';').collect();
// We must have at least 6 values
if status.len() < 6 {
return Err(PacketBad.context("Not enough values"));
}
Ok(BedrockResponse {
edition: status[0].to_string(),
name: status[1].to_string(),
version_name: status[3].to_string(),
protocol_version: status[2].to_string(),
players_maximum: status[5].parse().map_err(|e| TypeParse.context(e))?,
players_online: status[4].parse().map_err(|e| TypeParse.context(e))?,
id: status.get(6).map(std::string::ToString::to_string),
map: status.get(7).map(std::string::ToString::to_string),
game_mode: match status.get(8) {
None => None,
Some(v) => Some(GameMode::from_bedrock(v)?),
},
server_type: Server::Bedrock,
})
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<BedrockResponse> {
Self::new(address, timeout_settings)?.get_info()
}
}

View file

@ -0,0 +1,178 @@
use crate::{
buffer::Buffer,
games::minecraft::{as_string, as_varint, get_string, get_varint, JavaResponse, Player, RequestSettings, Server},
protocols::types::TimeoutSettings,
socket::{Socket, TcpSocket},
utils::retry_on_timeout,
GDErrorKind::{JsonParse, PacketBad},
GDResult,
};
use byteorder::LittleEndian;
use serde_json::Value;
use std::net::SocketAddr;
use std::time::{SystemTime, UNIX_EPOCH};
pub struct Java {
socket: TcpSocket,
request_settings: RequestSettings,
retry_count: usize,
}
impl Java {
fn new(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
request_settings: Option<RequestSettings>,
) -> GDResult<Self> {
let socket = TcpSocket::new(address, &timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
Ok(Self {
socket,
request_settings: request_settings.unwrap_or_default(),
retry_count,
})
}
fn send(&mut self, data: Vec<u8>) -> GDResult<()> {
self.socket
.send(&[as_varint(data.len() as i32), data].concat())
}
fn receive(&mut self) -> GDResult<Vec<u8>> {
let data = &self.socket.receive(None)?;
let mut buffer = Buffer::<LittleEndian>::new(data);
let _packet_length = get_varint(&mut buffer)? as usize;
// this declared 'packet length' from within the packet might be wrong (?), not
// checking with it...
Ok(buffer.remaining_bytes().to_vec())
}
fn send_handshake(&mut self) -> GDResult<()> {
let handshake_payload = [
&[
// Packet ID (0)
0x00,
], // Protocol Version (-1 to determine version)
as_varint(self.request_settings.protocol_version).as_slice(),
// Server address (can be anything)
as_string(&self.request_settings.hostname)?.as_slice(),
// Server port (can be anything)
&self.socket.port().to_le_bytes(),
&[
// Next state (1 for status)
0x01,
],
]
.concat();
self.send(handshake_payload)?;
Ok(())
}
fn send_status_request(&mut self) -> GDResult<()> {
self.send(
[0x00] // Packet ID (0)
.to_vec(),
)?;
Ok(())
}
fn send_ping_request(&mut self) -> GDResult<()> {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
let mut payload = [0x01].to_vec(); // Packet ID (1)
payload.extend_from_slice(&timestamp.to_be_bytes()); // Timestamp (long, 8 bytes)
self.send(payload)?;
Ok(())
}
/// Send minecraft ping request and parse the response.
/// This function will retry fetch on timeouts.
fn get_info(&mut self) -> GDResult<JavaResponse> {
retry_on_timeout(self.retry_count, move || self.get_info_impl())
}
/// Send minecraft ping request and parse the response (without retry
/// logic).
fn get_info_impl(&mut self) -> GDResult<JavaResponse> {
self.send_handshake()?;
self.send_status_request()?;
self.send_ping_request()?;
let socket_data = self.receive()?;
let mut buffer = Buffer::<LittleEndian>::new(&socket_data);
if get_varint(&mut buffer)? != 0 {
// first var int is the packet id
return Err(PacketBad.context("Expected 0"));
}
let json_response = get_string(&mut buffer)?;
let value_response: Value = serde_json::from_str(&json_response).map_err(|e| JsonParse.context(e))?;
let game_version = value_response["version"]["name"]
.as_str()
.ok_or(PacketBad)?
.to_string();
let protocol_version = value_response["version"]["protocol"]
.as_i64()
.ok_or(PacketBad)? as i32;
let max_players = value_response["players"]["max"].as_u64().ok_or(PacketBad)? as u32;
let online_players = value_response["players"]["online"]
.as_u64()
.ok_or(PacketBad)? as u32;
let players: Option<Vec<Player>> = match value_response["players"]["sample"].is_null() {
true => None,
false => {
Some({
let players_values = value_response["players"]["sample"]
.as_array()
.ok_or(PacketBad)?;
let mut players = Vec::with_capacity(players_values.len());
for player in players_values {
players.push(Player {
name: player["name"].as_str().ok_or(PacketBad)?.to_string(),
id: player["id"].as_str().ok_or(PacketBad)?.to_string(),
});
}
players
})
}
};
Ok(JavaResponse {
game_version,
protocol_version,
players_maximum: max_players,
players_online: online_players,
players,
description: value_response["description"].to_string(),
favicon: value_response["favicon"].as_str().map(str::to_string),
previews_chat: value_response["previewsChat"].as_bool(),
enforces_secure_chat: value_response["enforcesSecureChat"].as_bool(),
server_type: Server::Java,
})
}
pub fn query(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
request_settings: Option<RequestSettings>,
) -> GDResult<JavaResponse> {
Self::new(address, timeout_settings, request_settings)?.get_info()
}
}

View file

@ -0,0 +1,83 @@
use byteorder::BigEndian;
use crate::minecraft::protocol::legacy_v1_6::LegacyV1_6;
use crate::{
buffer::{Buffer, Utf16Decoder},
games::minecraft::{JavaResponse, LegacyGroup, Server},
protocols::types::TimeoutSettings,
socket::{Socket, TcpSocket},
utils::{error_by_expected_size, retry_on_timeout},
GDErrorKind::{PacketBad, ProtocolFormat},
GDResult,
};
use std::net::SocketAddr;
pub struct LegacyV1_4 {
socket: TcpSocket,
retry_count: usize,
}
impl LegacyV1_4 {
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = TcpSocket::new(address, &timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
Ok(Self {
socket,
retry_count,
})
}
fn send_initial_request(&mut self) -> GDResult<()> { self.socket.send(&[0xFE, 0x01]) }
/// Send info request and parse response.
/// This function will retry fetch on timeouts.
fn get_info(&mut self) -> GDResult<JavaResponse> {
retry_on_timeout(self.retry_count, move || self.get_info_impl())
}
/// Send info request and parse response (without retry logic).
fn get_info_impl(&mut self) -> GDResult<JavaResponse> {
self.send_initial_request()?;
let data = self.socket.receive(None)?;
let mut buffer = Buffer::<BigEndian>::new(&data);
if buffer.read::<u8>()? != 0xFF {
return Err(ProtocolFormat.context("Expected 0xFF"));
}
let length = buffer.read::<u16>()? * 2;
error_by_expected_size((length + 3) as usize, data.len())?;
if LegacyV1_6::is_protocol(&mut buffer)? {
return LegacyV1_6::get_response(&mut buffer);
}
let packet_string = buffer.read_string::<Utf16Decoder<BigEndian>>(None)?;
let split: Vec<&str> = packet_string.split('§').collect();
error_by_expected_size(3, split.len())?;
let description = split[0].to_string();
let online_players = split[1].parse().map_err(|e| PacketBad.context(e))?;
let max_players = split[2].parse().map_err(|e| PacketBad.context(e))?;
Ok(JavaResponse {
game_version: "1.4+".to_string(),
protocol_version: -1,
players_maximum: max_players,
players_online: online_players,
players: None,
description,
favicon: None,
previews_chat: None,
enforces_secure_chat: None,
server_type: Server::Legacy(LegacyGroup::V1_4),
})
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<JavaResponse> {
Self::new(address, timeout_settings)?.get_info()
}
}

View file

@ -0,0 +1,116 @@
use byteorder::BigEndian;
use crate::{
buffer::{Buffer, Utf16Decoder},
games::minecraft::{JavaResponse, LegacyGroup, Server},
protocols::types::TimeoutSettings,
socket::{Socket, TcpSocket},
utils::{error_by_expected_size, retry_on_timeout},
GDErrorKind::{PacketBad, ProtocolFormat},
GDResult,
};
use std::net::SocketAddr;
pub struct LegacyV1_6 {
socket: TcpSocket,
retry_count: usize,
}
impl LegacyV1_6 {
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = TcpSocket::new(address, &timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
Ok(Self {
socket,
retry_count,
})
}
fn send_initial_request(&mut self) -> GDResult<()> {
self.socket.send(&[
0xfe, // Packet ID (FE)
0x01, // Ping payload (01)
0xfa, // Packet identifier for plugin message
0x00, 0x07, // Length of 'GameDig' string (7) as unsigned short
0x00, 0x47, 0x00, 0x61, 0x00, 0x6D, 0x00, 0x65, 0x00, 0x44, 0x00, 0x69, 0x00,
0x67, // 'GameDig' string as UTF-16BE
])?;
Ok(())
}
pub(crate) fn is_protocol(buffer: &mut Buffer<BigEndian>) -> GDResult<bool> {
let state = buffer
.remaining_bytes()
.starts_with(&[0x00, 0xA7, 0x00, 0x31, 0x00, 0x00]);
if state {
buffer.move_cursor(6)?;
}
Ok(state)
}
pub(crate) fn get_response(buffer: &mut Buffer<BigEndian>) -> GDResult<JavaResponse> {
// This is a specific order!
let protocol_version = buffer
.read_string::<Utf16Decoder<BigEndian>>(None)?
.parse()
.map_err(|e| PacketBad.context(e))?;
let game_version = buffer.read_string::<Utf16Decoder<BigEndian>>(None)?;
let description = buffer.read_string::<Utf16Decoder<BigEndian>>(None)?;
let online_players = buffer
.read_string::<Utf16Decoder<BigEndian>>(None)?
.parse()
.map_err(|e| PacketBad.context(e))?;
let max_players = buffer
.read_string::<Utf16Decoder<BigEndian>>(None)?
.parse()
.map_err(|e| PacketBad.context(e))?;
Ok(JavaResponse {
game_version,
protocol_version,
players_maximum: max_players,
players_online: online_players,
players: None,
description,
favicon: None,
previews_chat: None,
enforces_secure_chat: None,
server_type: Server::Legacy(LegacyGroup::V1_6),
})
}
/// Send info request and parse response.
/// This function will retry fetch on timeouts.
fn get_info(&mut self) -> GDResult<JavaResponse> {
retry_on_timeout(self.retry_count, move || self.get_info_impl())
}
/// Send info request and parse response (without retry logic).
fn get_info_impl(&mut self) -> GDResult<JavaResponse> {
self.send_initial_request()?;
let data = self.socket.receive(None)?;
let mut buffer = Buffer::<BigEndian>::new(&data);
if buffer.read::<u8>()? != 0xFF {
return Err(ProtocolFormat.context("Expected 0xFF"));
}
let length = buffer.read::<u16>()? * 2;
error_by_expected_size((length + 3) as usize, data.len())?;
if !Self::is_protocol(&mut buffer)? {
return Err(ProtocolFormat.context("Not legacy 1.6 protocol"));
}
Self::get_response(&mut buffer)
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<JavaResponse> {
Self::new(address, timeout_settings)?.get_info()
}
}

View file

@ -0,0 +1,79 @@
use crate::{
buffer::{Buffer, Utf16Decoder},
games::minecraft::{JavaResponse, LegacyGroup, Server},
protocols::types::TimeoutSettings,
socket::{Socket, TcpSocket},
utils::{error_by_expected_size, retry_on_timeout},
GDErrorKind::{PacketBad, ProtocolFormat},
GDResult,
};
use std::net::SocketAddr;
use byteorder::BigEndian;
pub struct LegacyVB1_8 {
socket: TcpSocket,
retry_count: usize,
}
impl LegacyVB1_8 {
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = TcpSocket::new(address, &timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
Ok(Self {
socket,
retry_count,
})
}
fn send_initial_request(&mut self) -> GDResult<()> { self.socket.send(&[0xFE]) }
/// Send request for info and parse response.
/// This function will retry fetch on timeouts.
fn get_info(&mut self) -> GDResult<JavaResponse> {
retry_on_timeout(self.retry_count, move || self.get_info_impl())
}
/// Send request for info and parse response (without retry logic).
fn get_info_impl(&mut self) -> GDResult<JavaResponse> {
self.send_initial_request()?;
let data = self.socket.receive(None)?;
let mut buffer = Buffer::<BigEndian>::new(&data);
if buffer.read::<u8>()? != 0xFF {
return Err(ProtocolFormat.context("Expected 0xFF"));
}
let length = buffer.read::<u16>()? * 2;
error_by_expected_size((length + 3) as usize, data.len())?;
let packet_string = buffer.read_string::<Utf16Decoder<BigEndian>>(None)?;
let split: Vec<&str> = packet_string.split('§').collect();
error_by_expected_size(3, split.len())?;
let description = split[0].to_string();
let online_players = split[1].parse().map_err(|e| PacketBad.context(e))?;
let max_players = split[2].parse().map_err(|e| PacketBad.context(e))?;
Ok(JavaResponse {
game_version: "Beta 1.8+".to_string(),
protocol_version: -1,
players_maximum: max_players,
players_online: online_players,
players: None,
description,
favicon: None,
previews_chat: None,
enforces_secure_chat: None,
server_type: Server::Legacy(LegacyGroup::VB1_8),
})
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<JavaResponse> {
Self::new(address, timeout_settings)?.get_info()
}
}

View file

@ -0,0 +1,91 @@
use crate::games::minecraft::types::RequestSettings;
use crate::{
games::minecraft::{
protocol::{
bedrock::Bedrock,
java::Java,
legacy_v1_4::LegacyV1_4,
legacy_v1_6::LegacyV1_6,
legacy_vb1_8::LegacyVB1_8,
},
BedrockResponse,
JavaResponse,
LegacyGroup,
},
protocols::types::TimeoutSettings,
GDErrorKind::AutoQuery,
GDResult,
};
use std::net::SocketAddr;
mod bedrock;
mod java;
mod legacy_v1_4;
mod legacy_v1_6;
mod legacy_vb1_8;
/// Queries a Minecraft server with all the protocol variants one by one (Java
/// -> Bedrock -> Legacy (1.6 -> 1.4 -> Beta 1.8)).
pub fn query(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
request_settings: Option<RequestSettings>,
) -> GDResult<JavaResponse> {
if let Ok(response) = query_java(address, timeout_settings, request_settings) {
return Ok(response);
}
if let Ok(response) = query_bedrock(address, timeout_settings) {
return Ok(JavaResponse::from_bedrock_response(response));
}
if let Ok(response) = query_legacy(address, timeout_settings) {
return Ok(response);
}
Err(AutoQuery.into())
}
/// Query a Java Server.
pub fn query_java(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
request_settings: Option<RequestSettings>,
) -> GDResult<JavaResponse> {
Java::query(address, timeout_settings, request_settings)
}
/// Query a (Java) Legacy Server (1.6 -> 1.4 -> Beta 1.8).
pub fn query_legacy(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<JavaResponse> {
if let Ok(response) = query_legacy_specific(LegacyGroup::V1_6, address, timeout_settings) {
return Ok(response);
}
if let Ok(response) = query_legacy_specific(LegacyGroup::V1_4, address, timeout_settings) {
return Ok(response);
}
if let Ok(response) = query_legacy_specific(LegacyGroup::VB1_8, address, timeout_settings) {
return Ok(response);
}
Err(AutoQuery.into())
}
/// Query a specific (Java) Legacy Server.
pub fn query_legacy_specific(
group: LegacyGroup,
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<JavaResponse> {
match group {
LegacyGroup::V1_6 => LegacyV1_6::query(address, timeout_settings),
LegacyGroup::V1_4 => LegacyV1_4::query(address, timeout_settings),
LegacyGroup::VB1_8 => LegacyVB1_8::query(address, timeout_settings),
}
}
/// Query a Bedrock Server.
pub fn query_bedrock(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<BedrockResponse> {
Bedrock::query(address, timeout_settings)
}

View file

@ -0,0 +1,337 @@
// Although its a lightly modified version, this file contains code
// by Jaiden Bernard (2021-2022 - MIT) from
// https://github.com/thisjaiden/golden_apple/blob/master/src/lib.rs
use crate::{
buffer::Buffer,
protocols::{
types::{CommonPlayer, CommonResponse, ExtraRequestSettings, GenericPlayer},
GenericResponse,
},
GDErrorKind::{InvalidInput, PacketBad, UnknownEnumCast},
GDResult,
};
use byteorder::ByteOrder;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// The type of Minecraft Server you want to query.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Server {
/// Java Edition.
Java,
/// Legacy Java.
Legacy(LegacyGroup),
/// Bedrock Edition.
Bedrock,
}
/// Legacy Java (Versions) Groups.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum LegacyGroup {
/// 1.6
V1_6,
/// 1.4 - 1.5
V1_4,
/// Beta 1.8 - 1.3
VB1_8,
}
/// Information about a player.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
pub name: String,
pub id: String,
}
impl CommonPlayer for Player {
fn as_original(&self) -> GenericPlayer<'_> { GenericPlayer::Minecraft(self) }
fn name(&self) -> &str { &self.name }
}
/// Versioned response type
#[cfg_attr(feature = "serde", derive(Serialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionedResponse<'a> {
Bedrock(&'a BedrockResponse),
Java(&'a JavaResponse),
}
/// A Java query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct JavaResponse {
/// Version name, example: "1.19.2".
pub game_version: String,
/// Protocol version, example: 760 (for 1.19.1 or 1.19.2).
/// Note that for versions below 1.6 this field is always -1.
pub protocol_version: i32,
/// Number of server capacity.
pub players_maximum: u32,
/// Number of online players.
pub players_online: u32,
/// Some online players (can be missing).
pub players: Option<Vec<Player>>,
/// Server's description or MOTD.
pub description: String,
/// The favicon (can be missing).
pub favicon: Option<String>,
/// Tells if the chat preview is enabled (can be missing).
pub previews_chat: Option<bool>,
/// Tells if secure chat is enforced (can be missing).
pub enforces_secure_chat: Option<bool>,
/// Tell's the server type.
pub server_type: Server,
}
/// Java-only additional request settings.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct RequestSettings {
/// Some Minecraft servers do not respond as expected if this
/// isn't a specific value, `mc.hypixel.net` is an example.
pub hostname: String,
/// Specifies the client [protocol version number](https://wiki.vg/Protocol_version_numbers),
/// `-1` means anything.
pub protocol_version: i32,
}
impl Default for RequestSettings {
/// `hostname`: "gamedig"
/// `protocol_version`: -1
fn default() -> Self {
Self {
hostname: "gamedig".to_string(),
protocol_version: -1,
}
}
}
impl RequestSettings {
/// Make a new *RequestSettings* with just the hostname, the protocol
/// version defaults to -1
pub const fn new_just_hostname(hostname: String) -> Self {
Self {
hostname,
protocol_version: -1,
}
}
}
impl From<ExtraRequestSettings> for RequestSettings {
fn from(value: ExtraRequestSettings) -> Self {
let default = Self::default();
Self {
hostname: value.hostname.unwrap_or(default.hostname),
protocol_version: value.protocol_version.unwrap_or(default.protocol_version),
}
}
}
impl CommonResponse for JavaResponse {
fn as_original(&self) -> GenericResponse<'_> { GenericResponse::Minecraft(VersionedResponse::Java(self)) }
fn description(&self) -> Option<&str> { Some(&self.description) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
self.players
.as_ref()
.map(|players| players.iter().map(|p| p as &dyn CommonPlayer).collect())
}
}
/// A Bedrock Edition query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct BedrockResponse {
/// Server's edition.
pub edition: String,
/// Server's name.
pub name: String,
/// Version name, example: "1.19.40".
pub version_name: String,
/// Protocol version, example: 760 (for 1.19.2).
pub protocol_version: String,
/// Maximum number of players the server reports it can hold.
pub players_maximum: u32,
/// Number of players on the server.
pub players_online: u32,
/// Server id.
pub id: Option<String>,
/// Currently running map's name.
pub map: Option<String>,
/// Current game mode.
pub game_mode: Option<GameMode>,
/// Tells the server type.
pub server_type: Server,
}
impl CommonResponse for BedrockResponse {
fn as_original(&self) -> GenericResponse<'_> { GenericResponse::Minecraft(VersionedResponse::Bedrock(self)) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn map(&self) -> Option<&str> { self.map.as_deref() }
fn game_version(&self) -> Option<&str> { Some(&self.version_name) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
}
impl JavaResponse {
pub fn from_bedrock_response(response: BedrockResponse) -> Self {
Self {
game_version: response.version_name,
protocol_version: 0,
players_maximum: response.players_maximum,
players_online: response.players_online,
players: None,
description: response.name,
favicon: None,
previews_chat: None,
enforces_secure_chat: None,
server_type: Server::Bedrock,
}
}
}
/// A server's game mode (used only by Bedrock servers.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum GameMode {
Survival,
Creative,
Hardcore,
Spectator,
Adventure,
}
impl GameMode {
pub fn from_bedrock(value: &&str) -> GDResult<Self> {
match *value {
"Survival" => Ok(Self::Survival),
"Creative" => Ok(Self::Creative),
"Hardcore" => Ok(Self::Hardcore),
"Spectator" => Ok(Self::Spectator),
"Adventure" => Ok(Self::Adventure),
_ => Err(UnknownEnumCast.context(format!("Unknown gamemode {value:?}"))),
}
}
}
pub(crate) fn get_varint<B: ByteOrder>(buffer: &mut Buffer<B>) -> GDResult<i32> {
let mut result = 0;
let msb: u8 = 0b1000_0000;
let mask: u8 = !msb;
for i in 0 .. 5 {
let current_byte = buffer.read::<u8>()?;
result |= ((current_byte & mask) as i32) << (7 * i);
// The 5th byte is only allowed to have the 4 smallest bits set
if i == 4 && (current_byte & 0xf0 != 0) {
return Err(PacketBad.context("Bad 5th byte"));
}
if (current_byte & msb) == 0 {
break;
}
}
Ok(result)
}
pub(crate) fn as_varint(value: i32) -> Vec<u8> {
let mut bytes = vec![];
let mut reading_value = value;
let msb: u8 = 0b1000_0000;
let mask: i32 = 0b0111_1111;
for _ in 0 .. 5 {
let tmp = (reading_value & mask) as u8;
reading_value &= !mask;
reading_value = reading_value.rotate_right(7);
if reading_value == 0 {
bytes.push(tmp);
break;
}
bytes.push(tmp | msb);
}
bytes
}
pub(crate) fn get_string<B: ByteOrder>(buffer: &mut Buffer<B>) -> GDResult<String> {
let length = get_varint(buffer)? as usize;
let mut text = Vec::with_capacity(length);
for _ in 0 .. length {
text.push(buffer.read::<u8>()?)
}
String::from_utf8(text).map_err(|e| PacketBad.context(e))
}
pub(crate) fn as_string(value: &str) -> GDResult<Vec<u8>> {
let length = value
.len()
.try_into()
.map_err(|e| InvalidInput.context(e))?;
let mut buf = as_varint(length);
buf.extend(value.as_bytes());
Ok(buf)
}
#[cfg(test)]
mod tests {
use super::{as_string, as_varint, get_varint};
use crate::buffer::Buffer;
use crate::minecraft::get_string;
use byteorder::LittleEndian;
#[test]
fn int_as_varint() {
assert_eq!(as_varint(1), [1]);
assert_eq!(as_varint(25565), [221, 199, 1]);
assert_eq!(as_varint(1298923567), [175, 128, 176, 235, 4]);
}
#[test]
fn varint_as_int() {
let mut buffer = Buffer::<LittleEndian>::new(&[1, 127, 221, 199, 1, 0]);
assert_eq!(get_varint(&mut buffer), Ok(1));
assert_eq!(get_varint(&mut buffer), Ok(127));
assert_eq!(get_varint(&mut buffer), Ok(25565));
assert_eq!(buffer.remaining_bytes(), [0]);
}
#[test]
fn string_as_minecraft_string() {
assert_eq!(as_string("A"), Ok(vec![1, 65]));
assert_eq!(
as_string("VarString"),
Ok(vec![9, 86, 97, 114, 83, 116, 114, 105, 110, 103])
);
}
#[test]
fn minecraft_get_string() {
let mut buffer = Buffer::<LittleEndian>::new(&[3, 65, 65, 65, 1, 66]);
assert_eq!(get_string(&mut buffer), Ok("AAA".to_string()));
assert_eq!(get_string(&mut buffer), Ok("B".to_string()));
assert_eq!(buffer.remaining_length(), 0);
}
}

View file

@ -0,0 +1,8 @@
/// The implementation.
/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/minetest.js)
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;

View file

@ -0,0 +1,23 @@
use crate::minetest::Response;
use crate::{minetest_master_server, GDErrorKind, GDResult, TimeoutSettings};
use std::net::IpAddr;
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, &None) }
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: &Option<TimeoutSettings>,
) -> GDResult<Response> {
let address = address.to_string();
let port = port.unwrap_or(30000);
let servers = minetest_master_server::query(timeout_settings.unwrap_or_default())?;
for server in servers.list {
if server.ip == address && server.port == port {
return Ok(server.into());
}
}
Err(GDErrorKind::AutoQuery.context("Server not found in the master query list."))
}

View file

@ -0,0 +1,108 @@
use crate::minetest_master_server::Server;
use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer};
use crate::protocols::GenericResponse;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
pub name: String,
}
impl CommonPlayer for Player {
fn as_original(&self) -> GenericPlayer { GenericPlayer::Minetest(self) }
fn name(&self) -> &str { &self.name }
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Response {
pub name: String,
pub description: String,
pub game_version: String,
pub players_maximum: u32,
pub players_online: u32,
pub has_password: Option<bool>,
pub players: Vec<Player>,
pub id: String,
pub ip: String,
pub port: u16,
pub creative: Option<bool>,
pub damage: bool,
pub game_time: u32,
pub lag: Option<f32>,
pub proto_max: u16,
pub proto_min: u16,
pub pvp: bool,
pub uptime: u32,
pub url: Option<String>,
pub update_time: u32,
pub start: u32,
pub clients_top: u32,
pub updates: u32,
pub pop_v: f32,
pub geo_continent: Option<String>,
pub ping: f32,
}
impl From<Server> for Response {
fn from(server: Server) -> Self {
Self {
name: server.name,
description: server.description,
game_version: server.version,
players_maximum: server.clients_max,
players_online: server.total_clients,
has_password: server.password,
players: server
.clients_list
.unwrap_or_default()
.into_iter()
.map(|name| Player { name })
.collect(),
ip: server.address,
creative: server.creative,
damage: server.damage,
game_time: server.game_time,
id: server.gameid,
lag: server.lag,
port: server.port,
proto_max: server.proto_max,
proto_min: server.proto_min,
pvp: server.pvp,
uptime: server.uptime,
url: server.url,
update_time: server.update_time,
start: server.start,
clients_top: server.clients_top,
updates: server.updates,
pop_v: server.pop_v,
geo_continent: server.geo_continent,
ping: server.ping,
}
}
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse { GenericResponse::Minetest(self) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn description(&self) -> Option<&str> { Some(&self.description) }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
fn has_password(&self) -> Option<bool> { self.has_password }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
Some(
self.players
.iter()
.map(|p| p as &dyn CommonPlayer)
.collect(),
)
}
}

View file

@ -0,0 +1,50 @@
//! Currently supported games.
#[cfg(feature = "tls")]
pub mod epic;
pub mod gamespy;
pub mod quake;
pub mod unreal2;
pub mod valve;
#[cfg(all(feature = "tls", feature = "serde", feature = "services"))]
pub mod minetest;
#[cfg(feature = "tls")]
pub use epic::*;
pub use gamespy::*;
pub use quake::*;
pub use unreal2::*;
pub use valve::*;
#[cfg(all(feature = "tls", feature = "serde", feature = "services"))]
pub use minetest::*;
/// Battalion 1944
pub mod battalion1944;
/// Eco
pub mod eco;
/// Frontlines: Fuel of War
pub mod ffow;
/// Just Cause 2: Multiplayer
pub mod jc2m;
/// Mindustry
pub mod mindustry;
/// Minecraft
pub mod minecraft;
/// Savage 2
pub mod savage2;
/// The Ship
pub mod theship;
pub mod types;
pub use types::*;
pub mod query;
pub use query::*;
#[cfg(feature = "game_defs")]
mod definitions;
#[cfg(feature = "game_defs")]
pub use definitions::GAMES;

View file

@ -0,0 +1,9 @@
//! Quake game query modules
use crate::protocols::quake::game_query_mod;
game_query_mod!(quake1, "Quake 1", one, 27500);
game_query_mod!(quake2, "Quake 2", two, 27910);
game_query_mod!(q3a, "Quake 3 Arena", three, 27960);
game_query_mod!(sof2, "Soldier of Fortune 2", three, 20100);
game_query_mod!(warsow, "Warsow", three, 44400);

View file

@ -0,0 +1,137 @@
//! Generic query functions
use std::net::{IpAddr, SocketAddr};
#[cfg(all(feature = "services", feature = "tls", feature = "serde"))]
use crate::games::minetest;
use crate::games::types::Game;
use crate::games::{eco, ffow, jc2m, mindustry, minecraft, savage2, theship};
use crate::protocols;
use crate::protocols::gamespy::GameSpyVersion;
use crate::protocols::quake::QuakeVersion;
use crate::protocols::types::{CommonResponse, ExtraRequestSettings, ProprietaryProtocol, Protocol, TimeoutSettings};
use crate::GDResult;
/// Make a query given a game definition
#[inline]
pub fn query(game: &Game, address: &IpAddr, port: Option<u16>) -> GDResult<Box<dyn CommonResponse>> {
query_with_timeout_and_extra_settings(game, address, port, None, None)
}
/// Make a query given a game definition and timeout settings
#[inline]
pub fn query_with_timeout(
game: &Game,
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Box<dyn CommonResponse>> {
query_with_timeout_and_extra_settings(game, address, port, timeout_settings, None)
}
/// Make a query given a game definition, timeout settings, and extra settings
pub fn query_with_timeout_and_extra_settings(
game: &Game,
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
extra_settings: Option<ExtraRequestSettings>,
) -> GDResult<Box<dyn CommonResponse>> {
let socket_addr = SocketAddr::new(*address, port.unwrap_or(game.default_port));
Ok(match &game.protocol {
Protocol::Valve(engine) => {
protocols::valve::query(
&socket_addr,
*engine,
extra_settings
.or_else(|| Option::from(game.request_settings.clone()))
.map(ExtraRequestSettings::into),
timeout_settings,
)
.map(Box::new)?
}
#[cfg(feature = "tls")]
Protocol::Epic(credentials) => {
protocols::epic::query_with_timeout(credentials.clone(), &socket_addr, timeout_settings).map(Box::new)?
}
Protocol::Gamespy(version) => {
match version {
GameSpyVersion::One => protocols::gamespy::one::query(&socket_addr, timeout_settings).map(Box::new)?,
GameSpyVersion::Two => protocols::gamespy::two::query(&socket_addr, timeout_settings).map(Box::new)?,
GameSpyVersion::Three => {
protocols::gamespy::three::query(&socket_addr, timeout_settings).map(Box::new)?
}
}
}
Protocol::Quake(version) => {
match version {
QuakeVersion::One => protocols::quake::one::query(&socket_addr, timeout_settings).map(Box::new)?,
QuakeVersion::Two => protocols::quake::two::query(&socket_addr, timeout_settings).map(Box::new)?,
QuakeVersion::Three => protocols::quake::three::query(&socket_addr, timeout_settings).map(Box::new)?,
}
}
Protocol::Unreal2 => {
protocols::unreal2::query(
&socket_addr,
&extra_settings
.map(ExtraRequestSettings::into)
.unwrap_or_default(),
timeout_settings,
)
.map(Box::new)?
}
Protocol::PROPRIETARY(protocol) => {
match protocol {
ProprietaryProtocol::Savage2 => {
savage2::query_with_timeout(address, port, timeout_settings).map(Box::new)?
}
ProprietaryProtocol::TheShip => {
theship::query_with_timeout(address, port, timeout_settings).map(Box::new)?
}
ProprietaryProtocol::FFOW => ffow::query_with_timeout(address, port, timeout_settings).map(Box::new)?,
ProprietaryProtocol::JC2M => jc2m::query_with_timeout(address, port, timeout_settings).map(Box::new)?,
ProprietaryProtocol::Mindustry => mindustry::query(address, port, &timeout_settings).map(Box::new)?,
ProprietaryProtocol::Minecraft(version) => {
match version {
Some(minecraft::Server::Java) => {
minecraft::protocol::query_java(
&socket_addr,
timeout_settings,
extra_settings.map(ExtraRequestSettings::into),
)
.map(Box::new)?
}
Some(minecraft::Server::Bedrock) => {
minecraft::protocol::query_bedrock(&socket_addr, timeout_settings).map(Box::new)?
}
Some(minecraft::Server::Legacy(group)) => {
minecraft::protocol::query_legacy_specific(*group, &socket_addr, timeout_settings)
.map(Box::new)?
}
None => {
minecraft::protocol::query(
&socket_addr,
timeout_settings,
extra_settings.map(ExtraRequestSettings::into),
)
.map(Box::new)?
}
}
}
ProprietaryProtocol::Eco => {
eco::query_with_timeout_and_extra_settings(
address,
port,
&timeout_settings,
extra_settings.map(ExtraRequestSettings::into),
)
.map(Box::new)?
}
#[cfg(all(feature = "services", feature = "tls", feature = "serde"))]
ProprietaryProtocol::Minetest => {
minetest::query_with_timeout(address, port, &timeout_settings).map(Box::new)?
}
}
}
})
}

View file

@ -0,0 +1,8 @@
/// The implementation.
/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/savage2.js)
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;

View file

@ -0,0 +1,37 @@
use crate::buffer::{Buffer, Utf8Decoder};
use crate::games::savage2::types::Response;
use crate::protocols::types::TimeoutSettings;
use crate::socket::{Socket, UdpSocket};
use crate::GDResult;
use byteorder::LittleEndian;
use std::net::{IpAddr, SocketAddr};
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let addr = &SocketAddr::new(*address, port.unwrap_or(11235));
let mut socket = UdpSocket::new(addr, &timeout_settings)?;
socket.send(&[0x01])?;
let data = socket.receive(None)?;
let mut buffer = Buffer::<LittleEndian>::new(&data);
buffer.move_cursor(12)?;
Ok(Response {
name: buffer.read_string::<Utf8Decoder>(None)?,
players_online: buffer.read::<u8>()?,
players_maximum: buffer.read::<u8>()?,
time: buffer.read_string::<Utf8Decoder>(None)?,
map: buffer.read_string::<Utf8Decoder>(None)?,
next_map: buffer.read_string::<Utf8Decoder>(None)?,
location: buffer.read_string::<Utf8Decoder>(None)?,
players_minimum: buffer.read::<u8>()?,
game_mode: buffer.read_string::<Utf8Decoder>(None)?,
protocol_version: buffer.read_string::<Utf8Decoder>(None)?,
level_minimum: buffer.read::<u8>()?,
})
}

View file

@ -0,0 +1,30 @@
use crate::protocols::types::CommonResponse;
use crate::protocols::GenericResponse;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response {
pub name: String,
pub players_online: u8,
pub players_maximum: u8,
pub players_minimum: u8,
pub time: String,
pub map: String,
pub next_map: String,
pub location: String,
pub game_mode: String,
pub protocol_version: String,
pub level_minimum: u8,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse<'_> { GenericResponse::Savage2(self) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn players_maximum(&self) -> u32 { self.players_maximum.into() }
fn players_online(&self) -> u32 { self.players_online.into() }
}

View file

@ -0,0 +1,8 @@
/// The implementation.
/// Reference: [server queries](https://developer.valvesoftware.com/wiki/Server_queries)
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;

View file

@ -0,0 +1,23 @@
use crate::games::theship::types::Response;
use crate::protocols::types::TimeoutSettings;
use crate::protocols::valve;
use crate::protocols::valve::Engine;
use crate::GDResult;
use std::net::{IpAddr, SocketAddr};
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let valve_response = valve::query(
&SocketAddr::new(*address, port.unwrap_or(27015)),
Engine::new(2400),
None,
timeout_settings,
)?;
Response::new_from_valve_response(valve_response)
}

View file

@ -0,0 +1,122 @@
use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer};
use crate::protocols::valve::{get_optional_extracted_data, Server, ServerPlayer};
use crate::protocols::{valve, GenericResponse};
use crate::GDErrorKind::PacketBad;
use crate::GDResult;
use std::collections::HashMap;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub struct TheShipPlayer {
pub name: String,
pub score: i32,
pub duration: f32,
pub deaths: u32,
pub money: u32,
}
impl TheShipPlayer {
pub fn new_from_valve_player(player: &ServerPlayer) -> GDResult<Self> {
Ok(Self {
name: player.name.clone(),
score: player.score,
duration: player.duration,
deaths: player.deaths.ok_or(PacketBad)?,
money: player.money.ok_or(PacketBad)?,
})
}
}
impl CommonPlayer for TheShipPlayer {
fn as_original(&self) -> GenericPlayer<'_> { GenericPlayer::TheShip(self) }
fn name(&self) -> &str { &self.name }
fn score(&self) -> Option<i32> { Some(self.score) }
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct Response {
pub protocol_version: u8,
pub name: String,
pub map: String,
pub game_mode: String,
pub game_version: String,
pub players: Vec<TheShipPlayer>,
pub players_online: u8,
pub players_maximum: u8,
pub players_bots: u8,
pub server_type: Server,
pub has_password: bool,
pub vac_secured: bool,
pub port: Option<u16>,
pub steam_id: Option<u64>,
pub tv_port: Option<u16>,
pub tv_name: Option<String>,
pub keywords: Option<String>,
pub rules: HashMap<String, String>,
pub mode: u8,
pub witnesses: u8,
pub duration: u8,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse<'_> { GenericResponse::TheShip(self) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
fn players_maximum(&self) -> u32 { self.players_maximum.into() }
fn players_online(&self) -> u32 { self.players_online.into() }
fn players_bots(&self) -> Option<u32> { Some(self.players_bots.into()) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
Some(
self.players
.iter()
.map(|p| p as &dyn CommonPlayer)
.collect(),
)
}
}
impl Response {
pub fn new_from_valve_response(response: valve::Response) -> GDResult<Self> {
let (port, steam_id, tv_port, tv_name, keywords) = get_optional_extracted_data(response.info.extra_data);
let the_unwrapped_ship = response.info.the_ship.ok_or(PacketBad)?;
Ok(Self {
protocol_version: response.info.protocol_version,
name: response.info.name,
map: response.info.map,
game_mode: response.info.game_mode,
game_version: response.info.game_version,
players_online: response.info.players_online,
players: response
.players
.ok_or(PacketBad)?
.iter()
.map(TheShipPlayer::new_from_valve_player)
.collect::<GDResult<Vec<TheShipPlayer>>>()?,
players_maximum: response.info.players_maximum,
players_bots: response.info.players_bots,
server_type: response.info.server_type,
has_password: response.info.has_password,
vac_secured: response.info.vac_secured,
port,
steam_id,
tv_port,
tv_name,
keywords,
rules: response.rules.ok_or(PacketBad)?,
mode: the_unwrapped_ship.mode,
witnesses: the_unwrapped_ship.witnesses,
duration: the_unwrapped_ship.duration,
})
}
}

View file

@ -0,0 +1,20 @@
//! Game related types
use crate::protocols::types::{ExtraRequestSettings, Protocol};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// Definition of a game
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Game {
/// Full name of the game
pub name: &'static str,
/// Default port used by game
pub default_port: u16,
/// The protocol the game's query uses
pub protocol: Protocol,
/// Request settings.
pub request_settings: ExtraRequestSettings,
}

View file

@ -0,0 +1,10 @@
//! Unreal2 game query modules
use crate::protocols::unreal2::game_query_mod;
game_query_mod!(darkesthour, "Darkest Hour: Europe '44-'45 (2008)", 7758);
game_query_mod!(devastation, "Devastation (2003)", 7778);
game_query_mod!(killingfloor, "Killing Floor", 7708);
game_query_mod!(redorchestra, "Red Orchestra", 7759);
game_query_mod!(ut2003, "Unreal Tournament 2003", 7758);
game_query_mod!(ut2004, "Unreal Tournament 2004", 7778);

View file

@ -0,0 +1,201 @@
//! Valve game query modules
use crate::protocols::valve::game_query_mod;
game_query_mod!(abioticfactor, "Abiotic Factor", Engine::new(427_410), 27015);
game_query_mod!(
a2oa,
"ARMA 2: Operation Arrowhead",
Engine::new(33930),
2304
);
game_query_mod!(arma3, "ARMA 3", Engine::new(107_410), 2303);
game_query_mod!(basedefense, "Base Defense", Engine::new(632_730), 27015);
game_query_mod!(alienswarm, "Alien Swarm", Engine::new(630), 27015);
game_query_mod!(aoc, "Age of Chivalry", Engine::new(17510), 27015);
game_query_mod!(
aapg,
"America's Army: Proving Grounds",
Engine::new(203_290),
27020,
GatheringSettings {
players: GatherToggle::Enforce,
rules: GatherToggle::Skip,
check_app_id: true,
}
);
game_query_mod!(ase, "ARK: Survival Evolved", Engine::new(346_110), 27015);
game_query_mod!(
asrd,
"Alien Swarm: Reactive Drop",
Engine::new(563_560),
2304
);
game_query_mod!(atlas, "ATLAS", Engine::new(834_910), 57561);
game_query_mod!(avorion, "Avorion", Engine::new(445_220), 27020);
game_query_mod!(
ballisticoverkill,
"Ballistic Overkill",
Engine::new(296_300),
27016
);
game_query_mod!(
armareforger,
"Arma Reforger",
Engine::new(0),
17777,
GatheringSettings {
players: GatherToggle::Enforce,
rules: GatherToggle::Enforce,
check_app_id: false,
}
);
game_query_mod!(
avp2010,
"Aliens vs. Predator 2010",
Engine::new(10_680),
27015
);
game_query_mod!(barotrauma, "Barotrauma", Engine::new(602_960), 27016);
game_query_mod!(blackmesa, "Black Mesa", Engine::new(362_890), 27015);
game_query_mod!(brainbread2, "BrainBread 2", Engine::new(346_330), 27015);
game_query_mod!(
codbo3,
"Call Of Duty: Black Ops 3",
Engine::new(311_210),
27017
);
game_query_mod!(codenamecure, "Codename CURE", Engine::new(355_180), 27015);
game_query_mod!(
colonysurvival,
"Colony Survival",
Engine::new(366_090),
27004
);
game_query_mod!(
conanexiles,
"Conan Exiles",
Engine::new(440_900),
27015,
GatheringSettings {
players: GatherToggle::Skip,
rules: GatherToggle::Enforce,
check_app_id: true,
}
);
game_query_mod!(
counterstrike,
"Counter-Strike",
Engine::new_gold_src(false),
27015
);
game_query_mod!(counterstrike2, "Counter-Strike 2", Engine::new(730), 27015);
game_query_mod!(creativerse, "Creativerse", Engine::new(280_790), 26901);
game_query_mod!(
cscz,
"Counter Strike: Condition Zero",
Engine::new_gold_src(false),
27015
);
game_query_mod!(
csgo,
"Counter-Strike: Global Offensive",
Engine::new(730),
27015
);
game_query_mod!(css, "Counter-Strike: Source", Engine::new(240), 27015);
game_query_mod!(dab, "Double Action: Boogaloo", Engine::new(317_360), 27015);
game_query_mod!(dod, "Day of Defeat", Engine::new_gold_src(false), 27015);
game_query_mod!(dods, "Day of Defeat: Source", Engine::new(300), 27015);
game_query_mod!(doi, "Day of Infamy", Engine::new(447_820), 27015);
game_query_mod!(dst, "Don't Starve Together", Engine::new(322_320), 27016);
game_query_mod!(enshrouded, "Enshrouded", Engine::new(1_203_620), 15637);
game_query_mod!(garrysmod, "Garry's Mod", Engine::new(4000), 27016);
game_query_mod!(hl2d, "Half-Life 2 Deathmatch", Engine::new(320), 27015);
game_query_mod!(
hlds,
"Half-Life Deathmatch: Source",
Engine::new(360),
27015
);
game_query_mod!(hll, "Hell Let Loose", Engine::new(686_810), 26420);
game_query_mod!(
imic,
"Insurgency: Modern Infantry Combat",
Engine::new(17700),
27015
);
game_query_mod!(insurgency, "Insurgency", Engine::new(222_880), 27015);
game_query_mod!(
insurgencysandstorm,
"Insurgency: Sandstorm",
Engine::new(581_320),
27131
);
game_query_mod!(l4d, "Left 4 Dead", Engine::new(500), 27015);
game_query_mod!(l4d2, "Left 4 Dead 2", Engine::new(550), 27015);
game_query_mod!(
ohd,
"Operation: Harsh Doorstop",
Engine::new_with_dedicated(736_590, 950_900),
27005
);
game_query_mod!(onset, "Onset", Engine::new(1_105_810), 7776);
game_query_mod!(postscriptum, "Post Scriptum", Engine::new(736_220), 10037);
game_query_mod!(
projectzomboid,
"Project Zomboid",
Engine::new(108_600),
16261
);
game_query_mod!(risingworld, "Rising World", Engine::new(324_080), 4254);
game_query_mod!(ror2, "Risk of Rain 2", Engine::new(632_360), 27016);
game_query_mod!(rust, "Rust", Engine::new(252_490), 27015);
game_query_mod!(sco, "Sven Co-op", Engine::new_gold_src(false), 27015);
game_query_mod!(sdtd, "7 Days to Die", Engine::new(251_570), 26900);
game_query_mod!(soulmask, "Soulmask", Engine::new(2_646_460), 27015);
game_query_mod!(squad, "Squad", Engine::new(393_380), 27165);
game_query_mod!(
starbound,
"Starbound",
Engine::new(211_820),
21025,
GatheringSettings {
players: GatherToggle::Enforce,
rules: GatherToggle::Enforce,
check_app_id: false,
}
);
game_query_mod!(teamfortress2, "Team Fortress 2", Engine::new(440), 27015);
game_query_mod!(
tfc,
"Team Fortress Classic",
Engine::new_gold_src(false),
27015
);
game_query_mod!(theforest, "The Forest", Engine::new(556_450), 27016);
game_query_mod!(thefront, "The Front", Engine::new(2_285_150), 27015);
game_query_mod!(unturned, "Unturned", Engine::new(304_930), 27015);
game_query_mod!(
valheim,
"Valheim",
Engine::new(892_970),
2457,
GatheringSettings {
players: GatherToggle::Enforce,
rules: GatherToggle::Skip,
check_app_id: true,
}
);
game_query_mod!(vrising, "V Rising", Engine::new(1_604_030), 27016);
game_query_mod!(zps, "Zombie Panic: Source", Engine::new(17_500), 27015);
game_query_mod!(moe, "Myth of Empires", Engine::new(1_371_580), 12888);
game_query_mod!(mordhau, "Mordhau", Engine::new(629_760), 27015);
game_query_mod!(
pvak2,
"Pirates, Vikings, and Knights II",
Engine::new(17_570),
27015
);
game_query_mod!(nla, "Nova-Life: Amboise", Engine::new(885_570), 27015);
game_query_mod!(pixark, "PixARK", Engine::new(593_600), 27015);

491
crates/lib/src/http.rs Normal file
View file

@ -0,0 +1,491 @@
//! Client for making HTTP requests.
//!
//! This is the first draft implementation: feel free to change things to suit
//! your needs.
// Because this is first draft some functionality is not used yet.
// TODO: When this is used in more places remove this and refine the interface.
#![allow(dead_code)]
use crate::GDErrorKind::{HostLookup, InvalidInput, PacketReceive, PacketSend, ProtocolFormat};
use crate::{GDResult, TimeoutSettings};
use std::io::Read;
use std::net::{SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs};
use ureq::{Agent, AgentBuilder, Request};
use url::{Host, Url};
use serde::{de::DeserializeOwned, Serialize};
/// Max length of HTTP responses in bytes: 1GB
const MAX_RESPONSE_LENGTH: usize = 1024 * 1024 * 1024;
/// HTTP request client. Define parameters host parameters on new, then re-use
/// for each request.
///
/// When making requests directly to the server use [HttpClient::new] as this
/// allows directly specifying the IP to connect to.
///
/// When requests must go through an intermediatary (that we don't know the IP
/// of) use [HttpClient::from_url] which will perform a DNS lookup internally.
///
/// For example usage see [tests].
pub struct HttpClient {
client: Agent,
address: Url,
headers: Vec<(String, String)>,
}
/// HttpHeaders for use with a single request.
pub type HttpHeaders<'a> = Option<&'a [(&'a str, &'a str)]>;
/// HTTP Protocols.
///
/// Note: if the `tls` feature is disabled this will only contain Http.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum HttpProtocol {
#[default]
Http,
#[cfg(feature = "tls")]
Https,
}
impl HttpProtocol {
/// Convert [Protocol] to a static str for use in a [Url].
/// e.g. "http:"
pub const fn as_str(&self) -> &'static str {
use HttpProtocol::*;
match self {
Http => "http:",
#[cfg(feature = "tls")]
Https => "https:",
}
}
}
/// Additional settings for HTTPClients.
///
/// # Can be created using builder functions:
/// ```ignore, We cannot test private functionality
/// use gamedig::http::{HttpSettings, HttpProtocol};
///
/// let _ = HttpSettings::default()
/// .protocol(HttpProtocol::Http)
/// .hostname(String::from("test.com"))
/// .header(String::from("Authorization"), String::from("Bearer Token"));
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct HttpSettings<S: Into<String>> {
/// Choose whether to use HTTP or HTTPS.
pub protocol: HttpProtocol,
/// Choose a hostname override (used to set the [Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) header) and for TLS.
pub hostname: Option<S>,
/// Choose HTTP headers to send with requests.
pub headers: Vec<(S, S)>,
}
impl<S: Into<String>> HttpSettings<S> {
/// Set the HTTP protocol (defaults to HTTP).
pub const fn protocol(mut self, protocol: HttpProtocol) -> HttpSettings<S> {
self.protocol = protocol;
self
}
/// Set the desired HTTP host name: used for the HTTP Host header and for
/// TLS negotiation.
pub fn hostname(mut self, hostname: S) -> HttpSettings<S> {
self.hostname = Some(hostname);
self
}
/// Overwrite all the current HTTP headers with new headers.
pub fn headers(mut self, headers: Vec<(S, S)>) -> HttpSettings<S> {
self.headers = headers;
self
}
/// Set one HTTP header value.
pub fn header(mut self, name: S, value: S) -> HttpSettings<S> {
self.headers.push((name, value));
self
}
}
impl HttpClient {
/// Creates a new HTTPClient that can be used to send requests.
///
/// # Parameters
/// - [address](SocketAddr): The IP and port the HTTP request will connect
/// to.
/// - [timeout_settings](TimeoutSettings): Used to set the connect and
/// socket timeouts for the requests.
/// - [http_settings](HttpSettings): Additional settings for the HTTPClient.
pub fn new<S: Into<String>>(
address: &SocketAddr,
timeout_settings: &Option<TimeoutSettings>,
http_settings: HttpSettings<S>,
) -> GDResult<Self>
where
Self: Sized,
{
let mut client_builder = AgentBuilder::new();
// Set timeout settings
let (read_timeout, write_timeout) = TimeoutSettings::get_read_and_write_or_defaults(timeout_settings);
if let Some(read_timeout) = read_timeout {
client_builder = client_builder.timeout_read(read_timeout);
}
if let Some(write_timeout) = write_timeout {
client_builder = client_builder.timeout_write(write_timeout);
}
if let Some(connect_timeout) = TimeoutSettings::get_connect_or_default(timeout_settings) {
client_builder = client_builder.timeout_connect(connect_timeout);
}
// Every request sent from this client will connect to the address set
{
let address = *address;
client_builder = client_builder.resolver(move |_: &str| Ok(vec![address]));
}
// Set a friendly user-agent string
client_builder = client_builder.user_agent(concat!(
env!("CARGO_PKG_NAME"),
"/",
env!("CARGO_PKG_VERSION")
));
let client = client_builder.build();
let host = http_settings
.hostname
.map(S::into)
.unwrap_or_else(|| address.ip().to_string());
Ok(Self {
client,
// TODO: Use Url from_parts if it gets added
address: Url::parse(&format!(
"{}//{}:{}",
http_settings.protocol.as_str(),
host,
address.port()
))
.map_err(|e| InvalidInput.context(e))?,
headers: http_settings
.headers
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
})
}
/// Create a new HTTP client from a pre-existing URL, performing a DNS
/// lookup on the host when necessary.
///
/// This is aimed to be used when we know the domain of the server but not
/// the IP i.e. when the server is not the service being directly queried
/// for server info.
pub fn from_url<U: TryInto<Url>>(
url: U,
timeout_settings: &Option<TimeoutSettings>,
headers: Option<Vec<(&str, &str)>>,
) -> GDResult<Self>
where
U::Error: std::error::Error + Send + Sync + 'static,
{
let url: Url = url.try_into().map_err(|e| InvalidInput.context(e))?;
let host = url
.host()
.ok_or_else(|| InvalidInput.context("URL used to create a HTTPClient must have a host"))?;
let port = url
.port_or_known_default()
.ok_or_else(|| InvalidInput.context("URL used to create HttpClient must have a port"))?;
let address = match host {
Host::Ipv4(ip) => SocketAddr::V4(SocketAddrV4::new(ip, port)),
Host::Ipv6(ip) => SocketAddr::V6(SocketAddrV6::new(ip, port, 0, 0)),
Host::Domain(domain) => {
format!("{domain}:{port}")
.to_socket_addrs()
.map_err(|e| HostLookup.context(e))?
.next()
.ok_or_else(|| HostLookup.context("No socket addresses found for host"))?
}
};
let http_settings = HttpSettings {
hostname: url.host_str(),
protocol: match url.scheme() {
#[cfg(feature = "tls")]
"https" => HttpProtocol::Https,
_ => HttpProtocol::Http,
},
headers: headers.unwrap_or_default(),
};
Self::new(&address, timeout_settings, http_settings)
}
/// Send a HTTP GET request and return the response data as a buffer.
pub fn get(&mut self, path: &str, headers: HttpHeaders) -> GDResult<Vec<u8>> { self.request("GET", path, headers) }
/// Send a HTTP GET request and parse the JSON resonse.
pub fn get_json<T: DeserializeOwned>(&mut self, path: &str, headers: HttpHeaders) -> GDResult<T> {
self.request_json("GET", path, headers)
}
/// Send a HTTP Post request with JSON data and parse a JSON response.
pub fn post_json<T: DeserializeOwned, S: Serialize>(
&mut self,
path: &str,
headers: HttpHeaders,
data: S,
) -> GDResult<T> {
self.request_with_json_data("POST", path, headers, data)
}
/// Send a HTTP Post request with FORM data and parse a JSON response.
pub fn post_json_with_form<T: DeserializeOwned>(
&mut self,
path: &str,
headers: HttpHeaders,
data: &[(&str, &str)],
) -> GDResult<T> {
self.request_with_form_data("POST", path, headers, data)
}
// NOTE: More methods can be added here as required using the request_json or
// request_with_json methods
fn make_request(&self, method: &str, headers: HttpHeaders) -> Request {
let mut request = self.client.request_url(method, &self.address);
// Set the request headers.
for (key, value) in self.headers.iter() {
request = request.set(key, value);
}
if let Some(headers) = headers {
for (key, value) in headers {
request = request.set(key, value);
}
}
request
}
/// Internal request method, makes a request with an arbitrary HTTP method.
#[inline]
fn request(&mut self, method: &str, path: &str, headers: HttpHeaders) -> GDResult<Vec<u8>> {
// Append the path to the pre-parsed URL and create a request object.
self.address.set_path(path);
let request = self.make_request(method, headers);
// Send the request.
let http_response = request.call().map_err(|e| PacketSend.context(e))?;
let length = if let Some(length) = http_response.header("Content-Length") {
length
.parse::<usize>()
.map_err(|e| ProtocolFormat.context(e))?
.min(MAX_RESPONSE_LENGTH)
} else {
5012 // Sensible default allocation
};
let mut buffer: Vec<u8> = Vec::with_capacity(length);
let _ = http_response
.into_reader()
.take(MAX_RESPONSE_LENGTH as u64)
.read_to_end(&mut buffer)
.map_err(|e| PacketReceive.context(e))?;
Ok(buffer)
}
/// Send a HTTP request without any data and parse the JSON response.
#[inline]
fn request_json<T: DeserializeOwned>(&mut self, method: &str, path: &str, headers: HttpHeaders) -> GDResult<T> {
// Append the path to the pre-parsed URL and create a request object.
self.address.set_path(path);
let request = self.make_request(method, headers);
// Send the request and parse the response as JSON.
request
.call()
.map_err(|e| PacketSend.context(e))?
.into_json::<T>()
.map_err(|e| ProtocolFormat.context(e))
}
/// Send a HTTP request with JSON data and parse the JSON response.
#[inline]
fn request_with_json_data<T: DeserializeOwned, S: Serialize>(
&mut self,
method: &str,
path: &str,
headers: HttpHeaders,
data: S,
) -> GDResult<T> {
self.address.set_path(path);
let request = self.make_request(method, headers);
request
.send_json(data)
.map_err(|e| PacketSend.context(e))?
.into_json::<T>()
.map_err(|e| ProtocolFormat.context(e))
}
/// Send a HTTP request with FORM data and parse the JSON response.
#[inline]
fn request_with_form_data<T: DeserializeOwned>(
&mut self,
method: &str,
path: &str,
headers: HttpHeaders,
data: &[(&str, &str)],
) -> GDResult<T> {
self.address.set_path(path);
let request = self.make_request(method, headers);
request
.send_form(data)
.map_err(|e| PacketSend.context(e))?
.into_json::<T>()
.map_err(|e| ProtocolFormat.context(e))
}
}
#[cfg(test)]
mod tests {
use std::net::{Ipv4Addr, SocketAddrV4, ToSocketAddrs};
use super::*;
#[test]
fn http_settings_builder() {
const HOSTNAME: &str = "example.org";
#[cfg(feature = "tls")]
const PROTOCOL: HttpProtocol = HttpProtocol::Https;
#[cfg(not(feature = "tls"))]
const PROTOCOL: HttpProtocol = HttpProtocol::Http;
let settings = HttpSettings::default()
.hostname(HOSTNAME)
.protocol(PROTOCOL)
.header("Gamedig", "Is Awesome")
.headers(vec![("Foo", "bar")])
.header("Baz", "Buzz");
assert_eq!(settings.hostname, Some(HOSTNAME));
assert_eq!(settings.protocol, PROTOCOL);
assert_eq!(settings.headers, vec![("Foo", "bar"), ("Baz", "Buzz"),]);
}
#[test]
fn http_client_new() {
const PROTOCOL: HttpProtocol = HttpProtocol::Http;
const ADDRESS: SocketAddr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8000));
let settings = HttpSettings {
protocol: PROTOCOL,
hostname: Some("github.com"),
headers: vec![("Authorization", "UUDDLRLRBA")],
};
let client = HttpClient::new(&ADDRESS, &None, settings).unwrap();
assert_eq!(client.address.as_str(), "http://github.com:8000/");
assert_eq!(
client.headers,
vec![(String::from("Authorization"), String::from("UUDDLRLRBA")),]
);
}
#[cfg(feature = "tls")]
#[test]
#[ignore = "HTTP requests won't work without internet"]
fn https_json_get_request() {
let address = "api.github.com:443"
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
let settings = HttpSettings::default()
.protocol(HttpProtocol::Https)
.hostname("api.github.com");
let mut client = HttpClient::new(&address, &None, settings).unwrap();
let response: serde_json::Value = client.get_json("/events", None).unwrap();
println!("{:?}", response);
}
#[test]
#[ignore = "HTTP requests won't work without internet"]
fn http_json_get_request() {
let address = "postman-echo.com:80"
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
let settings = HttpSettings::default().hostname("postman-echo.com");
let mut client = HttpClient::new(&address, &None, settings).unwrap();
let response: serde_json::Value = client.get_json("/get", None).unwrap();
println!("{:?}", response);
}
#[test]
#[ignore = "HTTP requests won't work without internet"]
fn http_get_request() {
let address = "ifconfig.me:80".to_socket_addrs().unwrap().next().unwrap();
let settings = HttpSettings::default()
.hostname("ifconfig.me")
.header("User-Agent", "Curl/8.6.0");
let mut client = HttpClient::new(&address, &None, settings).unwrap();
let response = client.get("/", None).unwrap();
println!("{:?}", std::str::from_utf8(&response));
}
#[test]
#[ignore = "HTTP requests won't work without internet"]
fn http_get_from_url() {
let mut client = HttpClient::from_url("http://postman-echo.com/path-is-ignored", &None, None).unwrap();
let response: serde_json::Value = client.get_json("/get", None).unwrap();
println!("{:?}", response);
}
#[test]
#[ignore = "HTTP requests won't work without internet"]
fn http_get_from_url_parsed() {
let url = Url::parse("http://postman-echo.com/path-is-ignored").unwrap();
let mut client = HttpClient::from_url(url, &None, None).unwrap();
let response: serde_json::Value = client.get_json("/get", None).unwrap();
println!("{:?}", response);
}
}

65
crates/lib/src/lib.rs Normal file
View file

@ -0,0 +1,65 @@
//! Game Server Query Library.
//!
//! # Usage example:
//!
//! ## For a specific game
//! ```
//! use gamedig::games::teamfortress2;
//!
//! let response = teamfortress2::query(&"127.0.0.1".parse().unwrap(), None); // None is the default port (which is 27015), could also be Some(27015)
//! match response { // Result type, must check what it is...
//! Err(error) => println!("Couldn't query, error: {}", error),
//! Ok(r) => println!("{:#?}", r)
//! }
//! ```
//!
//! ## Using a game definition
//! ```
//! use gamedig::{GAMES, query};
//!
//! let game = GAMES.get("teamfortress2").unwrap(); // Get a game definition, the full list can be found in src/games/mod.rs
//! let response = query(game, &"127.0.0.1".parse().unwrap(), None); // None will use the default port
//! match response {
//! Err(error) => println!("Couldn't query, error: {}", error),
//! Ok(r) => println!("{:#?}", r.as_json()),
//! }
//! ```
//!
//! # Crate features:
//! Enabled by default: `games`, `game_defs`, `services`
//!
//! `serde` - enables serde serialization/deserialization for many gamedig types
//! using serde derive. <br>
//! `games` - include games support. <br>
//! `services` - include services support. <br>
//! `game_defs` - include game definitions for programmatic access (enabled by
//! default). <br>
//! `clap` - enable clap derivations for gamedig settings types. <br>
//! `tls` - enable TLS support for the HTTP client.
pub mod errors;
#[cfg(feature = "games")]
pub mod games;
pub mod protocols;
#[cfg(feature = "services")]
pub mod services;
mod buffer;
mod http;
mod socket;
mod utils;
#[cfg(feature = "packet_capture")]
pub mod capture;
pub use errors::*;
#[cfg(feature = "games")]
pub use games::*;
#[allow(unused_imports)]
#[cfg(feature = "games")]
pub use query::*;
#[cfg(feature = "services")]
pub use services::*;
// Re-export types needed to call games::query::query in the root
pub use protocols::types::{ExtraRequestSettings, TimeoutSettings};

View file

@ -0,0 +1,58 @@
/// The implementation.
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;
/// Generate a module containing a query function for an epic (EOS) game.
///
/// * `mod_name` - The name to be given to the game module (see ID naming
/// conventions in CONTRIBUTING.md).
/// * `pretty_name` - The full name of the game, will be used as the
/// documentation for the created module.
/// * `steam_app`, `default_port` - Passed through to [game_query_fn].
#[cfg(feature = "games")]
macro_rules! game_query_mod {
($mod_name: ident, $pretty_name: expr, $default_port: literal, $credentials: expr) => {
#[doc = $pretty_name]
pub mod $mod_name {
use crate::protocols::epic::Credentials;
crate::protocols::epic::game_query_fn!($pretty_name, $default_port, $credentials);
}
};
}
#[cfg(feature = "games")]
pub(crate) use game_query_mod;
/// Generate a query function for an epic (EOS) game.
///
/// * `default_port` - The default port the game uses.
/// * `credentials` - Credentials to access EOS.
#[cfg(feature = "games")]
macro_rules! game_query_fn {
($pretty_name: expr, $default_port: literal, $credentials: expr) => {
crate::protocols::epic::game_query_fn! {@gen $default_port, concat!(
"Make a Epic query for ", $pretty_name, ".\n\n",
"If port is `None`, then the default port (", stringify!($default_port), ") will be used."), $credentials}
};
(@gen $default_port: literal, $doc: expr, $credentials: expr) => {
#[doc = $doc]
pub fn query(
address: &std::net::IpAddr,
port: Option<u16>,
) -> crate::GDResult<crate::protocols::epic::Response> {
crate::protocols::epic::query(
$credentials,
&std::net::SocketAddr::new(*address, port.unwrap_or($default_port)),
)
}
};
}
#[cfg(feature = "games")]
pub(crate) use game_query_fn;

View file

@ -0,0 +1,183 @@
use crate::http::HttpClient;
use crate::protocols::epic::Response;
use crate::GDErrorKind::{JsonParse, PacketBad};
use crate::{GDResult, TimeoutSettings};
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use serde::Deserialize;
#[cfg(feature = "serde")]
use serde::Serialize;
use serde_json::Value;
use std::net::SocketAddr;
const EPIC_API_ENDPOINT: &str = "https://api.epicgames.dev";
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Credentials {
#[cfg_attr(feature = "serde", serde(skip_deserializing, skip_serializing))]
pub deployment: &'static str,
#[cfg_attr(feature = "serde", serde(skip_deserializing, skip_serializing))]
pub id: &'static str,
#[cfg_attr(feature = "serde", serde(skip_deserializing, skip_serializing))]
pub secret: &'static str,
pub auth_by_external: bool,
}
pub struct EpicProtocol {
client: HttpClient,
credentials: Credentials,
}
#[derive(Deserialize)]
struct ClientTokenResponse {
access_token: String,
}
#[derive(Deserialize)]
struct QueryResponse {
sessions: Value,
}
macro_rules! extract_optional_field {
($value:expr, $fields:expr, $map_func:expr) => {
$fields
.iter()
.fold(Some(&$value), |acc, &key| acc.and_then(|val| val.get(key)))
.map($map_func)
.flatten()
};
}
macro_rules! extract_field {
($value:expr, $fields:expr, $map_func:expr) => {
extract_optional_field!($value, $fields, $map_func)
.ok_or(PacketBad.context("Field is missing or is not parsable."))?
};
}
impl EpicProtocol {
pub fn new(credentials: Credentials, timeout_settings: TimeoutSettings) -> GDResult<Self> {
Ok(Self {
client: HttpClient::from_url(EPIC_API_ENDPOINT, &Some(timeout_settings), None)?,
credentials,
})
}
pub fn auth_by_external(&self) -> GDResult<String> { Ok(String::new()) }
pub fn auth_by_client(&mut self) -> GDResult<String> {
let body = [
("grant_type", "client_credentials"),
("deployment_id", self.credentials.deployment),
];
let auth_format = format!("{}:{}", self.credentials.id, self.credentials.secret);
let auth_base = BASE64_STANDARD.encode(auth_format);
let auth = format!("Basic {}", auth_base.as_str());
let authorization = auth.as_str();
let headers = [
("Authorization", authorization),
("Content-Type", "application/x-www-form-urlencoded"),
];
let response =
self.client
.post_json_with_form::<ClientTokenResponse>("/auth/v1/oauth/token", Some(&headers), &body)?;
Ok(response.access_token)
}
pub fn query_raw(&mut self, address: &SocketAddr) -> GDResult<Value> {
let port = address.port();
let address = address.ip().to_string();
let body = format!(
"{{\"criteria\":[{{\"key\":\"attributes.ADDRESS_s\",\"op\":\"EQUAL\",\"value\":\"{}\"}}]}}",
address
);
let body = serde_json::from_str::<Value>(body.as_str()).map_err(|e| JsonParse.context(e))?;
let token = if self.credentials.auth_by_external {
self.auth_by_external()?
} else {
self.auth_by_client()?
};
let authorization = format!("Bearer {}", token);
let headers = [
("Content-Type", "application/json"),
("Accept", "application/json"),
("Authorization", authorization.as_str()),
];
let url = format!("/matchmaking/v1/{}/filter", self.credentials.deployment);
let response: QueryResponse = self.client.post_json(url.as_str(), Some(&headers), body)?;
if let Value::Array(sessions) = response.sessions {
if sessions.is_empty() {
return Err(PacketBad.context("No servers provided."));
}
for session in sessions.into_iter() {
let attributes = session
.get("attributes")
.ok_or(PacketBad.context("Expected attributes field missing in sessions."))?;
let address_match = attributes
.get("ADDRESSBOUND_s")
.and_then(Value::as_str)
.map_or(false, |v| v == address || v == format!("0.0.0.0:{}", port))
|| attributes
.get("GAMESERVER_PORT_1")
.and_then(Value::as_u64)
.map_or(false, |v| v == port as u64);
if address_match {
return Ok(session);
}
}
return Err(
PacketBad.context("Servers were provided but the specified one couldn't be found amongst them.")
);
}
Err(PacketBad.context("Expected session field to be an array."))
}
pub fn query(&mut self, address: &SocketAddr) -> GDResult<Response> {
let value = self.query_raw(address)?;
let build_version = extract_optional_field!(value, ["attributes", "BUILDID_s"], Value::as_str);
let minor_version = extract_optional_field!(value, ["attributes", "MINORBUILDID_s"], Value::as_str);
let game_version = match (build_version, minor_version) {
(Some(b), Some(m)) => Some(format!("{b}.{m}")),
_ => None,
};
Ok(Response {
name: extract_field!(value, ["attributes", "CUSTOMSERVERNAME_s"], Value::as_str).to_string(),
map: extract_field!(value, ["attributes", "MAPNAME_s"], Value::as_str).to_string(),
has_password: extract_field!(value, ["attributes", "SERVERPASSWORD_b"], Value::as_bool),
players_online: extract_field!(value, ["totalPlayers"], Value::as_u64) as u32,
players_maxmimum: extract_field!(value, ["settings", "maxPublicPlayers"], Value::as_u64) as u32,
players: vec![],
game_version,
raw: value,
})
}
}
pub fn query(credentials: Credentials, address: &SocketAddr) -> GDResult<Response> {
query_with_timeout(credentials, address, None)
}
pub fn query_with_timeout(
credentials: Credentials,
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let mut client = EpicProtocol::new(credentials, timeout_settings.unwrap_or_default())?;
client.query(address)
}

Some files were not shown because too many files have changed in this diff Show more