├── .actrc
├── .github
├── .act-event.json
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── feature_request.md
│ └── help-regarding-code-protocol-errors.md
├── badges
│ ├── .gitkeep
│ └── node.svg
├── dependabot.yml
├── labeler.yml
└── workflows
│ ├── audit.yml
│ ├── ci.yml
│ ├── labeler.yml
│ ├── node-badge.yml
│ └── scripts
│ └── node-badge.mjs
├── .gitignore
├── .pre-commit-config.yaml
├── .rustfmt.toml
├── CONTRIBUTING.md
├── Cargo.toml
├── GAMES.md
├── LICENSE.md
├── PROTOCOLS.md
├── README.md
├── RESPONSES.md
├── SERVICES.md
├── VERSIONS.md
└── crates
├── cli
├── .cargo
│ └── config.toml
├── CHANGELOG.md
├── Cargo.toml
├── LICENSE.md
├── README.md
└── src
│ ├── error.rs
│ └── main.rs
├── id-tests
├── Cargo.toml
└── src
│ ├── lib.rs
│ ├── main.rs
│ └── utils.rs
└── lib
├── CHANGELOG.md
├── Cargo.toml
├── README.md
├── examples
├── generic.rs
├── minecraft.rs
├── teamfortress2.rs
├── test_eco.rs
├── valve_master_server_query.rs
└── valve_protocol_query.rs
├── src
├── buffer.rs
├── capture
│ ├── mod.rs
│ ├── packet.rs
│ ├── pcap.rs
│ ├── socket.rs
│ └── writer.rs
├── errors
│ ├── error.rs
│ ├── kind.rs
│ ├── mod.rs
│ └── result.rs
├── games
│ ├── battalion1944.rs
│ ├── definitions.rs
│ ├── eco
│ │ ├── mod.rs
│ │ ├── protocol.rs
│ │ └── types.rs
│ ├── epic.rs
│ ├── ffow
│ │ ├── mod.rs
│ │ ├── protocol.rs
│ │ └── types.rs
│ ├── gamespy.rs
│ ├── jc2m
│ │ ├── mod.rs
│ │ ├── protocol.rs
│ │ └── types.rs
│ ├── mindustry
│ │ ├── mod.rs
│ │ ├── protocol.rs
│ │ └── types.rs
│ ├── minecraft
│ │ ├── mod.rs
│ │ ├── protocol
│ │ │ ├── bedrock.rs
│ │ │ ├── java.rs
│ │ │ ├── legacy_v1_4.rs
│ │ │ ├── legacy_v1_6.rs
│ │ │ ├── legacy_vb1_8.rs
│ │ │ └── mod.rs
│ │ └── types.rs
│ ├── minetest
│ │ ├── mod.rs
│ │ ├── protocol.rs
│ │ └── types.rs
│ ├── mod.rs
│ ├── quake.rs
│ ├── query.rs
│ ├── savage2
│ │ ├── mod.rs
│ │ ├── protocol.rs
│ │ └── types.rs
│ ├── theship
│ │ ├── mod.rs
│ │ ├── protocol.rs
│ │ └── types.rs
│ ├── types.rs
│ ├── unreal2.rs
│ └── valve.rs
├── http.rs
├── lib.rs
├── protocols
│ ├── epic
│ │ ├── mod.rs
│ │ ├── protocol.rs
│ │ └── types.rs
│ ├── gamespy
│ │ ├── common.rs
│ │ ├── mod.rs
│ │ └── protocols
│ │ │ ├── mod.rs
│ │ │ ├── one
│ │ │ ├── mod.rs
│ │ │ ├── protocol.rs
│ │ │ └── types.rs
│ │ │ ├── three
│ │ │ ├── mod.rs
│ │ │ ├── protocol.rs
│ │ │ └── types.rs
│ │ │ └── two
│ │ │ ├── mod.rs
│ │ │ ├── protocol.rs
│ │ │ └── types.rs
│ ├── mod.rs
│ ├── quake
│ │ ├── client.rs
│ │ ├── mod.rs
│ │ ├── one.rs
│ │ ├── three.rs
│ │ ├── two.rs
│ │ └── types.rs
│ ├── types.rs
│ ├── unreal2
│ │ ├── mod.rs
│ │ ├── protocol.rs
│ │ └── types.rs
│ └── valve
│ │ ├── mod.rs
│ │ ├── protocol.rs
│ │ └── types.rs
├── services
│ ├── minetest_master_server
│ │ ├── mod.rs
│ │ ├── service.rs
│ │ └── types.rs
│ ├── mod.rs
│ └── valve_master_server
│ │ ├── mod.rs
│ │ ├── service.rs
│ │ └── types.rs
├── socket.rs
└── utils.rs
└── tests
└── game_ids.rs
/.actrc:
--------------------------------------------------------------------------------
1 | # Configuration file for act (run github actions locally using docker)
2 | # https://github.com/nektos/act
3 |
4 | # Swap docker image for the one containing the rust toolchain
5 | -P ubuntu-latest=ghcr.io/catthehacker/ubuntu:rust-latest
6 |
7 | # Load custom event
8 | -e .github/.act-event.json
9 |
--------------------------------------------------------------------------------
/.github/.act-event.json:
--------------------------------------------------------------------------------
1 | {
2 | "act": true,
3 | "repository": {
4 | "default_branch": "main"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report for a found bug
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **Steps To Reproduce**
14 | Please provide the steps to reproduce the behavior (if not possible, describe as many details as possible).
15 |
16 | **Expected behavior**
17 | A clear and concise description of what you expected to happen.
18 |
19 | **Screenshots or Data**
20 | If applicable, add screenshots/data to help explain your problem.
21 |
22 | **Additional context**
23 | Add any other context about the problem here.
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest a feature
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **What is this feature about?**
11 | Shortly explain what your requested feature is about.
12 |
13 | **Additional context/references**
14 | Add any other context or references about the feature request here.
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/help-regarding-code-protocol-errors.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Help regarding code/protocol errors
3 | about: Use this if you can't figure out how to use something.
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **This issue shall be made only if you have already gone through the docs, have you done it?**
11 | Please state if there is something confusing regarding the docs (eg. location or wording).
12 |
13 | **What's you problem?**
14 | State as concise as possible what you want to do and can't do.
15 |
16 | **Suggestions to make this clearer**
17 | Mention how could stuff be improved so that someone doesn't have the same problem as you (eg. Error should give more information).
18 |
--------------------------------------------------------------------------------
/.github/badges/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamedig/rust-gamedig/350f29719378dd2d25aeb8a2c341ba5255010d3e/.github/badges/.gitkeep
--------------------------------------------------------------------------------
/.github/badges/node.svg:
--------------------------------------------------------------------------------
1 |
2 | Node game coverage: 23.73%
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Node game coverage
15 | Node game coverage
16 | 23.73%
17 | 23.73%
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/.github/workflows"
5 | schedule:
6 | interval: "daily"
7 |
8 | - package-ecosystem: "cargo"
9 | directory: "/crates/cli"
10 | schedule:
11 | interval: "daily"
12 |
13 | - package-ecosystem: "cargo"
14 | directory: "/crates/lib"
15 | schedule:
16 | interval: "daily"
17 |
--------------------------------------------------------------------------------
/.github/labeler.yml:
--------------------------------------------------------------------------------
1 | ci:
2 | - .github/workflows/**
3 | - .github/labeler.yml
4 | - .actrc
5 | - .pre-commit-config.yaml
6 |
7 | protocol:
8 | - crates/lib/src/protocols/**
9 |
10 | game:
11 | - crates/lib/src/games/**
12 |
13 | cli:
14 | - crates/cli/**
15 |
16 | crate:
17 | - Cargo.toml
18 | - Cargo.lock
19 |
--------------------------------------------------------------------------------
/.github/workflows/audit.yml:
--------------------------------------------------------------------------------
1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/softprops/github-actions-schemas/master/workflow.json
2 | name: Security audit
3 | on:
4 | push:
5 | paths:
6 | - "**/Cargo.toml"
7 | - "**/Cargo.lock"
8 | jobs:
9 | security_audit:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 |
14 | - name: Generate Cargo.lock # https://github.com/rustsec/audit-check/issues/27
15 | run: cargo generate-lockfile
16 |
17 | - name: Audit Check
18 | uses: rustsec/audit-check@v2.0.0
19 | with:
20 | token: ${{ secrets.GITHUB_TOKEN }}
21 |
--------------------------------------------------------------------------------
/.github/workflows/labeler.yml:
--------------------------------------------------------------------------------
1 | name: "Pull Request Labeler"
2 | on:
3 | - pull_request_target
4 |
5 | jobs:
6 | triage:
7 | permissions:
8 | contents: read
9 | pull-requests: write
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: actions/labeler@v4
14 | with:
15 | repo-token: "${{ secrets.GITHUB_TOKEN }}"
16 | dot: true
17 |
--------------------------------------------------------------------------------
/.github/workflows/node-badge.yml:
--------------------------------------------------------------------------------
1 | # Based on: https://github.com/emibcn/badge-action/blob/master/.github/workflows/test.yml
2 | name: "Generate node comparison badge"
3 |
4 | on:
5 | push:
6 | paths:
7 | - "crates/lib/src/games/definitions.rs"
8 | - ".github/workflows/node-badge.yml"
9 | - ".github/workflows/scripts/node-badge.mjs"
10 | branches:
11 | - "main" # Limit badge commits to only happen on the main branch
12 | 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.
13 | - cron: "34 3 * * 2" # Update once a week in case node-gamedig has changed
14 | workflow_dispatch:
15 |
16 | jobs:
17 | badge:
18 | runs-on: "ubuntu-latest"
19 | name: Create node comparison badge
20 | env:
21 | BADGE_PATH: ".github/badges/node.svg"
22 | steps:
23 | - name: Extract branch name
24 | shell: bash
25 | run: echo "branch=${GITHUB_REF#refs/heads/}" >> "${GITHUB_OUTPUT}"
26 | id: extract_branch
27 |
28 | - uses: actions/checkout@v4
29 | - uses: actions/checkout@v4
30 | with:
31 | repository: "gamedig/node-gamedig"
32 | path: "node-gamedig"
33 | sparse-checkout: |
34 | lib/games.js
35 | package.json
36 |
37 | - name: Calculate comparison
38 | id: comparison
39 | run: node .github/workflows/scripts/node-badge.mjs
40 |
41 | - name: Generate the badge SVG image
42 | uses: emibcn/badge-action@v2.0.3
43 | id: badge
44 | with:
45 | label: "Node game coverage"
46 | status: "${{ steps.comparison.outputs.percent }}%"
47 | color: "0f80c1"
48 | path: ${{ env.BADGE_PATH }}
49 |
50 | - name: "Commit badge"
51 | continue-on-error: true
52 | run: |
53 | git config --local user.email "action@github.com"
54 | git config --local user.name "GitHub Action"
55 | git add "${BADGE_PATH}"
56 | git commit -m "Add/Update badge"
57 |
58 | - name: Push badge commit
59 | uses: ad-m/github-push-action@master
60 | if: ${{ success() }}
61 | with:
62 | github_token: ${{ secrets.GITHUB_TOKEN }}
63 | branch: ${{ steps.extract_branch.outputs.branch }}
64 |
--------------------------------------------------------------------------------
/.github/workflows/scripts/node-badge.mjs:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | //! Calculate the percentage of games from node that we support
4 | // Expects node-gamedig checkout out in git root /node-gamedig
5 | // Expects the generic example to output a list of game IDs when no arguments are provided
6 |
7 | import process from "node:process";
8 | import { closeSync, openSync, writeSync } from "node:fs";
9 | import { spawnSync } from "node:child_process";
10 |
11 | const setOutput = (key, value) => {
12 | const file = openSync(process.env.GITHUB_OUTPUT, "a");
13 | writeSync(file, `${key}=${value}\n`);
14 | closeSync(file);
15 | };
16 |
17 | // Get node IDs
18 | // NOTE: Here we directly import from games to avoid loading
19 | // unecessary parts of the library that would require us
20 | // to install dependencies.
21 | import { games } from "../../../node-gamedig/lib/games.js";
22 |
23 | const node_ids = new Set(Object.keys(games));
24 | const node_total = node_ids.size;
25 |
26 | // Get rust IDs
27 |
28 | const command = spawnSync("cargo", [
29 | "run",
30 | "-p",
31 | "gamedig",
32 | "--example",
33 | "generic",
34 | ]);
35 |
36 | if (command.status !== 0) {
37 | console.error(command.stderr.toString("utf8"));
38 | process.exit(1);
39 | }
40 |
41 | const rust_ids_pretty = command.stdout.toString("utf8");
42 | const rust_ids = new Set(
43 | rust_ids_pretty
44 | .split("\n")
45 | .map((line) => line.split("\t")[0])
46 | .filter((id) => id.length > 0)
47 | );
48 |
49 | // Detect missing node IDs
50 |
51 | for (const id of rust_ids) {
52 | if (node_ids.delete(id)) {
53 | rust_ids.delete(id);
54 | }
55 | }
56 |
57 | console.log("Node remains", node_ids);
58 | console.log("Rust remains", rust_ids);
59 |
60 | const percent = 1 - node_ids.size / node_total;
61 |
62 | // Output percent to 2 decimal places
63 | setOutput("percent", Math.round(percent * 10000) / 100);
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
7 | Cargo.lock
8 |
9 | # These are backup files generated by rustfmt
10 | **/*.rs.bk
11 |
12 | # Others
13 | .idea/
14 | .venv/
15 | .vscode/
16 |
17 | test_everything.py
18 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | # See https://pre-commit.com for more information
2 | # See https://pre-commit.com/hooks.html for more hooks
3 | repos:
4 | - repo: local
5 | hooks:
6 | - id: clippy
7 | name: Check clippy
8 | language: system
9 | files: '([.]rs|Cargo\.toml)$'
10 | pass_filenames: false
11 | entry: rustup run --install nightly-2025-04-19 cargo-clippy -- --workspace --all-features -- -D warnings
12 |
13 | - id: build-no-features
14 | name: Check crate build with no features
15 | language: system
16 | files: '([.]rs|Cargo\.toml)$'
17 | pass_filenames: false
18 | entry: cargo check --workspace --no-default-features
19 |
20 | - id: build-all-features
21 | name: Check crate builds with all features
22 | language: system
23 | files: '([.]rs|Cargo\.toml)$'
24 | pass_filenames: false
25 | entry: cargo check --workspace --all-features --lib --bins --examples
26 |
27 | - id: test
28 | name: Check tests pass
29 | language: system
30 | files: '([.]rs|Cargo\.toml)$'
31 | pass_filenames: false
32 | entry: cargo test --workspace --bins --lib --examples --tests --all-features
33 |
34 | - id: format
35 | name: Check rustfmt
36 | language: system
37 | files: '([.]rs|Cargo\.toml)$'
38 | pass_filenames: false
39 | entry: rustup run --install nightly-2025-04-19 cargo-fmt --check
40 |
41 | - id: msrv
42 | name: Check MSRV compiles (lib only)
43 | language: system
44 | files: '([.]rs|Cargo\.toml)$'
45 | pass_filenames: false
46 | entry: rustup run --install 1.81.0 cargo check -p gamedig
47 |
48 | - id: docs
49 | name: Check rustdoc compiles
50 | language: system
51 | files: '([.]rs|Cargo\.toml)$'
52 | pass_filenames: false
53 | entry: env RUSTDOCFLAGS="-D warnings" cargo doc
54 |
55 | - id: actions
56 | name: Check actions work
57 | language: system
58 | files: '^[.]github/workflows/'
59 | pass_filenames: false
60 | entry: act --rm
61 |
--------------------------------------------------------------------------------
/.rustfmt.toml:
--------------------------------------------------------------------------------
1 | attr_fn_like_width = 70
2 | array_width = 60
3 | binop_separator = "Front"
4 | blank_lines_lower_bound = 0
5 | blank_lines_upper_bound = 1
6 | brace_style = "PreferSameLine"
7 | chain_width = 60
8 | color = "Auto"
9 | combine_control_expr = false
10 | comment_width = 80
11 | condense_wildcard_suffixes = true
12 | control_brace_style = "AlwaysSameLine"
13 | disable_all_formatting = false
14 | doc_comment_code_block_width = 100
15 | edition = "2021"
16 | emit_mode = "Files"
17 | empty_item_single_line = true
18 | error_on_line_overflow = false
19 | error_on_unformatted = false
20 | fn_call_width = 60
21 | fn_params_layout = "Tall"
22 | fn_single_line = true
23 | force_explicit_abi = true
24 | force_multiline_blocks = true
25 | format_generated_files = true
26 | format_macro_bodies = true
27 | format_strings = true
28 | group_imports = "Preserve"
29 | hard_tabs = false
30 | show_parse_errors = true
31 | hex_literal_case = "Preserve"
32 | ignore = []
33 | indent_style = "Block"
34 | imports_granularity = "Preserve"
35 | imports_indent = "Block"
36 | imports_layout = "HorizontalVertical"
37 | inline_attribute_width = 0
38 | make_backup = false
39 | match_arm_blocks = true
40 | match_arm_leading_pipes = "Never"
41 | match_block_trailing_comma = false
42 | max_width = 120
43 | merge_derives = true
44 | newline_style = "Auto"
45 | normalize_comments = true
46 | normalize_doc_attributes = false
47 | overflow_delimited_expr = false
48 | reorder_impl_items = false
49 | reorder_imports = true
50 | reorder_modules = true
51 | required_version = "1.8.0"
52 | short_array_element_width_threshold = 10
53 | single_line_if_else_max_width = 50
54 | skip_children = false
55 | space_after_colon = true
56 | space_before_colon = false
57 | spaces_around_ranges = true
58 | struct_field_align_threshold = 0
59 | struct_lit_single_line = true
60 | struct_lit_width = 18
61 | struct_variant_width = 35
62 | tab_spaces = 4
63 | trailing_comma = "Vertical"
64 | trailing_semicolon = true
65 | type_punctuation_density = "Wide"
66 | unstable_features = false
67 | use_field_init_shorthand = false
68 | use_small_heuristics = "Default"
69 | use_try_shorthand = true
70 | style_edition = "2021"
71 | where_single_line = true
72 | wrap_comments = true
73 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to rust-GameDig
2 |
3 | This project is very open to new suggestions, additions and/or changes, these
4 | can come in the form of *discussions* about the project's state, *proposing a
5 | new feature*, *holding a few points on why we shall do X breaking change* or
6 | *submitting a fix*.
7 |
8 | ## Communications
9 |
10 | GitHub is the place we use to track bugs and discuss new features/changes,
11 | although we have a [Discord](https://discord.gg/NVCMn3tnxH) server for the
12 | community, all bugs, suggestions and changes will be reported on GitHub
13 | alongside with their backing points to ensure the transparency of the project's
14 | development.
15 |
16 | ## Issues
17 |
18 | Before opening an issue, check if there is an existing relevant issue first,
19 | someone might just have had your issue already, or you might find something
20 | related that could be of help.
21 |
22 | When opening a new issue, make sure to fill the issue template. They are made
23 | to make the subject to be as understandable as possible, not doing so may result
24 | in your issue not being managed right away, if you don't understand something
25 | (be it regarding your own problem/the issue template/the library), please state
26 | so.
27 |
28 | ## Development
29 |
30 | Note before contributing that everything done here is under the [MIT](https://opensource.org/license/mit/) license.
31 |
32 | ### Naming
33 |
34 | Naming is an important matter, and it shouldn't be changed unless necessary.
35 |
36 | Game **names** should be added as they appear on steam (or other storefront
37 | if not listed there) with the release year appended in brackets (except when the
38 | release year is already part of the name).
39 | If there is a mod that needs to be added (or it adds the support for server
40 | queries for the game), its name should be composed of the game name, a separating
41 | **bracket**, the mod name and the release year as specified previously
42 | (e.g. `Grand Theft Auto V - FiveM (2013)`).
43 |
44 | A game's **identification** is a lowercase alphanumeric string will and be forged
45 | following these rules:
46 |
47 | 1. Names composed of a maximum of two words (unless #4 applies) will result in an
48 | id where the words are concatenated (`Dead Cells` -> `deadcells`), acronyms in
49 | the name count as a single word (`S.T.A.L.K.E.R.` -> `stalker`).
50 | 2. Names of more than two words shall be made into an acronym made of the
51 | initial
52 | letters (`The Binding of Isaac` -> `tboi`), [hypenation composed words](https://prowritingaid.com/hyphenated-words)
53 | don't count as a single word, but of how many parts they are made of
54 | (`Dino D-Day`, 3 words, so `ddd`).
55 | 3. If a game has the exact name as a previously existing id's game
56 | (`Star Wars Battlefront 2`, the 2005 and 2017 one), append the release year to
57 | the newer id (2005 would be `swb2` (suppose we already have this one supported)
58 | and 2017 would be `swb22017`).
59 | 4. If a new id (`Day of Dragons` -> `dod`) results in an id that already exists
60 | (`Day of Defeat` -> `dod`), then the new name should ignore rule #2
61 | (`Day of Dragons` -> `dayofdragons`).
62 | 5. Roman numbering will be converted to arabic numbering (`XIV` -> `14`).
63 | 6. Unless numbers (years included) are at the end of a name, they will be considered
64 | words. If a number is not in the first position, its entire numeric digits will be
65 | used instead of the acronym of that number's digits (`Left 4 Dead` -> `l4d`). If the
66 | number is in the first position the longhand (words: 5 -> five) representation of the
67 | number will be used to create an acronym (`7 Days to Die` -> `sdtd`). Other examples:
68 | `Team Fortress 2` -> `teamfortress2`, `Unreal Tournament 2003` ->
69 | `unrealtournament2003`.
70 | 7. If a game supports multiple protocols, multiple entries will be done for said game
71 | where the edition/protocol name (first disposable in this order) will be appended to
72 | the base game id's: `` (where the protocol id will follow all
73 | rules except #2) (Minecraft is mainly divided by 2 editions, Java and Bedrock
74 | which will be `minecraftjava` and `minecraftbedrock` respectively, but it also has
75 | legacy versions, which use another protocol, an example would be the one for `1.6`,
76 | so the name would be `Legacy 1.6` which its id will be `legacy16`, resulting in the
77 | entry of `minecraftlegacy16`). One more entry can be added by the base name of the
78 | game, which queries in a group said supported protocols to make generic queries
79 | easier and disposable.
80 | 8. If its actually about a mod that adds the ability for queries to be performed,
81 | process only the mod name.
82 |
83 | ### Making commits
84 |
85 | Where possible please format commits as complete atomic changes that don't rely on
86 | any future commits. Also make sure that the commit message is as descriptive as
87 | possible.
88 |
89 | To avoid CI failing when you make a PR you can use our pre-commit hooks: tests that
90 | run before you are able to make a commit (you can skip this at any time by adding
91 | the `-n` flag to `git commit`).
92 |
93 | To set this up you need the following programs installed
94 |
95 | - [pre-commit](https://pre-commit.com/)
96 | - [rustup](https://rustup.rs/)
97 | - [act](https://github.com/nektos/act) (If you want to test changes to github actions workflows)
98 |
99 | Once these are installed you can enable the pre-commit hook by running the following in
100 | the root directory of the repository.
101 |
102 | ```shell
103 | $ pre-commit install
104 | ```
105 |
106 | ### Priorities
107 |
108 | Game suggestions will be prioritized by maintainers based on whether the game
109 | uses a protocol already implemented in the library (games that use already
110 | implemented protocols will be added first), except in the case where a
111 | contribution is made with the protocol needed to implement the game.
112 |
113 | The same goes for protocols, if 2 were to be requested, the one implemented in
114 | the most games will be prioritized.
115 |
116 | ### Releases
117 |
118 | Currently, there is no release schedule.
119 | Releases are made when the team decides one will be fitting to be done.
120 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = ["crates/cli", "crates/lib", "crates/id-tests"]
3 |
4 | # Edition 2021, uses resolver = 2
5 | resolver = "2"
6 |
7 | [profile.release]
8 | opt-level = 3
9 | debug = false
10 | rpath = true
11 | lto = 'fat'
12 | codegen-units = 1
13 |
14 | [profile.release.package."*"]
15 | opt-level = 3
16 |
17 | # When building locally, use the local version of the library
18 | # Comment this out when you want to resolve the library from crates.io
19 | # This is only for crates that use gamedig as a dependency
20 | # https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html
21 | [patch.crates-io]
22 | gamedig = { path = "./crates/lib" }
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 - 2025 GameDig Organization & Contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/PROTOCOLS.md:
--------------------------------------------------------------------------------
1 | A protocol is defined as proprietary if it is being used only for a single scope (or series, like Minecraft).
2 |
3 | # Supported protocols:
4 |
5 | | Name | For | Proprietary? | Documentation reference | Notes |
6 | |---------------------------|-------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
7 | | 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. |
8 | | Minecraft | Games | Yes | Java: [List Server Protocol](https://wiki.vg/Server_List_Ping) Bedrock: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/minecraftbedrock.js) | |
9 | | 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. |
10 | | 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) | |
11 | | Just Cause 2: Multiplayer | Games | Yes | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/jc2mp.js) |
12 | | 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. |
13 | | Savage 2 | Games | Yes | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/savage2.js) | |
14 | | Epic | Games | No | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/epic.js) | Available only on the 'tls' feature. |
15 |
16 | ## Planned to add support:
17 |
18 | _
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | rust-GameDig
2 |
3 | The fast library for querying game servers/services.
4 |
5 |
22 |
23 |
24 | This library brings what
25 |
26 | node-GameDig
27 |
28 | does (and not only), to pure Rust!
29 |
30 |
31 | **Warning**: This project goes through frequent API breaking changes and hasn't been thoroughly tested.
32 |
33 | ## Community
34 |
35 | Checkout the GameDig Community Discord Server [here](https://discord.gg/NVCMn3tnxH).
36 | Note that it isn't be a replacement for GitHub issues, if you have found a problem
37 | within the library or want to request a feature, it's better to do so here rather than
38 | on Discord.
39 |
40 | ## Usage
41 |
42 | Minimum Supported Rust Version is `1.81.0` and the code is cross-platform.
43 |
44 | Pick a game/service/protocol (check the [GAMES](GAMES.md), [SERVICES](SERVICES.md) and [PROTOCOLS](PROTOCOLS.md) files
45 | to see the currently supported ones), provide the ip and the port (be aware that some game servers use a separate port
46 | for the info queries, the port can also be optional if the server is running the default ports) then query on it.
47 |
48 | [Team Fortress 2](https://store.steampowered.com/app/440/Team_Fortress_2/) query example:
49 |
50 | ```rust
51 | use gamedig::games::teamfortress2;
52 |
53 | fn main() {
54 | let response = teamfortress2::query(&"127.0.0.1".parse().unwrap(), None);
55 | // None is the default port (which is 27015), could also be Some(27015)
56 |
57 | match response { // Result type, must check what it is...
58 | Err(error) => println!("Couldn't query, error: {}", error),
59 | Ok(r) => println!("{:#?}", r)
60 | }
61 | }
62 | ```
63 |
64 | Response (note that some games have a different structure):
65 |
66 | ```json5
67 | {
68 | protocol: 17,
69 | name: "Team Fortress 2 Dedicated Server.",
70 | map: "ctf_turbine",
71 | game: "tf2",
72 | appid: 440,
73 | players_online: 0,
74 | players_details: [],
75 | players_maximum: 69,
76 | players_bots: 0,
77 | server_type: Dedicated,
78 | has_password: false,
79 | vac_secured: true,
80 | version: "7638371",
81 | port: Some(27015),
82 | steam_id: Some(69753253289735296),
83 | tv_port: None,
84 | tv_name: None,
85 | keywords: Some(
86 | "alltalk,nocrits"
87 | ),
88 | rules: [
89 | "mp_autoteambalance"
90 | :
91 | "1",
92 | "mp_maxrounds"
93 | :
94 | "5",
95 | //....
96 | ]
97 | }
98 | ```
99 |
100 | Want to see more examples? Checkout the [examples](crates/lib/examples) folder.
101 |
102 | ## Command Line Interface
103 |
104 | The library also has an [official CLI](https://crates.io/crates/gamedig_cli) that you can use, it has
105 | MSRV of `1.81.0`.
106 |
107 | ## Documentation
108 |
109 | The documentation is available at [docs.rs](https://docs.rs/gamedig/latest/gamedig/).
110 | Curious about the history and what changed between versions?
111 | Everything is in the changelogs file: [lib](crates/lib/CHANGELOG.md) and [cli](crates/lib/CHANGELOG.md).
112 |
113 | ## Contributing
114 |
115 | If you want to see your favorite game/service being supported here, open an issue, and I'll prioritize it (or do a pull
116 | request if you want to implement it yourself)!
117 |
118 | Before contributing please read [CONTRIBUTING](CONTRIBUTING.md).
119 |
--------------------------------------------------------------------------------
/SERVICES.md:
--------------------------------------------------------------------------------
1 | # Supported services:
2 |
3 | | Name | Documentation reference |
4 | |------------------------|-------------------------------------------------------------------------------------------------------|
5 | | Valve Master Server | [Master Server Query Protocol](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol) |
6 | | MineTest Master Server | [Node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/minetest.js) |
7 |
8 | ## Planned to add support:
9 |
10 | TeamSpeak
11 |
--------------------------------------------------------------------------------
/VERSIONS.md:
--------------------------------------------------------------------------------
1 | # MSRV (Minimum Supported Rust Version)
2 |
3 | Current: `1.81.0`
4 |
5 | Places to update:
6 |
7 | - `Cargo.toml`
8 | - `README.md`
9 | - `.github/workflows/ci.yml`
10 | - `.pre-commit-config.yaml`
11 |
12 | # rustfmt version
13 |
14 | Current: `1.8.0`
15 |
16 | Places to update:
17 |
18 | - `.rustfmt.toml`
19 | - The nightly rust version
20 |
21 | # The nightly rust version
22 |
23 | The toolchain version used to run rustfmt in CI
24 |
25 | Current: `nightly-2025-04-19`
26 |
27 | Places to update:
28 |
29 | - `.github/workflows/ci.yml`
30 | - `.pre-commit-config.yaml`
31 |
--------------------------------------------------------------------------------
/crates/cli/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [profile.release]
2 | opt-level = 'z'
3 | debug = false
4 | rpath = true
5 | lto = 'fat'
6 | codegen-units = 1
7 | strip = 'debuginfo'
8 |
9 | [profile.release.package."*"]
10 | opt-level = 'z'
11 |
--------------------------------------------------------------------------------
/crates/cli/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Who knows what the future holds...
2 |
3 | # X.Y.Z - DD/MM/YYYY
4 |
5 | Nothing... yet.
6 |
7 | # 0.3.0 - 23/04/2025
8 |
9 | ### Changes:
10 |
11 | - CLI now uses `gamedig` v0.7.0 (To update, run `cargo install gamedig_cli`).
12 |
13 | ### Breaking Changes:
14 |
15 | - MSRV has been updated to `1.81.0` to match the latest `gamedig` version.
16 |
17 | # 0.2.1 - 05/12/2024
18 |
19 | Dependencies:
20 | - `gamedig`: `v0.6.0 -> v0.6.1`
21 |
22 | # 0.2.0 - 26/11/2024
23 |
24 | ### Breaking Changes:
25 |
26 | - Restructured the release flow to be more consistent (GitHub releases will no longer be available, use cargo instead).
27 | - Changed crate name from `gamedig-cli` to `gamedig_cli` to align with recommended naming conventions.
28 | - The CLI now requires a minimum Rust version of `1.74.1`.
29 |
30 | # 0.1.1 - 15/07/2024
31 |
32 | ### Changes:
33 |
34 | - Dependency updates (by @cainthebest)
35 | - `gamedig`: `v0.5.0 -> v0.5.1`
36 | - `clap`: `v4.1.11 -> v4.5.4`
37 | - `quick-xml`: `v0.31.0 -> v0.36.0`
38 | - `webbrowser`: `v0.8.12 -> v1.0.0`
39 |
40 | # 0.1.0 - 15/03/2024
41 |
42 | ### Changes:
43 |
44 | - Added the CLI (by @cainthebest).
45 | - Added DNS lookup support (by @Douile).
46 | - Added JSON output option (by @Douile).
47 | - Added BSON output in hex or base64 (by @cainthebest).
48 | - Added XML output option (by @cainthebest).
49 | - Added ExtraRequestSettings as CLI arguments (by @Douile).
50 | - Added TimeoutSettings as CLI argument (by @Douile).
51 | - Added Comprehensive end-user documentation for the CLI interface (by @Douile & @cainthebest).
52 | - Tweaked compile-time flags to allow for a more preformant binary (by @cainthebest).
53 | - Added client for socket capture, dev tools are not included by default (by @Douile).
54 | - Added license information to the CLI (by @cainthebest).
55 | - Added source code information to the CLI (by @cainthebest).
56 |
--------------------------------------------------------------------------------
/crates/cli/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "gamedig_cli"
3 | authors = [
4 | "rust-GameDig contributors [https://github.com/gamedig/rust-gamedig/contributors]",
5 | ]
6 | description = "A command line interface for gamedig"
7 | license = "MIT"
8 | version = "0.3.0"
9 | edition = "2021"
10 | default-run = "gamedig_cli"
11 | homepage = "https://gamedig.github.io/"
12 | repository = "https://github.com/gamedig/rust-gamedig"
13 | readme = "README.md"
14 | keywords = ["server", "query", "game", "check", "status"]
15 | rust-version = "1.81.0"
16 | categories = ["command-line-interface"]
17 |
18 | [features]
19 | default = ["json", "bson", "xml", "browser"]
20 |
21 | # Tools
22 | packet_capture = ["gamedig/packet_capture"]
23 |
24 | # Output formats
25 | bson = ["dep:serde", "dep:bson", "dep:hex", "dep:base64", "gamedig/serde"]
26 | json = ["dep:serde", "dep:serde_json", "gamedig/serde"]
27 | xml = ["dep:serde", "dep:serde_json", "dep:quick-xml", "gamedig/serde"]
28 |
29 | # Misc
30 | browser = ["dep:webbrowser"]
31 |
32 | [dependencies]
33 | # Core Dependencies
34 | thiserror = "2.0.0"
35 | clap = { version = "4.5.4", default-features = false, features = ["derive"] }
36 | gamedig = { version = "0.7.0", default-features = false, features = [
37 | "clap",
38 | "games",
39 | "game_defs",
40 | ] }
41 |
42 | # Feature Dependencies
43 | # Serialization / Deserialization
44 | serde = { version = "1", optional = true, default-features = false }
45 |
46 | # BSON
47 | bson = { version = "2.8.1", optional = true, default-features = false }
48 | base64 = { version = "0.22.0", optional = true, default-features = false, features = ["std"] }
49 | hex = { version = "0.4.3", optional = true, default-features = false }
50 |
51 | # JSON
52 | serde_json = { version = "1", optional = true, default-features = false }
53 |
54 | # XML
55 | quick-xml = { version = "0.37.0", optional = true, default-features = false }
56 |
57 | # Browser
58 | webbrowser = { version = "1.0.0", optional = true, default-features = false }
59 |
60 |
--------------------------------------------------------------------------------
/crates/cli/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 - 2024 GameDig Organization & Contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/crates/cli/README.md:
--------------------------------------------------------------------------------
1 | # Rust GameDig CLI
2 |
3 | The official [rust-GameDig](https://crates.io/crates/gamedig) Command Line Interface.
4 |
5 | [](https://github.com/gamedig/rust-gamedig/actions) [](https://github.com/gamedig/rust-gamedig/blob/main/LICENSE.md) [](https://github.com/gamedig/node-gamedig)
6 |
7 | ## Installation
8 |
9 | You can install the CLI via `cargo`:
10 |
11 | ```sh
12 | cargo install gamedig_cli
13 | ```
14 |
15 | or
16 |
17 | ```sh
18 | cargo install gamedig_cli --git https://github.com/gamedig/rust-gamedig.git
19 | ```
20 |
21 | ## Usage
22 |
23 | 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.
24 |
25 | Here's also a quick rundown of a simple query with the `json-pretty` format:
26 |
27 | Pick a game/service/protocol (check
28 | the [GAMES](https://github.com/gamedig/rust-gamedig/blob/main/GAMES.md), [SERVICES](https://github.com/gamedig/rust-gamedig/blob/main/SERVICES.md)
29 | and [PROTOCOLS](https://github.com/gamedig/rust-gamedig/blob/main/PROTOCOLS.md) files to see the currently supported
30 | ones), provide the ip and the port (be aware that some game servers use a separate port for the info queries, the port
31 | can also be optional if the server is running the default ports) then query on it.
32 |
33 | [Team Fortress 2](https://store.steampowered.com/app/440/Team_Fortress_2/) query example:
34 |
35 | ```sh
36 | gamedig_cli query -g teamfortress2 -i 127.0.0.1 -f json-pretty
37 | ```
38 |
39 | What we are doing here:
40 |
41 | - `-g` (or `--game`) specifies the game.
42 | - `-i` (or `--ip`) target ip.
43 | - `-f` (or `--format`) our preferred format.
44 |
45 | Note: We haven't specified a port (via `-p` or `--port`), so the default one for the game will be used (`27015` in this
46 | case).
47 |
48 | Response (note that some games have a different structure):
49 |
50 | ```json
51 | {
52 | "name": "A cool server.",
53 | "description": null,
54 | "game_mode": "Team Fortress",
55 | "game_version": "8690085",
56 | "map": "cp_foundry",
57 | "players_maximum": 24,
58 | "players_online": 0,
59 | "players_bots": 0,
60 | "has_password": false,
61 | "players": []
62 | }
63 | ```
64 |
65 | ## Contributing
66 |
67 | Please read [CONTRIBUTING](https://github.com/gamedig/rust-gamedig/blob/main/CONTRIBUTING.md).
68 |
--------------------------------------------------------------------------------
/crates/cli/src/error.rs:
--------------------------------------------------------------------------------
1 | pub type Result = std::result::Result;
2 |
3 | #[derive(thiserror::Error, Debug)]
4 | pub enum Error {
5 | #[error("IO Error: {0}")]
6 | Io(#[from] std::io::Error),
7 |
8 | #[error("Clap Error: {0}")]
9 | Clap(#[from] clap::Error),
10 |
11 | #[error("Gamedig Error: {0}")]
12 | Gamedig(#[from] gamedig::errors::GDError),
13 |
14 | #[cfg(any(feature = "json", feature = "xml"))]
15 | #[error("Serde Error: {0}")]
16 | Serde(#[from] serde_json::Error),
17 |
18 | #[cfg(feature = "bson")]
19 | #[error("Bson Error: {0}")]
20 | Bson(#[from] bson::ser::Error),
21 |
22 | #[cfg(feature = "xml")]
23 | #[error("Xml Error: {0}")]
24 | Xml(#[from] quick_xml::Error),
25 |
26 | #[error("Unknown Game: {0}")]
27 | UnknownGame(String),
28 |
29 | #[error("Invalid hostname: {0}")]
30 | InvalidHostname(String),
31 | }
32 |
--------------------------------------------------------------------------------
/crates/id-tests/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "gamedig-id-tests"
3 | version = "0.0.1"
4 | edition = "2021"
5 | authors = [
6 | "rust-GameDig contributors [https://github.com/gamedig/rust-gamedig/contributors]",
7 | "node-GameDig contributors [https://github.com/gamedig/node-gamedig/contributors]",
8 | ]
9 | license = "MIT"
10 | description = "Test if IDs match the gamedig rules"
11 | homepage = "https://github.com/gamedig/rust-gamedig/CONTRIBUTING.md#naming"
12 | repository = "https://github.com/gamedig/rust-gamedig"
13 | readme = "README.md"
14 | rust-version = "1.65.0"
15 |
16 | [features]
17 | cli = ["dep:serde_json", "dep:serde"]
18 | default = ["cli"]
19 |
20 | [[bin]]
21 | name = "gamedig-id-tests"
22 | required-features = ["cli"]
23 |
24 | [dependencies]
25 | number_to_words = "0.1"
26 | roman_numeral = "0.1"
27 |
28 | serde_json = { version = "1", optional = true }
29 | serde = { version = "1", optional = true, features = ["derive"] }
--------------------------------------------------------------------------------
/crates/id-tests/src/main.rs:
--------------------------------------------------------------------------------
1 | #![cfg(feature = "cli")]
2 |
3 | use std::collections::HashMap;
4 |
5 | /// Format for input games (the same as used in node-gamedig/lib/games.js).
6 | type GamesInput = HashMap;
7 |
8 | #[derive(Debug, Clone, PartialEq, serde::Deserialize)]
9 | struct Game {
10 | name: String,
11 | }
12 |
13 | use gamedig_id_tests::test_game_name_rules;
14 |
15 | fn main() {
16 | let games: GamesInput = std::env::args_os().nth(1).map_or_else(
17 | || serde_json::from_reader(std::io::stdin().lock()).unwrap(),
18 | |file| {
19 | let file = std::fs::OpenOptions::new().read(true).open(file).unwrap();
20 |
21 | serde_json::from_reader(file).unwrap()
22 | },
23 | );
24 |
25 | let failed = test_game_name_rules(
26 | games
27 | .iter()
28 | .map(|(key, game)| (key.as_str(), game.name.as_str())),
29 | );
30 |
31 | assert!(failed.is_empty());
32 | }
33 |
--------------------------------------------------------------------------------
/crates/id-tests/src/utils.rs:
--------------------------------------------------------------------------------
1 | /// Split a str when characters swap between being digits and not digits.
2 | pub fn split_on_switch_between_alpha_numeric(text: &str) -> Vec {
3 | if text.is_empty() {
4 | return vec![];
5 | }
6 |
7 | let mut parts = Vec::with_capacity(text.len());
8 | let mut current = Vec::with_capacity(text.len());
9 |
10 | let mut iter = text.chars();
11 | let c = iter.next().unwrap();
12 | let mut last_was_numeric = c.is_ascii_digit();
13 | current.push(c);
14 |
15 | for c in iter {
16 | if c.is_ascii_digit() == last_was_numeric {
17 | current.push(c);
18 | } else {
19 | parts.push(current.iter().collect());
20 | current.clear();
21 | current.push(c);
22 | last_was_numeric = !last_was_numeric;
23 | }
24 | }
25 |
26 | parts.push(current.into_iter().collect());
27 |
28 | parts
29 | }
30 |
31 | #[test]
32 | fn split_correctly() {
33 | assert_eq!(
34 | split_on_switch_between_alpha_numeric("2D45A"),
35 | &["2", "D", "45", "A"]
36 | );
37 | }
38 |
39 | #[test]
40 | fn split_symbol_broken_numbers() {
41 | let game_name = super::extract_game_parts_from_name("Darkest Hour: Europe '44-'45");
42 | assert_eq!(game_name.words, &["Darkest", "Hour", "Europe", "4445"]);
43 | }
44 |
45 | /// Extract parts at end of string enclosed in brackets.
46 | pub fn extract_bracketed_suffix(text: &str) -> (&str, Option<&str>) {
47 | if let Some(text) = text.strip_suffix(')') {
48 | if let Some((text, extra)) = text.rsplit_once('(') {
49 | return (text, Some(extra));
50 | }
51 | }
52 |
53 | (text, None)
54 | }
55 |
56 | #[test]
57 | fn extract_brackets_correctly() {
58 | assert_eq!(
59 | extract_bracketed_suffix("no brackets here"),
60 | ("no brackets here", None)
61 | );
62 | assert_eq!(
63 | extract_bracketed_suffix("Game name (with protocol here)"),
64 | ("Game name ", Some("with protocol here"))
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/crates/lib/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "gamedig"
3 | version = "0.7.0"
4 | edition = "2021"
5 | authors = [
6 | "rust-GameDig contributors [https://github.com/gamedig/rust-gamedig/contributors]",
7 | "node-GameDig contributors [https://github.com/gamedig/node-gamedig/contributors]",
8 | ]
9 | license = "MIT"
10 | description = "Query game servers and not only."
11 | homepage = "https://gamedig.github.io/"
12 | documentation = "https://docs.rs/gamedig/latest/gamedig/"
13 | repository = "https://github.com/gamedig/rust-gamedig"
14 | readme = "README.md"
15 | keywords = ["server", "query", "game", "check", "status"]
16 | rust-version = "1.81.0"
17 | categories = ["parser-implementations", "parsing", "network-programming", "encoding"]
18 |
19 | [features]
20 | default = ["games", "services", "game_defs"]
21 |
22 | # Enable query functions for specific games
23 | games = []
24 | # Enable game definitions for use with the generic query functions
25 | game_defs = ["dep:phf", "games"]
26 |
27 | # Enable service querying
28 | services = []
29 |
30 | # Enable serde derivations for our types
31 | serde = []
32 |
33 | # Enable clap derivations for our types
34 | clap = ["dep:clap"]
35 | packet_capture = ["dep:pcap-file", "dep:pnet_packet", "dep:lazy_static"]
36 |
37 | # Enable TLS for HTTP Client
38 | tls = ["ureq/tls"]
39 |
40 | [dependencies]
41 | byteorder = "1.5"
42 | bzip2-rs = "0.1"
43 | crc32fast = "1.4"
44 | base64 = "0.22.0"
45 |
46 | encoding_rs = "0.8"
47 | ureq = { version = "2.9", default-features = false, features = ["gzip", "json"] }
48 | url = "2"
49 |
50 | serde = { version = "1.0", features = ["derive"] }
51 | serde_json = { version = "1.0" }
52 |
53 | phf = { version = "0.11", optional = true, features = ["macros"] }
54 |
55 | clap = { version = "4.5.4", optional = true, features = ["derive"] }
56 |
57 | pcap-file = { version = "2.0", optional = true }
58 | pnet_packet = { version = "0.35", optional = true }
59 | lazy_static = { version = "1.4", optional = true }
60 |
61 | [dev-dependencies]
62 | gamedig-id-tests = { path = "../id-tests", default-features = false }
63 |
64 | # Examples
65 | [[example]]
66 | name = "minecraft"
67 | required-features = ["games"]
68 |
69 | [[example]]
70 | name = "teamfortress2"
71 | required-features = ["games"]
72 |
73 | [[example]]
74 | name = "valve_master_server_query"
75 | required-features = ["services"]
76 |
77 | [[example]]
78 | name = "test_eco"
79 | required-features = ["games"]
80 |
81 | [[example]]
82 | name = "generic"
83 | required-features = ["games", "game_defs"]
84 |
--------------------------------------------------------------------------------
/crates/lib/README.md:
--------------------------------------------------------------------------------
1 | rust-GameDig
2 |
3 | The fast library for querying game servers/services.
4 |
5 |
19 |
20 |
21 | This library brings what
22 |
23 | node-GameDig
24 |
25 | does (and not only), to pure Rust!
26 |
27 |
28 | **Warning**: This project goes through frequent API breaking changes and hasn't been thoroughly tested.
29 |
30 | ## Community
31 |
32 | Checkout the GameDig Community Discord Server [here](https://discord.gg/NVCMn3tnxH).
33 | Note that it isn't be a replacement for GitHub issues, if you have found a problem
34 | within the library or want to request a feature, it's better to do so here rather than
35 | on Discord.
36 |
37 | ## Usage
38 |
39 | Minimum Supported Rust Version is `1.81.0` and the code is cross-platform.
40 |
41 | Pick a game/service/protocol (check
42 | the [GAMES](https://github.com/gamedig/rust-gamedig/blob/main/GAMES.md), [SERVICES](https://github.com/gamedig/rust-gamedig/blob/main/SERVICES.md)
43 | and [PROTOCOLS](https://github.com/gamedig/rust-gamedig/blob/main/PROTOCOLS.md) files to see the currently supported
44 | ones), provide the ip and the port (be aware that some game servers use a separate port for the info queries, the port
45 | can also be optional if the server is running the default ports) then query on it.
46 |
47 | [Team Fortress 2](https://store.steampowered.com/app/440/Team_Fortress_2/) query example:
48 |
49 | ```rust
50 | use gamedig::games::teamfortress2;
51 |
52 | fn main() {
53 | let response = teamfortress2::query(&"127.0.0.1".parse().unwrap(), None);
54 | // None is the default port (which is 27015), could also be Some(27015)
55 |
56 | match response { // Result type, must check what it is...
57 | Err(error) => println!("Couldn't query, error: {}", error),
58 | Ok(r) => println!("{:#?}", r)
59 | }
60 | }
61 | ```
62 |
63 | Response (note that some games have a different structure):
64 |
65 | ```json5
66 | {
67 | protocol: 17,
68 | name: "Team Fortress 2 Dedicated Server.",
69 | map: "ctf_turbine",
70 | game: "tf2",
71 | appid: 440,
72 | players_online: 0,
73 | players_details: [],
74 | players_maximum: 69,
75 | players_bots: 0,
76 | server_type: Dedicated,
77 | has_password: false,
78 | vac_secured: true,
79 | version: "7638371",
80 | port: Some(27015),
81 | steam_id: Some(69753253289735296),
82 | tv_port: None,
83 | tv_name: None,
84 | keywords: Some(
85 | "alltalk,nocrits"
86 | ),
87 | rules: [
88 | "mp_autoteambalance"
89 | :
90 | "1",
91 | "mp_maxrounds"
92 | :
93 | "5",
94 | //....
95 | ]
96 | }
97 | ```
98 |
99 | Want to see more examples? Checkout
100 | the [examples](https://github.com/gamedig/rust-gamedig/tree/main/crates/lib/examples) folder.
101 |
102 | ## Documentation
103 |
104 | The documentation is available at [docs.rs](https://docs.rs/gamedig/latest/gamedig/).
105 | Curious about the history and what changed between versions? Everything is in
106 | the [CHANGELOG](https://github.com/gamedig/rust-gamedig/blob/main/crates/lib/CHANGELOG.md) file.
107 |
108 | ## Contributing
109 |
110 | If you want to see your favorite game/service being supported here, open an issue, and I'll prioritize it (or do a pull
111 | request if you want to implement it yourself)!
112 |
113 | Before contributing please read [CONTRIBUTING](https://github.com/gamedig/rust-gamedig/blob/main/CONTRIBUTING.md).
114 |
115 |
--------------------------------------------------------------------------------
/crates/lib/examples/generic.rs:
--------------------------------------------------------------------------------
1 | use gamedig::{
2 | protocols::types::CommonResponse,
3 | query_with_timeout_and_extra_settings,
4 | ExtraRequestSettings,
5 | GDResult,
6 | Game,
7 | TimeoutSettings,
8 | GAMES,
9 | };
10 |
11 | use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
12 |
13 | /// Make a query given the name of a game
14 | /// The `game` argument is taken from the [GAMES](gamedig::GAMES) map.
15 | fn generic_query(
16 | game: &Game,
17 | addr: &IpAddr,
18 | port: Option,
19 | timeout_settings: Option,
20 | extra_settings: Option,
21 | ) -> GDResult> {
22 | println!("Querying {:#?} with game {:#?}.", addr, game);
23 |
24 | let response = query_with_timeout_and_extra_settings(game, addr, port, timeout_settings, extra_settings)?;
25 | println!("Response: {:#?}", response.as_json());
26 |
27 | let common = response.as_original();
28 | println!("Common response: {:#?}", common);
29 |
30 | Ok(response)
31 | }
32 |
33 | fn main() {
34 | let mut args = std::env::args().skip(1);
35 |
36 | // Handle arguments
37 | if let Some(game_name) = args.next() {
38 | let hostname = args.next().expect("Must provide an address");
39 | // Use to_socket_addrs to resolve hostname to IP
40 | let addr: SocketAddr = format!("{}:0", hostname)
41 | .to_socket_addrs()
42 | .unwrap()
43 | .next()
44 | .expect("Could not lookup host");
45 | let port: Option = args.next().map(|s| s.parse().unwrap());
46 |
47 | let timeout_settings = TimeoutSettings::new(
48 | TimeoutSettings::default().get_read(),
49 | TimeoutSettings::default().get_write(),
50 | TimeoutSettings::default().get_connect(),
51 | 2,
52 | )
53 | .unwrap();
54 |
55 | let game = GAMES
56 | .get(&game_name)
57 | .expect("Game doesn't exist, run without arguments to see a list of games");
58 |
59 | let extra_settings = game
60 | .request_settings
61 | .clone()
62 | .set_hostname(hostname.to_string())
63 | .set_check_app_id(false);
64 |
65 | generic_query(
66 | game,
67 | &addr.ip(),
68 | port,
69 | Some(timeout_settings),
70 | Some(extra_settings),
71 | )
72 | .unwrap();
73 | } else {
74 | // Without arguments print a list of games
75 | for (name, game) in GAMES.entries() {
76 | println!("{}\t{}", name, game.name);
77 | }
78 | }
79 | }
80 |
81 | #[cfg(test)]
82 | mod test {
83 | use gamedig::{protocols::types::TimeoutSettings, GAMES};
84 | use std::{
85 | net::{IpAddr, Ipv4Addr},
86 | time::Duration,
87 | };
88 |
89 | use super::generic_query;
90 |
91 | const ADDR: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);
92 |
93 | fn test_game(game_name: &str) {
94 | let timeout_settings = Some(
95 | TimeoutSettings::new(
96 | Some(Duration::from_nanos(1)),
97 | Some(Duration::from_nanos(1)),
98 | Some(Duration::from_nanos(1)),
99 | 0,
100 | )
101 | .unwrap(),
102 | );
103 |
104 | let game = GAMES
105 | .get(game_name)
106 | .expect("Game doesn't exist, run without arguments to see a list of games");
107 |
108 | assert!(generic_query(game, &ADDR, None, timeout_settings, None).is_err());
109 | }
110 |
111 | #[test]
112 | fn battlefield1942() { test_game("battlefield1942"); }
113 |
114 | #[test]
115 | fn minecraft() { test_game("minecraft"); }
116 |
117 | #[test]
118 | fn teamfortress2() { test_game("teamfortress2"); }
119 |
120 | #[test]
121 | fn quake2() { test_game("quake2"); }
122 |
123 | #[test]
124 | fn all_games() {
125 | for game_name in GAMES.keys() {
126 | test_game(game_name);
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/crates/lib/examples/minecraft.rs:
--------------------------------------------------------------------------------
1 | use gamedig::minecraft;
2 | use gamedig::minecraft::types::RequestSettings;
3 |
4 | fn main() {
5 | // or Some(), None is the default protocol port (which is 25565 for java
6 | // and 19132 for bedrock)
7 | let response = minecraft::query(&"127.0.0.1".parse().unwrap(), None);
8 | // This will fail if no server is available locally!
9 |
10 | match response {
11 | Err(error) => println!("Couldn't query, error: {}", error),
12 | Ok(r) => println!("{:#?}", r),
13 | }
14 |
15 | // This is an example to query a server with a hostname to be specified in the
16 | // packet. Passing -1 on the protocol_version means anything, note that
17 | // an invalid value here might result in server not responding.
18 | let response = minecraft::query_java(
19 | &"209.222.114.62".parse().unwrap(),
20 | Some(25565),
21 | Some(RequestSettings {
22 | hostname: "mc.hypixel.net".to_string(),
23 | protocol_version: -1,
24 | }),
25 | );
26 |
27 | match response {
28 | Err(error) => println!("Couldn't query, error: {}", error),
29 | Ok(r) => println!("{:#?}", r),
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/crates/lib/examples/teamfortress2.rs:
--------------------------------------------------------------------------------
1 | use gamedig::games::teamfortress2;
2 |
3 | fn main() {
4 | let response = teamfortress2::query(&"127.0.0.1".parse().unwrap(), None);
5 | // or Some(27015), None is the default protocol port (which is 27015)
6 |
7 | match response {
8 | // Result type, must check what it is...
9 | Err(error) => println!("Couldn't query, error: {}", error),
10 | Ok(r) => println!("{:#?}", r),
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/crates/lib/examples/test_eco.rs:
--------------------------------------------------------------------------------
1 | use gamedig::games::eco;
2 | use std::net::IpAddr;
3 | use std::str::FromStr;
4 |
5 | fn main() {
6 | let ip = IpAddr::from_str("142.132.154.69").unwrap();
7 | let port = 31111;
8 | let r = eco::query(&ip, Some(port));
9 | println!("{:#?}", r);
10 | }
11 |
--------------------------------------------------------------------------------
/crates/lib/examples/valve_master_server_query.rs:
--------------------------------------------------------------------------------
1 | use gamedig::valve_master_server::{query, Filter, Region, SearchFilters};
2 |
3 | fn main() {
4 | let search_filters = SearchFilters::new()
5 | .insert(Filter::RunsAppID(440))
6 | .insert(Filter::CanBeEmpty(false))
7 | .insert(Filter::CanBeFull(false))
8 | .insert(Filter::CanHavePassword(false))
9 | .insert(Filter::IsSecured(true))
10 | .insert(Filter::HasTags(vec!["minecraft".to_string()]));
11 |
12 | let ips = query(Region::Europe, Some(search_filters)).unwrap();
13 | println!("Servers: {:?} \n Amount: {}", ips, ips.len());
14 | }
15 |
--------------------------------------------------------------------------------
/crates/lib/examples/valve_protocol_query.rs:
--------------------------------------------------------------------------------
1 | use gamedig::protocols::types::GatherToggle;
2 | use gamedig::protocols::valve;
3 | use gamedig::protocols::valve::{Engine, GatheringSettings};
4 | use gamedig::TimeoutSettings;
5 | use std::net::{IpAddr, Ipv4Addr, SocketAddr};
6 | use std::time::Duration;
7 |
8 | fn main() {
9 | let address = &SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 27015);
10 | let engine = Engine::Source(None); // We don't specify a steam app id, let the query try to find it.
11 | let gather_settings = GatheringSettings {
12 | players: GatherToggle::Enforce, // We want to query for players
13 | rules: GatherToggle::Skip, // We don't want to query for rules
14 | check_app_id: false, // Loosen up the query a bit by not checking app id
15 | };
16 |
17 | let read_timeout = Duration::from_secs(2);
18 | let write_timeout = Duration::from_secs(3);
19 | let connect_timeout = Duration::from_secs(4);
20 | let retries = 1; // does another request if the first one fails.
21 | let timeout_settings = TimeoutSettings::new(
22 | Some(read_timeout),
23 | Some(write_timeout),
24 | Some(connect_timeout),
25 | retries,
26 | )
27 | .unwrap();
28 |
29 | let response = valve::query(
30 | address,
31 | engine,
32 | Some(gather_settings),
33 | Some(timeout_settings),
34 | );
35 | println!("{response:#?}");
36 | }
37 |
--------------------------------------------------------------------------------
/crates/lib/src/capture/mod.rs:
--------------------------------------------------------------------------------
1 | pub(crate) mod packet;
2 | mod pcap;
3 | pub(crate) mod socket;
4 | pub(crate) mod writer;
5 |
6 | use self::{pcap::Pcap, writer::Writer};
7 | use pcap_file::pcapng::{blocks::interface_description::InterfaceDescriptionBlock, PcapNgBlock, PcapNgWriter};
8 | use std::path::PathBuf;
9 |
10 | pub fn setup_capture(file_path: Option) {
11 | if let Some(file_path) = file_path {
12 | let file = std::fs::OpenOptions::new()
13 | .create_new(true)
14 | .write(true)
15 | .open(file_path.with_extension("pcap"))
16 | .unwrap();
17 |
18 | let mut pcap_writer = PcapNgWriter::new(file).unwrap();
19 |
20 | // Write headers
21 | let _ = pcap_writer.write_block(
22 | &InterfaceDescriptionBlock {
23 | linktype: pcap_file::DataLink::ETHERNET,
24 | snaplen: 0xFFFF,
25 | options: vec![],
26 | }
27 | .into_block(),
28 | );
29 |
30 | let writer = Box::new(Pcap::new(pcap_writer));
31 | attach(writer)
32 | }
33 | }
34 |
35 | /// Attaches a writer to the capture module.
36 | ///
37 | /// # Errors
38 | /// Returns an Error if the writer is already set.
39 | fn attach(writer: Box) { crate::capture::socket::set_writer(writer); }
40 |
--------------------------------------------------------------------------------
/crates/lib/src/capture/writer.rs:
--------------------------------------------------------------------------------
1 | use std::{io::Write, sync::Mutex};
2 |
3 | use super::{
4 | packet::{CapturePacket, Protocol},
5 | pcap::Pcap,
6 | };
7 | use crate::GDResult;
8 | use lazy_static::lazy_static;
9 |
10 | lazy_static! {
11 | /// A globally accessible, lazily-initialized static writer instance.
12 | /// This writer is intended for capturing and recording network packets.
13 | /// The writer is wrapped in a Mutex to ensure thread-safe access and modification.
14 | pub(crate) static ref CAPTURE_WRITER: Mutex>> = Mutex::new(None);
15 | }
16 |
17 | /// Trait defining the functionality for a writer that handles network packet
18 | /// captures. This trait includes methods for writing packet data, handling new
19 | /// connections, and closing connections.
20 | pub(crate) trait Writer {
21 | /// Writes a given packet's data to an underlying storage or stream.
22 | ///
23 | /// # Arguments
24 | /// * `packet` - Reference to the packet being captured.
25 | /// * `data` - The raw byte data associated with the packet.
26 | ///
27 | /// # Returns
28 | /// A `GDResult` indicating the success or failure of the write operation.
29 | fn write(&mut self, packet: &CapturePacket, data: &[u8]) -> GDResult<()>;
30 |
31 | /// Handles the creation of a new connection, potentially logging or
32 | /// initializing resources.
33 | ///
34 | /// # Arguments
35 | /// * `packet` - Reference to the packet indicating a new connection.
36 | ///
37 | /// # Returns
38 | /// A `GDResult` indicating the success or failure of handling the new
39 | /// connection.
40 | fn new_connect(&mut self, packet: &CapturePacket) -> GDResult<()>;
41 |
42 | /// Closes a connection, handling any necessary cleanup or finalization.
43 | ///
44 | /// # Arguments
45 | /// * `packet` - Reference to the packet indicating the closure of a
46 | /// connection.
47 | ///
48 | /// # Returns
49 | /// A `GDResult` indicating the success or failure of the connection closure
50 | /// operation.
51 | fn close_connection(&mut self, packet: &CapturePacket) -> GDResult<()>;
52 | }
53 |
54 | /// Implementation of the `Writer` trait for the `Pcap` struct.
55 | /// This implementation enables writing, connection handling, and closure
56 | /// specific to PCAP (Packet Capture) format.
57 | impl Writer for Pcap {
58 | fn write(&mut self, info: &CapturePacket, data: &[u8]) -> GDResult<()> {
59 | self.write_transport_packet(info, data);
60 |
61 | Ok(())
62 | }
63 |
64 | fn new_connect(&mut self, packet: &CapturePacket) -> GDResult<()> {
65 | match packet.protocol {
66 | Protocol::Tcp => {
67 | self.write_tcp_handshake(packet);
68 | }
69 | Protocol::Udp => {}
70 | }
71 |
72 | self.state.stream_count = self.state.stream_count.wrapping_add(1);
73 |
74 | Ok(())
75 | }
76 |
77 | fn close_connection(&mut self, packet: &CapturePacket) -> GDResult<()> {
78 | match packet.protocol {
79 | Protocol::Tcp => {
80 | self.send_tcp_fin(packet);
81 | }
82 | Protocol::Udp => {}
83 | }
84 | Ok(())
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/crates/lib/src/errors/error.rs:
--------------------------------------------------------------------------------
1 | use crate::GDErrorKind;
2 | use std::error::Error;
3 | use std::fmt::Formatter;
4 | use std::{backtrace, fmt};
5 |
6 | pub(crate) type ErrorSource = Box;
7 |
8 | /// The GameDig error type.
9 | ///
10 | /// Can be created in three ways (all of which will implicitly generate a
11 | /// backtrace):
12 | ///
13 | /// Directly from an [error kind](GDErrorKind) (without a
14 | /// source).
15 | ///
16 | /// ```
17 | /// use gamedig::{GDError, GDErrorKind};
18 | /// let _: GDError = GDErrorKind::PacketBad.into();
19 | /// ```
20 | ///
21 | /// [From an error kind with a source](GDErrorKind::context) (any
22 | /// type that implements `Into>`).
23 | ///
24 | /// ```
25 | /// use gamedig::{GDError, GDErrorKind};
26 | /// let _: GDError = GDErrorKind::PacketBad.context("Reason the packet was bad");
27 | /// ```
28 | ///
29 | /// Using the [new helper](GDError::new).
30 | ///
31 | /// ```
32 | /// use gamedig::{GDError, GDErrorKind};
33 | /// let _: GDError = GDError::new(GDErrorKind::PacketBad, Some("Reason the packet was bad".into()));
34 | /// ```
35 | pub struct GDError {
36 | pub kind: GDErrorKind,
37 | pub source: Option,
38 | pub backtrace: Option,
39 | }
40 |
41 | impl From for GDError {
42 | fn from(value: GDErrorKind) -> Self {
43 | let backtrace = Some(backtrace::Backtrace::capture());
44 | Self {
45 | kind: value,
46 | source: None,
47 | backtrace,
48 | }
49 | }
50 | }
51 |
52 | impl PartialEq for GDError {
53 | fn eq(&self, other: &Self) -> bool { self.kind == other.kind }
54 | }
55 |
56 | impl Error for GDError {
57 | fn source(&self) -> Option<&(dyn Error + 'static)> { self.source.as_ref().map(|err| Box::as_ref(err) as _) }
58 | }
59 |
60 | impl fmt::Debug for GDError {
61 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
62 | writeln!(f, "GDError{{ kind={:?}", self.kind)?;
63 | if let Some(source) = &self.source {
64 | writeln!(f, " source={source:?}")?;
65 | }
66 | if let Some(backtrace) = &self.backtrace {
67 | let bt = format!("{backtrace:#?}");
68 | writeln!(f, " backtrace={}", bt.replace('\n', "\n "))?;
69 | }
70 | writeln!(f, "}}")?;
71 | Ok(())
72 | }
73 | }
74 |
75 | impl fmt::Display for GDError {
76 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{self:?}") }
77 | }
78 |
79 | impl GDError {
80 | /// Create a new error (with automatic backtrace)
81 | pub fn new(kind: GDErrorKind, source: Option) -> Self {
82 | let backtrace = Some(backtrace::Backtrace::capture());
83 | Self {
84 | kind,
85 | source,
86 | backtrace,
87 | }
88 | }
89 |
90 | /// Create a new error using any type that can be converted to an error
91 | pub fn from_error>(kind: GDErrorKind, source: E) -> Self {
92 | Self::new(kind, Some(source.into()))
93 | }
94 | }
95 |
96 | #[cfg(test)]
97 | mod tests {
98 | use super::*;
99 |
100 | // test error trait GDError
101 | #[test]
102 | fn test_error_trait() {
103 | let source: Result = "nan".parse();
104 | let source_err = source.unwrap_err();
105 |
106 | let error_with_context = GDErrorKind::TypeParse.context(source_err.clone());
107 | assert!(error_with_context.source().is_some());
108 | assert_eq!(
109 | format!("{}", error_with_context.source().unwrap()),
110 | format!("{source_err}")
111 | );
112 |
113 | let error_without_context: GDError = GDErrorKind::TypeParse.into();
114 | assert!(error_without_context.source().is_none());
115 | }
116 |
117 | // Test creating GDError with GDError::new
118 | #[test]
119 | fn test_create_new() {
120 | let error_from_new = GDError::new(GDErrorKind::InvalidInput, None);
121 | assert!(error_from_new.backtrace.is_some());
122 | assert_eq!(error_from_new.kind, GDErrorKind::InvalidInput);
123 | assert!(error_from_new.source.is_none());
124 | }
125 |
126 | // Test creating GDError with GDErrorKind::context
127 | #[test]
128 | fn test_create_context() {
129 | let error_from_context = GDErrorKind::InvalidInput.context("test");
130 | assert!(error_from_context.backtrace.is_some());
131 | assert_eq!(error_from_context.kind, GDErrorKind::InvalidInput);
132 | assert!(error_from_context.source.is_some());
133 | }
134 |
135 | // Test creating GDError with From for GDError
136 | #[test]
137 | fn test_create_into() {
138 | let error_from_into: GDError = GDErrorKind::InvalidInput.into();
139 | assert!(error_from_into.backtrace.is_some());
140 | assert_eq!(error_from_into.kind, GDErrorKind::InvalidInput);
141 | assert!(error_from_into.source.is_none());
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/crates/lib/src/errors/kind.rs:
--------------------------------------------------------------------------------
1 | use crate::error::ErrorSource;
2 | use crate::GDError;
3 |
4 | /// All GameDig Error kinds.
5 | #[derive(Debug, Clone, PartialEq, Eq)]
6 | pub enum GDErrorKind {
7 | /// The received packet was bigger than the buffer size.
8 | PacketOverflow,
9 | /// The received packet was shorter than the expected one.
10 | PacketUnderflow,
11 | /// The received packet is badly formatted.
12 | PacketBad,
13 | /// Couldn't send the packet.
14 | PacketSend,
15 | /// Couldn't receieve data when it was expected.
16 | PacketReceive,
17 | /// Couldn't decompress data.
18 | Decompress,
19 | /// Couldn't create a socket connection.
20 | SocketConnect,
21 | /// Couldn't bind a socket.
22 | SocketBind,
23 | /// Invalid input to the library.
24 | InvalidInput,
25 | /// The server response indicated that it is a different game than the game
26 | /// queried.
27 | BadGame,
28 | /// Couldn't automatically query (none of the attempted protocols were
29 | /// successful).
30 | AutoQuery,
31 | /// A protocol-defined expected format was not met.
32 | ProtocolFormat,
33 | /// Couldn't cast a value to an enum.
34 | UnknownEnumCast,
35 | /// Couldn't parse a json string.
36 | JsonParse,
37 | /// Couldn't parse a value.
38 | TypeParse,
39 | /// Couldn't find the host specified.
40 | HostLookup,
41 | }
42 |
43 | impl GDErrorKind {
44 | /// Convert error kind into a full error with a source (and implicit
45 | /// backtrace)
46 | ///
47 | /// ```
48 | /// use gamedig::{GDErrorKind, GDResult};
49 | /// let _: GDResult = "thing".parse().map_err(|e| GDErrorKind::TypeParse.context(e));
50 | /// ```
51 | pub fn context>(self, source: E) -> GDError { GDError::from_error(self, source) }
52 | }
53 |
54 | #[cfg(test)]
55 | mod tests {
56 | use super::*;
57 |
58 | // Testing cloning the GDErrorKind type
59 | #[test]
60 | fn test_cloning() {
61 | let error = GDErrorKind::BadGame;
62 | let cloned_error = error.clone();
63 | assert_eq!(error, cloned_error);
64 | }
65 |
66 | // test display GDError
67 | #[test]
68 | fn test_display() {
69 | let err = GDErrorKind::BadGame.context("Rust is not a game");
70 | assert_eq!(
71 | format!("{err}"),
72 | "GDError{ kind=BadGame\n source=\"Rust is not a game\"\n backtrace=\n}\n"
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/crates/lib/src/errors/mod.rs:
--------------------------------------------------------------------------------
1 | //! Every GameDig errors.
2 |
3 | /// The Error with backtrace.
4 | pub mod error;
5 | /// All defined Error kinds.
6 | pub mod kind;
7 | /// `GDResult`, a shorthand of `Result`.
8 | pub mod result;
9 |
10 | pub use error::*;
11 | pub use kind::*;
12 | pub use result::*;
13 |
--------------------------------------------------------------------------------
/crates/lib/src/errors/result.rs:
--------------------------------------------------------------------------------
1 | use crate::GDError;
2 |
3 | /// `Result` of `T` and `GDError`.
4 | pub type GDResult = Result;
5 |
6 | #[cfg(test)]
7 | mod tests {
8 | use super::*;
9 | use crate::GDErrorKind;
10 |
11 | // Testing Ok variant of the GDResult type
12 | #[test]
13 | fn test_gdresult_ok() {
14 | let result: GDResult = Ok(42);
15 | assert_eq!(result, Ok(42));
16 | }
17 |
18 | // Testing Err variant of the GDResult type
19 | #[test]
20 | fn test_gdresult_err() {
21 | let result: GDResult = Err(GDErrorKind::InvalidInput.into());
22 | assert!(result.is_err());
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/crates/lib/src/games/battalion1944.rs:
--------------------------------------------------------------------------------
1 | use crate::protocols::valve::Engine;
2 | use crate::{
3 | protocols::valve::{self, game},
4 | GDErrorKind::TypeParse,
5 | GDResult,
6 | };
7 | use std::net::{IpAddr, SocketAddr};
8 |
9 | pub fn query(address: &IpAddr, port: Option) -> GDResult {
10 | let mut valve_response = valve::query(
11 | &SocketAddr::new(*address, port.unwrap_or(7780)),
12 | Engine::new(489_940),
13 | None,
14 | None,
15 | )?;
16 |
17 | if let Some(rules) = &mut valve_response.rules {
18 | if let Some(bat_max_players) = rules.get("bat_max_players_i") {
19 | valve_response.info.players_maximum = bat_max_players.parse().map_err(|e| TypeParse.context(e))?;
20 | rules.remove("bat_max_players_i");
21 | }
22 |
23 | if let Some(bat_player_count) = rules.get("bat_player_count_s") {
24 | valve_response.info.players_online = bat_player_count.parse().map_err(|e| TypeParse.context(e))?;
25 | rules.remove("bat_player_count_s");
26 | }
27 |
28 | if let Some(bat_has_password) = rules.get("bat_has_password_s") {
29 | valve_response.info.has_password = bat_has_password == "Y";
30 | rules.remove("bat_has_password_s");
31 | }
32 |
33 | if let Some(bat_name) = rules.get("bat_name_s") {
34 | valve_response.info.name.clone_from(bat_name);
35 | rules.remove("bat_name_s");
36 | }
37 |
38 | if let Some(bat_gamemode) = rules.get("bat_gamemode_s") {
39 | valve_response.info.game_mode.clone_from(bat_gamemode);
40 | rules.remove("bat_gamemode_s");
41 | }
42 |
43 | rules.remove("bat_map_s");
44 | }
45 |
46 | Ok(game::Response::new_from_valve_response(valve_response))
47 | }
48 |
--------------------------------------------------------------------------------
/crates/lib/src/games/eco/mod.rs:
--------------------------------------------------------------------------------
1 | /// The implementation.
2 | /// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/ffow.js)
3 | pub mod protocol;
4 | /// All types used by the implementation.
5 | pub mod types;
6 |
7 | pub use protocol::*;
8 | pub use types::*;
9 |
--------------------------------------------------------------------------------
/crates/lib/src/games/eco/protocol.rs:
--------------------------------------------------------------------------------
1 | use crate::eco::{EcoRequestSettings, Response, Root};
2 | use crate::http::HttpClient;
3 | use crate::{GDResult, TimeoutSettings};
4 | use std::net::{IpAddr, SocketAddr};
5 |
6 | /// Query an eco server.
7 | #[inline]
8 | pub fn query(address: &IpAddr, port: Option) -> GDResult { query_with_timeout(address, port, &None) }
9 |
10 | /// Query an eco server.
11 | #[inline]
12 | pub fn query_with_timeout(
13 | address: &IpAddr,
14 | port: Option,
15 | timeout_settings: &Option,
16 | ) -> GDResult {
17 | query_with_timeout_and_extra_settings(address, port, timeout_settings, None)
18 | }
19 |
20 | /// Query an eco server.
21 | pub fn query_with_timeout_and_extra_settings(
22 | address: &IpAddr,
23 | port: Option,
24 | timeout_settings: &Option,
25 | extra_settings: Option,
26 | ) -> GDResult {
27 | let address = &SocketAddr::new(*address, port.unwrap_or(3001));
28 | let mut client = HttpClient::new(
29 | address,
30 | timeout_settings,
31 | extra_settings.unwrap_or_default().into(),
32 | )?;
33 |
34 | let response = client.get_json::("/frontpage", None)?;
35 |
36 | Ok(response.into())
37 | }
38 |
--------------------------------------------------------------------------------
/crates/lib/src/games/epic.rs:
--------------------------------------------------------------------------------
1 | //! Unreal2 game query modules
2 |
3 | use crate::protocols::epic::game_query_mod;
4 |
5 | game_query_mod!(
6 | asa,
7 | "Ark: Survival Ascended",
8 | 7777,
9 | Credentials {
10 | deployment: "ad9a8feffb3b4b2ca315546f038c3ae2",
11 | id: "xyza7891muomRmynIIHaJB9COBKkwj6n",
12 | secret: "PP5UGxysEieNfSrEicaD1N2Bb3TdXuD7xHYcsdUHZ7s",
13 | auth_by_external: false,
14 | }
15 | );
16 |
--------------------------------------------------------------------------------
/crates/lib/src/games/ffow/mod.rs:
--------------------------------------------------------------------------------
1 | /// The implementation.
2 | /// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/ffow.js)
3 | pub mod protocol;
4 | /// All types used by the implementation.
5 | pub mod types;
6 |
7 | pub use protocol::*;
8 | pub use types::*;
9 |
--------------------------------------------------------------------------------
/crates/lib/src/games/ffow/protocol.rs:
--------------------------------------------------------------------------------
1 | use crate::buffer::{Buffer, Utf8Decoder};
2 | use crate::games::ffow::types::Response;
3 | use crate::protocols::types::TimeoutSettings;
4 | use crate::protocols::valve::{Engine, Environment, Server, ValveProtocol};
5 | use crate::GDResult;
6 | use byteorder::LittleEndian;
7 | use std::net::{IpAddr, SocketAddr};
8 |
9 | pub fn query(address: &IpAddr, port: Option) -> GDResult { query_with_timeout(address, port, None) }
10 |
11 | pub fn query_with_timeout(
12 | address: &IpAddr,
13 | port: Option,
14 | timeout_settings: Option,
15 | ) -> GDResult {
16 | let mut client = ValveProtocol::new(
17 | &SocketAddr::new(*address, port.unwrap_or(5478)),
18 | timeout_settings,
19 | )?;
20 | let data = client.get_request_data(
21 | &Engine::GoldSrc(true),
22 | 0,
23 | 0x46,
24 | String::from("LSQ").into_bytes(),
25 | )?;
26 |
27 | let mut buffer = Buffer::::new(&data);
28 |
29 | let protocol_version = buffer.read::()?;
30 | let name = buffer.read_string::(None)?;
31 | let map = buffer.read_string::(None)?;
32 | let active_mod = buffer.read_string::(None)?;
33 | let game_mode = buffer.read_string::(None)?;
34 | let description = buffer.read_string::(None)?;
35 | let game_version = buffer.read_string::(None)?;
36 | buffer.move_cursor(2)?;
37 | let players_online = buffer.read::()?;
38 | let players_maximum = buffer.read::()?;
39 | let server_type = Server::from_gldsrc(buffer.read::()?)?;
40 | let environment_type = Environment::from_gldsrc(buffer.read::()?)?;
41 | let has_password = buffer.read::()? == 1;
42 | let vac_secured = buffer.read::()? == 1;
43 | buffer.move_cursor(1)?; // average fps
44 | let round = buffer.read::()?;
45 | let rounds_maximum = buffer.read::()?;
46 | let time_left = buffer.read::()?;
47 |
48 | Ok(Response {
49 | protocol_version,
50 | name,
51 | active_mod,
52 | game_mode,
53 | game_version,
54 | description,
55 | map,
56 | players_online,
57 | players_maximum,
58 | server_type,
59 | environment_type,
60 | has_password,
61 | vac_secured,
62 | round,
63 | rounds_maximum,
64 | time_left,
65 | })
66 | }
67 |
--------------------------------------------------------------------------------
/crates/lib/src/games/ffow/types.rs:
--------------------------------------------------------------------------------
1 | use crate::protocols::types::CommonResponse;
2 | use crate::protocols::valve::{Environment, Server};
3 | use crate::protocols::GenericResponse;
4 | #[cfg(feature = "serde")]
5 | use serde::{Deserialize, Serialize};
6 |
7 | /// The query response.
8 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9 | #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
10 | pub struct Response {
11 | /// Protocol used by the server.
12 | pub protocol_version: u8,
13 | /// Name of the server.
14 | pub name: String,
15 | /// Map name.
16 | pub active_mod: String,
17 | /// Running game mode.
18 | pub game_mode: String,
19 | /// The version that the server is running on.
20 | pub game_version: String,
21 | /// Description of the server.
22 | pub description: String,
23 | /// Current map.
24 | pub map: String,
25 | /// Number of players on the server.
26 | pub players_online: u8,
27 | /// Maximum number of players the server reports it can hold.
28 | pub players_maximum: u8,
29 | /// Dedicated, NonDedicated or SourceTV
30 | pub server_type: Server,
31 | /// The Operating System that the server is on.
32 | pub environment_type: Environment,
33 | /// Indicates whether the server requires a password.
34 | pub has_password: bool,
35 | /// Indicates whether the server uses VAC.
36 | pub vac_secured: bool,
37 | /// Current round index.
38 | pub round: u8,
39 | /// Maximum amount of rounds.
40 | pub rounds_maximum: u8,
41 | /// Time left for the current round in seconds.
42 | pub time_left: u16,
43 | }
44 |
45 | impl CommonResponse for Response {
46 | fn as_original(&self) -> GenericResponse { GenericResponse::FFOW(self) }
47 |
48 | fn name(&self) -> Option<&str> { Some(&self.name) }
49 | fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
50 | fn description(&self) -> Option<&str> { Some(&self.description) }
51 | fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
52 | fn map(&self) -> Option<&str> { Some(&self.map) }
53 | fn has_password(&self) -> Option { Some(self.has_password) }
54 | fn players_maximum(&self) -> u32 { self.players_maximum.into() }
55 | fn players_online(&self) -> u32 { self.players_online.into() }
56 | }
57 |
--------------------------------------------------------------------------------
/crates/lib/src/games/gamespy.rs:
--------------------------------------------------------------------------------
1 | //! Gamespy game query modules
2 |
3 | use crate::protocols::gamespy::game_query_mod;
4 |
5 | game_query_mod!(battlefield1942, "Battlefield 1942", one, 23000);
6 | game_query_mod!(crysiswars, "Crysis Wars", three, 64100);
7 | game_query_mod!(hce, "Halo: Combat Evolved", two, 2302);
8 | game_query_mod!(serioussam, "Serious Sam", one, 25601);
9 | game_query_mod!(unrealtournament, "Unreal Tournament", one, 7778);
10 |
--------------------------------------------------------------------------------
/crates/lib/src/games/jc2m/mod.rs:
--------------------------------------------------------------------------------
1 | /// The implementation.
2 | /// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/jc2mp.js)
3 | pub mod protocol;
4 | /// All types used by the implementation.
5 | pub mod types;
6 |
7 | pub use protocol::*;
8 | pub use types::*;
9 |
--------------------------------------------------------------------------------
/crates/lib/src/games/jc2m/protocol.rs:
--------------------------------------------------------------------------------
1 | use crate::buffer::{Buffer, Utf8Decoder};
2 | use crate::jc2m::{Player, Response};
3 | use crate::protocols::gamespy::common::has_password;
4 | use crate::protocols::gamespy::three::{data_to_map, GameSpy3};
5 | use crate::protocols::types::TimeoutSettings;
6 | use crate::GDErrorKind::{PacketBad, TypeParse};
7 | use crate::GDResult;
8 | use byteorder::BigEndian;
9 | use std::net::{IpAddr, SocketAddr};
10 |
11 | fn parse_players_and_teams(packet: &[u8]) -> GDResult> {
12 | let mut buf = Buffer::::new(packet);
13 |
14 | let count = buf.read::()?;
15 | let mut players = Vec::with_capacity(count as usize);
16 |
17 | while buf.remaining_length() != 0 {
18 | players.push(Player {
19 | name: buf.read_string::(None)?,
20 | steam_id: buf.read_string::(None)?,
21 | ping: buf.read::()?,
22 | });
23 | }
24 |
25 | Ok(players)
26 | }
27 |
28 | pub fn query(address: &IpAddr, port: Option) -> GDResult { query_with_timeout(address, port, None) }
29 |
30 | pub fn query_with_timeout(
31 | address: &IpAddr,
32 | port: Option,
33 | timeout_settings: Option,
34 | ) -> GDResult {
35 | let mut client = GameSpy3::new_custom(
36 | &SocketAddr::new(*address, port.unwrap_or(7777)),
37 | timeout_settings,
38 | [0xFF, 0xFF, 0xFF, 0x02],
39 | true,
40 | )?;
41 |
42 | let packets = client.get_server_packets()?;
43 | let data = packets
44 | .first()
45 | .ok_or_else(|| PacketBad.context("First packet missing"))?;
46 |
47 | let (mut server_vars, remaining_data) = data_to_map(data)?;
48 | let players = parse_players_and_teams(&remaining_data)?;
49 |
50 | let players_maximum = server_vars
51 | .remove("maxplayers")
52 | .ok_or_else(|| PacketBad.context("Server variables missing maxplayers"))?
53 | .parse()
54 | .map_err(|e| TypeParse.context(e))?;
55 | let players_online = match server_vars.remove("numplayers") {
56 | None => players.len(),
57 | Some(v) => {
58 | let reported_players = v.parse().map_err(|e| TypeParse.context(e))?;
59 | match reported_players < players.len() {
60 | true => players.len(),
61 | false => reported_players,
62 | }
63 | }
64 | } as u32;
65 |
66 | Ok(Response {
67 | game_version: server_vars.remove("version").ok_or(PacketBad)?,
68 | description: server_vars.remove("description").ok_or(PacketBad)?,
69 | name: server_vars.remove("hostname").ok_or(PacketBad)?,
70 | has_password: has_password(&mut server_vars)?,
71 | players,
72 | players_maximum,
73 | players_online,
74 | })
75 | }
76 |
--------------------------------------------------------------------------------
/crates/lib/src/games/jc2m/types.rs:
--------------------------------------------------------------------------------
1 | use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer};
2 | use crate::protocols::GenericResponse;
3 | #[cfg(feature = "serde")]
4 | use serde::{Deserialize, Serialize};
5 |
6 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
7 | #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
8 | pub struct Player {
9 | pub name: String,
10 | pub steam_id: String,
11 | pub ping: u16,
12 | }
13 |
14 | impl CommonPlayer for Player {
15 | fn as_original(&self) -> GenericPlayer { GenericPlayer::JCMP2(self) }
16 |
17 | fn name(&self) -> &str { &self.name }
18 | }
19 |
20 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
21 | #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
22 | pub struct Response {
23 | pub game_version: String,
24 | pub description: String,
25 | pub name: String,
26 | pub has_password: bool,
27 | pub players: Vec,
28 | pub players_maximum: u32,
29 | pub players_online: u32,
30 | }
31 |
32 | impl CommonResponse for Response {
33 | fn as_original(&self) -> GenericResponse { GenericResponse::JC2M(self) }
34 |
35 | fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
36 | fn description(&self) -> Option<&str> { Some(&self.description) }
37 | fn name(&self) -> Option<&str> { Some(&self.name) }
38 | fn has_password(&self) -> Option { Some(self.has_password) }
39 | fn players_maximum(&self) -> u32 { self.players_maximum }
40 | fn players_online(&self) -> u32 { self.players_online }
41 |
42 | fn players(&self) -> Option> {
43 | Some(
44 | self.players
45 | .iter()
46 | .map(|p| p as &dyn CommonPlayer)
47 | .collect(),
48 | )
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/crates/lib/src/games/mindustry/mod.rs:
--------------------------------------------------------------------------------
1 | //! Mindustry game ping (v146)
2 | //!
3 | //! [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/ArcNetProvider.java#L225-L259)
4 |
5 | use std::{net::IpAddr, net::SocketAddr};
6 |
7 | use crate::{GDResult, TimeoutSettings};
8 |
9 | use self::types::ServerData;
10 |
11 | pub mod types;
12 |
13 | pub mod protocol;
14 |
15 | /// Default mindustry server port
16 | ///
17 | /// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/Vars.java#L141-L142)
18 | pub const DEFAULT_PORT: u16 = 6567;
19 |
20 | /// Query a mindustry server.
21 | pub fn query(ip: &IpAddr, port: Option, timeout_settings: &Option) -> GDResult {
22 | let address = SocketAddr::new(*ip, port.unwrap_or(DEFAULT_PORT));
23 |
24 | protocol::query_with_retries(&address, timeout_settings)
25 | }
26 |
--------------------------------------------------------------------------------
/crates/lib/src/games/mindustry/protocol.rs:
--------------------------------------------------------------------------------
1 | use std::net::SocketAddr;
2 |
3 | use crate::{
4 | buffer::{self, Buffer},
5 | socket::{Socket, UdpSocket},
6 | utils,
7 | GDResult,
8 | TimeoutSettings,
9 | };
10 |
11 | use super::types::ServerData;
12 |
13 | /// Mindustry max datagram packet size.
14 | pub const MAX_BUFFER_SIZE: usize = 500;
15 |
16 | /// Send a ping packet.
17 | ///
18 | /// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/ArcNetProvider.java#L248)
19 | pub(crate) fn send_ping(socket: &mut UdpSocket) -> GDResult<()> { socket.send(&[-2i8 as u8, 1i8 as u8]) }
20 |
21 | /// Parse server data.
22 | ///
23 | /// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/NetworkIO.java#L122-L135)
24 | pub fn parse_server_data(
25 | buffer: &mut Buffer,
26 | ) -> GDResult {
27 | Ok(ServerData {
28 | host: buffer.read_string::(None)?,
29 | map: buffer.read_string::(None)?,
30 | players: buffer.read()?,
31 | wave: buffer.read()?,
32 | version: buffer.read()?,
33 | version_type: buffer.read_string::(None)?,
34 | gamemode: buffer.read::()?.try_into()?,
35 | player_limit: buffer.read()?,
36 | description: buffer.read_string::(None)?,
37 | mode_name: buffer.read_string::(None).ok(),
38 | })
39 | }
40 |
41 | /// Query a Mindustry server (without retries).
42 | pub fn query(address: &SocketAddr, timeout_settings: &Option) -> GDResult {
43 | let mut socket = UdpSocket::new(address, timeout_settings)?;
44 |
45 | send_ping(&mut socket)?;
46 |
47 | let socket_data = socket.receive(Some(MAX_BUFFER_SIZE))?;
48 | let mut buffer = Buffer::new(&socket_data);
49 |
50 | parse_server_data::(&mut buffer)
51 | }
52 |
53 | /// Query a Mindustry server.
54 | pub fn query_with_retries(address: &SocketAddr, timeout_settings: &Option) -> GDResult {
55 | let retries = TimeoutSettings::get_retries_or_default(timeout_settings);
56 |
57 | utils::retry_on_timeout(retries, || query(address, timeout_settings))
58 | }
59 |
--------------------------------------------------------------------------------
/crates/lib/src/games/mindustry/types.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | protocols::types::{CommonResponse, GenericResponse},
3 | GDErrorKind,
4 | };
5 | #[cfg(feature = "serde")]
6 | use serde::{Deserialize, Serialize};
7 |
8 | /// Mindustry sever data
9 | ///
10 | /// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/NetworkIO.java#L122-L135)
11 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
12 | #[derive(Debug, Clone, Eq, PartialEq)]
13 | pub struct ServerData {
14 | pub host: String,
15 | pub map: String,
16 | pub players: i32,
17 | pub wave: i32,
18 | pub version: i32,
19 | pub version_type: String,
20 | pub gamemode: GameMode,
21 | pub player_limit: i32,
22 | pub description: String,
23 | pub mode_name: Option,
24 | }
25 |
26 | /// Mindustry game mode
27 | ///
28 | /// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/game/Gamemode.java)
29 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
30 | #[derive(Debug, Clone, Eq, PartialEq)]
31 | pub enum GameMode {
32 | Survival,
33 | Sandbox,
34 | Attack,
35 | PVP,
36 | Editor,
37 | }
38 |
39 | impl TryFrom for GameMode {
40 | type Error = GDErrorKind;
41 | fn try_from(value: u8) -> Result {
42 | use GameMode::*;
43 | Ok(match value {
44 | 0 => Survival,
45 | 1 => Sandbox,
46 | 2 => Attack,
47 | 3 => PVP,
48 | 4 => Editor,
49 | _ => return Err(GDErrorKind::TypeParse),
50 | })
51 | }
52 | }
53 |
54 | impl GameMode {
55 | const fn as_str(&self) -> &'static str {
56 | match self {
57 | Self::Survival => "survival",
58 | Self::Sandbox => "sandbox",
59 | Self::Attack => "attack",
60 | Self::PVP => "pvp",
61 | Self::Editor => "editor",
62 | }
63 | }
64 | }
65 |
66 | impl CommonResponse for ServerData {
67 | fn as_original(&self) -> GenericResponse { GenericResponse::Mindustry(self) }
68 |
69 | fn players_online(&self) -> u32 { self.players.try_into().unwrap_or(0) }
70 | fn players_maximum(&self) -> u32 { self.player_limit.try_into().unwrap_or(0) }
71 |
72 | fn game_mode(&self) -> Option<&str> { Some(self.gamemode.as_str()) }
73 |
74 | fn map(&self) -> Option<&str> { Some(&self.map) }
75 | fn description(&self) -> Option<&str> { Some(&self.description) }
76 | }
77 |
78 | #[cfg(test)]
79 | mod test {
80 | use crate::protocols::types::CommonResponse;
81 |
82 | use super::ServerData;
83 |
84 | #[test]
85 | fn common_impl() {
86 | let data = ServerData {
87 | host: String::from("host"),
88 | map: String::from("map"),
89 | players: 5,
90 | wave: 2,
91 | version: 142,
92 | version_type: String::from("steam"),
93 | gamemode: super::GameMode::PVP,
94 | player_limit: 20,
95 | description: String::from("description"),
96 | mode_name: Some(String::from("campaign")),
97 | };
98 |
99 | let common: &dyn CommonResponse = &data;
100 |
101 | assert_eq!(common.players_online(), 5);
102 | assert_eq!(common.players_maximum(), 20);
103 | assert_eq!(common.game_mode(), Some("pvp"));
104 | assert_eq!(common.map(), Some("map"));
105 | assert_eq!(common.description(), Some("description"));
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/crates/lib/src/games/minecraft/mod.rs:
--------------------------------------------------------------------------------
1 | /// The implementation.
2 | /// Reference: [Server List Ping](https://wiki.vg/Server_List_Ping)
3 | pub mod protocol;
4 | /// All types used by the implementation.
5 | pub mod types;
6 |
7 | #[allow(unused_imports)]
8 | pub use protocol::*;
9 | pub use types::*;
10 |
11 | use crate::{GDErrorKind, GDResult};
12 | use std::net::{IpAddr, SocketAddr};
13 |
14 | /// Query with all the protocol variants one by one (Java -> Bedrock -> Legacy
15 | /// (1.6 -> 1.4 -> Beta 1.8)).
16 | pub fn query(address: &IpAddr, port: Option) -> GDResult {
17 | if let Ok(response) = query_java(address, port, None) {
18 | return Ok(response);
19 | }
20 |
21 | if let Ok(response) = query_bedrock(address, port) {
22 | return Ok(JavaResponse::from_bedrock_response(response));
23 | }
24 |
25 | if let Ok(response) = query_legacy(address, port) {
26 | return Ok(response);
27 | }
28 |
29 | Err(GDErrorKind::AutoQuery.into())
30 | }
31 |
32 | /// Query a Java Server.
33 | pub fn query_java(
34 | address: &IpAddr,
35 | port: Option,
36 | request_settings: Option,
37 | ) -> GDResult {
38 | protocol::query_java(
39 | &SocketAddr::new(*address, port_or_java_default(port)),
40 | None,
41 | request_settings,
42 | )
43 | }
44 |
45 | /// Query a (Java) Legacy Server (1.6 -> 1.4 -> Beta 1.8).
46 | pub fn query_legacy(address: &IpAddr, port: Option) -> GDResult {
47 | protocol::query_legacy(&SocketAddr::new(*address, port_or_java_default(port)), None)
48 | }
49 |
50 | /// Query a specific (Java) Legacy Server.
51 | pub fn query_legacy_specific(group: LegacyGroup, address: &IpAddr, port: Option) -> GDResult {
52 | protocol::query_legacy_specific(
53 | group,
54 | &SocketAddr::new(*address, port_or_java_default(port)),
55 | None,
56 | )
57 | }
58 |
59 | /// Query a Bedrock Server.
60 | pub fn query_bedrock(address: &IpAddr, port: Option) -> GDResult {
61 | protocol::query_bedrock(
62 | &SocketAddr::new(*address, port_or_bedrock_default(port)),
63 | None,
64 | )
65 | }
66 |
67 | fn port_or_java_default(port: Option) -> u16 { port.unwrap_or(25565) }
68 |
69 | fn port_or_bedrock_default(port: Option) -> u16 { port.unwrap_or(19132) }
70 |
--------------------------------------------------------------------------------
/crates/lib/src/games/minecraft/protocol/bedrock.rs:
--------------------------------------------------------------------------------
1 | // This file has code that has been documented by the NodeJS GameDig library
2 | // (MIT) from https://github.com/gamedig/node-gamedig/blob/master/protocols/minecraftbedrock.js
3 | use crate::{
4 | buffer::{Buffer, Utf8Decoder},
5 | games::minecraft::{BedrockResponse, GameMode, Server},
6 | protocols::types::TimeoutSettings,
7 | socket::{Socket, UdpSocket},
8 | utils::{error_by_expected_size, retry_on_timeout},
9 | GDErrorKind::{PacketBad, TypeParse},
10 | GDResult,
11 | };
12 |
13 | use std::net::SocketAddr;
14 |
15 | use byteorder::LittleEndian;
16 |
17 | pub struct Bedrock {
18 | socket: UdpSocket,
19 | retry_count: usize,
20 | }
21 |
22 | impl Bedrock {
23 | fn new(address: &SocketAddr, timeout_settings: Option) -> GDResult {
24 | let socket = UdpSocket::new(address, &timeout_settings)?;
25 |
26 | let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
27 | Ok(Self {
28 | socket,
29 | retry_count,
30 | })
31 | }
32 |
33 | fn send_status_request(&mut self) -> GDResult<()> {
34 | self.socket.send(&[
35 | 0x01, // Message ID: ID_UNCONNECTED_PING
36 | 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, // Nonce / timestamp
37 | 0x00, 0xff, 0xff, 0x00, 0xfe, 0xfe, 0xfe, 0xfe, 0xfd, 0xfd, 0xfd, 0xfd, 0x12, 0x34, // Magic
38 | 0x56, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Client GUID
39 | ])?;
40 |
41 | Ok(())
42 | }
43 |
44 | /// Send a status request, and parse the response.
45 | /// This function will retry fetch on timeouts.
46 | fn get_info(&mut self) -> GDResult {
47 | retry_on_timeout(self.retry_count, move || self.get_info_impl())
48 | }
49 |
50 | /// Send a status request, and parse the response (without retry logic).
51 | fn get_info_impl(&mut self) -> GDResult {
52 | self.send_status_request()?;
53 |
54 | let received = self.socket.receive(None)?;
55 | let mut buffer = Buffer::::new(&received);
56 |
57 | if buffer.read::()? != 0x1c {
58 | return Err(PacketBad.context("Expected 0x1c"));
59 | }
60 |
61 | // Checking for our nonce directly from a u64 (as the nonce is 8 bytes).
62 | if buffer.read::()? != 9_833_440_827_789_222_417 {
63 | return Err(PacketBad.context("Invalid nonce"));
64 | }
65 |
66 | // These 8 bytes are identical to the serverId string we receive in decimal
67 | // below
68 | buffer.move_cursor(8)?;
69 |
70 | // Verifying the magic value (as we need 16 bytes, cast to two u64 values)
71 | if buffer.read::()? != 18_374_403_896_610_127_616 {
72 | return Err(PacketBad.context("Invalid magic"));
73 | }
74 |
75 | if buffer.read::()? != 8_671_175_388_723_805_693 {
76 | return Err(PacketBad.context("Invalid magic"));
77 | }
78 |
79 | let remaining_length = buffer.switch_endian_chunk(2)?.read::()? as usize;
80 |
81 | error_by_expected_size(remaining_length, buffer.remaining_length())?;
82 |
83 | let binding = buffer.read_string::(None)?;
84 | let status: Vec<&str> = binding.split(';').collect();
85 |
86 | // We must have at least 6 values
87 | if status.len() < 6 {
88 | return Err(PacketBad.context("Not enough values"));
89 | }
90 |
91 | Ok(BedrockResponse {
92 | edition: status[0].to_string(),
93 | name: status[1].to_string(),
94 | version_name: status[3].to_string(),
95 | protocol_version: status[2].to_string(),
96 | players_maximum: status[5].parse().map_err(|e| TypeParse.context(e))?,
97 | players_online: status[4].parse().map_err(|e| TypeParse.context(e))?,
98 | id: status.get(6).map(std::string::ToString::to_string),
99 | map: status.get(7).map(std::string::ToString::to_string),
100 | game_mode: match status.get(8) {
101 | None => None,
102 | Some(v) => Some(GameMode::from_bedrock(v)?),
103 | },
104 | server_type: Server::Bedrock,
105 | })
106 | }
107 |
108 | pub fn query(address: &SocketAddr, timeout_settings: Option) -> GDResult {
109 | Self::new(address, timeout_settings)?.get_info()
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/crates/lib/src/games/minecraft/protocol/java.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | buffer::Buffer,
3 | games::minecraft::{as_string, as_varint, get_string, get_varint, JavaResponse, Player, RequestSettings, Server},
4 | protocols::types::TimeoutSettings,
5 | socket::{Socket, TcpSocket},
6 | utils::retry_on_timeout,
7 | GDErrorKind::{JsonParse, PacketBad},
8 | GDResult,
9 | };
10 |
11 | use byteorder::LittleEndian;
12 | use serde_json::Value;
13 | use std::net::SocketAddr;
14 |
15 | pub struct Java {
16 | socket: TcpSocket,
17 | request_settings: RequestSettings,
18 | retry_count: usize,
19 | }
20 |
21 | impl Java {
22 | fn new(
23 | address: &SocketAddr,
24 | timeout_settings: Option,
25 | request_settings: Option,
26 | ) -> GDResult {
27 | let socket = TcpSocket::new(address, &timeout_settings)?;
28 |
29 | let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
30 | Ok(Self {
31 | socket,
32 | request_settings: request_settings.unwrap_or_default(),
33 | retry_count,
34 | })
35 | }
36 |
37 | fn send(&mut self, data: Vec) -> GDResult<()> {
38 | self.socket
39 | .send(&[as_varint(data.len() as i32), data].concat())
40 | }
41 |
42 | fn receive(&mut self) -> GDResult> {
43 | let data = &self.socket.receive(None)?;
44 | let mut buffer = Buffer::::new(data);
45 |
46 | let _packet_length = get_varint(&mut buffer)? as usize;
47 | // this declared 'packet length' from within the packet might be wrong (?), not
48 | // checking with it...
49 |
50 | Ok(buffer.remaining_bytes().to_vec())
51 | }
52 |
53 | fn send_handshake(&mut self) -> GDResult<()> {
54 | let handshake_payload = [
55 | &[
56 | // Packet ID (0)
57 | 0x00,
58 | ], // Protocol Version (-1 to determine version)
59 | as_varint(self.request_settings.protocol_version).as_slice(),
60 | // Server address (can be anything)
61 | as_string(&self.request_settings.hostname)?.as_slice(),
62 | // Server port (can be anything)
63 | &self.socket.port().to_le_bytes(),
64 | &[
65 | // Next state (1 for status)
66 | 0x01,
67 | ],
68 | ]
69 | .concat();
70 |
71 | self.send(handshake_payload)?;
72 |
73 | Ok(())
74 | }
75 |
76 | fn send_status_request(&mut self) -> GDResult<()> {
77 | self.send(
78 | [0x00] // Packet ID (0)
79 | .to_vec(),
80 | )?;
81 |
82 | Ok(())
83 | }
84 |
85 | fn send_ping_request(&mut self) -> GDResult<()> {
86 | self.send(
87 | [0x01] // Packet ID (1)
88 | .to_vec(),
89 | )?;
90 |
91 | Ok(())
92 | }
93 |
94 | /// Send minecraft ping request and parse the response.
95 | /// This function will retry fetch on timeouts.
96 | fn get_info(&mut self) -> GDResult {
97 | retry_on_timeout(self.retry_count, move || self.get_info_impl())
98 | }
99 |
100 | /// Send minecraft ping request and parse the response (without retry
101 | /// logic).
102 | fn get_info_impl(&mut self) -> GDResult {
103 | self.send_handshake()?;
104 | self.send_status_request()?;
105 | self.send_ping_request()?;
106 |
107 | let socket_data = self.receive()?;
108 | let mut buffer = Buffer::::new(&socket_data);
109 |
110 | if get_varint(&mut buffer)? != 0 {
111 | // first var int is the packet id
112 | return Err(PacketBad.context("Expected 0"));
113 | }
114 |
115 | let json_response = get_string(&mut buffer)?;
116 | let value_response: Value = serde_json::from_str(&json_response).map_err(|e| JsonParse.context(e))?;
117 |
118 | let game_version = value_response["version"]["name"]
119 | .as_str()
120 | .ok_or(PacketBad)?
121 | .to_string();
122 | let protocol_version = value_response["version"]["protocol"]
123 | .as_i64()
124 | .ok_or(PacketBad)? as i32;
125 |
126 | let max_players = value_response["players"]["max"].as_u64().ok_or(PacketBad)? as u32;
127 | let online_players = value_response["players"]["online"]
128 | .as_u64()
129 | .ok_or(PacketBad)? as u32;
130 | let players: Option> = match value_response["players"]["sample"].is_null() {
131 | true => None,
132 | false => {
133 | Some({
134 | let players_values = value_response["players"]["sample"]
135 | .as_array()
136 | .ok_or(PacketBad)?;
137 |
138 | let mut players = Vec::with_capacity(players_values.len());
139 | for player in players_values {
140 | players.push(Player {
141 | name: player["name"].as_str().ok_or(PacketBad)?.to_string(),
142 | id: player["id"].as_str().ok_or(PacketBad)?.to_string(),
143 | });
144 | }
145 |
146 | players
147 | })
148 | }
149 | };
150 |
151 | Ok(JavaResponse {
152 | game_version,
153 | protocol_version,
154 | players_maximum: max_players,
155 | players_online: online_players,
156 | players,
157 | description: value_response["description"].to_string(),
158 | favicon: value_response["favicon"].as_str().map(str::to_string),
159 | previews_chat: value_response["previewsChat"].as_bool(),
160 | enforces_secure_chat: value_response["enforcesSecureChat"].as_bool(),
161 | server_type: Server::Java,
162 | })
163 | }
164 |
165 | pub fn query(
166 | address: &SocketAddr,
167 | timeout_settings: Option,
168 | request_settings: Option,
169 | ) -> GDResult {
170 | Self::new(address, timeout_settings, request_settings)?.get_info()
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/crates/lib/src/games/minecraft/protocol/legacy_v1_4.rs:
--------------------------------------------------------------------------------
1 | use byteorder::BigEndian;
2 |
3 | use crate::minecraft::protocol::legacy_v1_6::LegacyV1_6;
4 | use crate::{
5 | buffer::{Buffer, Utf16Decoder},
6 | games::minecraft::{JavaResponse, LegacyGroup, Server},
7 | protocols::types::TimeoutSettings,
8 | socket::{Socket, TcpSocket},
9 | utils::{error_by_expected_size, retry_on_timeout},
10 | GDErrorKind::{PacketBad, ProtocolFormat},
11 | GDResult,
12 | };
13 | use std::net::SocketAddr;
14 |
15 | pub struct LegacyV1_4 {
16 | socket: TcpSocket,
17 | retry_count: usize,
18 | }
19 |
20 | impl LegacyV1_4 {
21 | fn new(address: &SocketAddr, timeout_settings: Option) -> GDResult {
22 | let socket = TcpSocket::new(address, &timeout_settings)?;
23 |
24 | let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
25 | Ok(Self {
26 | socket,
27 | retry_count,
28 | })
29 | }
30 |
31 | fn send_initial_request(&mut self) -> GDResult<()> { self.socket.send(&[0xFE, 0x01]) }
32 |
33 | /// Send info request and parse response.
34 | /// This function will retry fetch on timeouts.
35 | fn get_info(&mut self) -> GDResult {
36 | retry_on_timeout(self.retry_count, move || self.get_info_impl())
37 | }
38 |
39 | /// Send info request and parse response (without retry logic).
40 | fn get_info_impl(&mut self) -> GDResult {
41 | self.send_initial_request()?;
42 |
43 | let data = self.socket.receive(None)?;
44 | let mut buffer = Buffer::::new(&data);
45 |
46 | if buffer.read::()? != 0xFF {
47 | return Err(ProtocolFormat.context("Expected 0xFF"));
48 | }
49 |
50 | let length = buffer.read::()? * 2;
51 | error_by_expected_size((length + 3) as usize, data.len())?;
52 |
53 | if LegacyV1_6::is_protocol(&mut buffer)? {
54 | return LegacyV1_6::get_response(&mut buffer);
55 | }
56 |
57 | let packet_string = buffer.read_string::>(None)?;
58 |
59 | let split: Vec<&str> = packet_string.split('§').collect();
60 | error_by_expected_size(3, split.len())?;
61 |
62 | let description = split[0].to_string();
63 | let online_players = split[1].parse().map_err(|e| PacketBad.context(e))?;
64 | let max_players = split[2].parse().map_err(|e| PacketBad.context(e))?;
65 |
66 | Ok(JavaResponse {
67 | game_version: "1.4+".to_string(),
68 | protocol_version: -1,
69 | players_maximum: max_players,
70 | players_online: online_players,
71 | players: None,
72 | description,
73 | favicon: None,
74 | previews_chat: None,
75 | enforces_secure_chat: None,
76 | server_type: Server::Legacy(LegacyGroup::V1_4),
77 | })
78 | }
79 |
80 | pub fn query(address: &SocketAddr, timeout_settings: Option) -> GDResult {
81 | Self::new(address, timeout_settings)?.get_info()
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/crates/lib/src/games/minecraft/protocol/legacy_v1_6.rs:
--------------------------------------------------------------------------------
1 | use byteorder::BigEndian;
2 |
3 | use crate::{
4 | buffer::{Buffer, Utf16Decoder},
5 | games::minecraft::{JavaResponse, LegacyGroup, Server},
6 | protocols::types::TimeoutSettings,
7 | socket::{Socket, TcpSocket},
8 | utils::{error_by_expected_size, retry_on_timeout},
9 | GDErrorKind::{PacketBad, ProtocolFormat},
10 | GDResult,
11 | };
12 | use std::net::SocketAddr;
13 |
14 | pub struct LegacyV1_6 {
15 | socket: TcpSocket,
16 | retry_count: usize,
17 | }
18 |
19 | impl LegacyV1_6 {
20 | fn new(address: &SocketAddr, timeout_settings: Option) -> GDResult {
21 | let socket = TcpSocket::new(address, &timeout_settings)?;
22 |
23 | let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
24 | Ok(Self {
25 | socket,
26 | retry_count,
27 | })
28 | }
29 |
30 | fn send_initial_request(&mut self) -> GDResult<()> {
31 | self.socket.send(&[
32 | 0xfe, // Packet ID (FE)
33 | 0x01, // Ping payload (01)
34 | 0xfa, // Packet identifier for plugin message
35 | 0x00, 0x07, // Length of 'GameDig' string (7) as unsigned short
36 | 0x00, 0x47, 0x00, 0x61, 0x00, 0x6D, 0x00, 0x65, 0x00, 0x44, 0x00, 0x69, 0x00,
37 | 0x67, // 'GameDig' string as UTF-16BE
38 | ])?;
39 |
40 | Ok(())
41 | }
42 |
43 | pub(crate) fn is_protocol(buffer: &mut Buffer) -> GDResult {
44 | let state = buffer
45 | .remaining_bytes()
46 | .starts_with(&[0x00, 0xA7, 0x00, 0x31, 0x00, 0x00]);
47 |
48 | if state {
49 | buffer.move_cursor(6)?;
50 | }
51 |
52 | Ok(state)
53 | }
54 |
55 | pub(crate) fn get_response(buffer: &mut Buffer) -> GDResult {
56 | // This is a specific order!
57 | let protocol_version = buffer
58 | .read_string::>(None)?
59 | .parse()
60 | .map_err(|e| PacketBad.context(e))?;
61 | let game_version = buffer.read_string::>(None)?;
62 | let description = buffer.read_string::>(None)?;
63 | let online_players = buffer
64 | .read_string::>(None)?
65 | .parse()
66 | .map_err(|e| PacketBad.context(e))?;
67 | let max_players = buffer
68 | .read_string::>(None)?
69 | .parse()
70 | .map_err(|e| PacketBad.context(e))?;
71 |
72 | Ok(JavaResponse {
73 | game_version,
74 | protocol_version,
75 | players_maximum: max_players,
76 | players_online: online_players,
77 | players: None,
78 | description,
79 | favicon: None,
80 | previews_chat: None,
81 | enforces_secure_chat: None,
82 | server_type: Server::Legacy(LegacyGroup::V1_6),
83 | })
84 | }
85 |
86 | /// Send info request and parse response.
87 | /// This function will retry fetch on timeouts.
88 | fn get_info(&mut self) -> GDResult {
89 | retry_on_timeout(self.retry_count, move || self.get_info_impl())
90 | }
91 |
92 | /// Send info request and parse response (without retry logic).
93 | fn get_info_impl(&mut self) -> GDResult {
94 | self.send_initial_request()?;
95 |
96 | let data = self.socket.receive(None)?;
97 | let mut buffer = Buffer::::new(&data);
98 |
99 | if buffer.read::()? != 0xFF {
100 | return Err(ProtocolFormat.context("Expected 0xFF"));
101 | }
102 |
103 | let length = buffer.read::()? * 2;
104 | error_by_expected_size((length + 3) as usize, data.len())?;
105 |
106 | if !Self::is_protocol(&mut buffer)? {
107 | return Err(ProtocolFormat.context("Not legacy 1.6 protocol"));
108 | }
109 |
110 | Self::get_response(&mut buffer)
111 | }
112 |
113 | pub fn query(address: &SocketAddr, timeout_settings: Option) -> GDResult {
114 | Self::new(address, timeout_settings)?.get_info()
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/crates/lib/src/games/minecraft/protocol/legacy_vb1_8.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | buffer::{Buffer, Utf16Decoder},
3 | games::minecraft::{JavaResponse, LegacyGroup, Server},
4 | protocols::types::TimeoutSettings,
5 | socket::{Socket, TcpSocket},
6 | utils::{error_by_expected_size, retry_on_timeout},
7 | GDErrorKind::{PacketBad, ProtocolFormat},
8 | GDResult,
9 | };
10 |
11 | use std::net::SocketAddr;
12 |
13 | use byteorder::BigEndian;
14 |
15 | pub struct LegacyVB1_8 {
16 | socket: TcpSocket,
17 | retry_count: usize,
18 | }
19 |
20 | impl LegacyVB1_8 {
21 | fn new(address: &SocketAddr, timeout_settings: Option) -> GDResult {
22 | let socket = TcpSocket::new(address, &timeout_settings)?;
23 |
24 | let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
25 | Ok(Self {
26 | socket,
27 | retry_count,
28 | })
29 | }
30 |
31 | fn send_initial_request(&mut self) -> GDResult<()> { self.socket.send(&[0xFE]) }
32 |
33 | /// Send request for info and parse response.
34 | /// This function will retry fetch on timeouts.
35 | fn get_info(&mut self) -> GDResult {
36 | retry_on_timeout(self.retry_count, move || self.get_info_impl())
37 | }
38 |
39 | /// Send request for info and parse response (without retry logic).
40 | fn get_info_impl(&mut self) -> GDResult {
41 | self.send_initial_request()?;
42 |
43 | let data = self.socket.receive(None)?;
44 | let mut buffer = Buffer::::new(&data);
45 |
46 | if buffer.read::()? != 0xFF {
47 | return Err(ProtocolFormat.context("Expected 0xFF"));
48 | }
49 |
50 | let length = buffer.read::()? * 2;
51 | error_by_expected_size((length + 3) as usize, data.len())?;
52 |
53 | let packet_string = buffer.read_string::>(None)?;
54 |
55 | let split: Vec<&str> = packet_string.split('§').collect();
56 | error_by_expected_size(3, split.len())?;
57 |
58 | let description = split[0].to_string();
59 | let online_players = split[1].parse().map_err(|e| PacketBad.context(e))?;
60 | let max_players = split[2].parse().map_err(|e| PacketBad.context(e))?;
61 |
62 | Ok(JavaResponse {
63 | game_version: "Beta 1.8+".to_string(),
64 | protocol_version: -1,
65 | players_maximum: max_players,
66 | players_online: online_players,
67 | players: None,
68 | description,
69 | favicon: None,
70 | previews_chat: None,
71 | enforces_secure_chat: None,
72 | server_type: Server::Legacy(LegacyGroup::VB1_8),
73 | })
74 | }
75 |
76 | pub fn query(address: &SocketAddr, timeout_settings: Option) -> GDResult {
77 | Self::new(address, timeout_settings)?.get_info()
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/crates/lib/src/games/minecraft/protocol/mod.rs:
--------------------------------------------------------------------------------
1 | use crate::games::minecraft::types::RequestSettings;
2 | use crate::{
3 | games::minecraft::{
4 | protocol::{
5 | bedrock::Bedrock,
6 | java::Java,
7 | legacy_v1_4::LegacyV1_4,
8 | legacy_v1_6::LegacyV1_6,
9 | legacy_vb1_8::LegacyVB1_8,
10 | },
11 | BedrockResponse,
12 | JavaResponse,
13 | LegacyGroup,
14 | },
15 | protocols::types::TimeoutSettings,
16 | GDErrorKind::AutoQuery,
17 | GDResult,
18 | };
19 | use std::net::SocketAddr;
20 |
21 | mod bedrock;
22 | mod java;
23 | mod legacy_v1_4;
24 | mod legacy_v1_6;
25 | mod legacy_vb1_8;
26 |
27 | /// Queries a Minecraft server with all the protocol variants one by one (Java
28 | /// -> Bedrock -> Legacy (1.6 -> 1.4 -> Beta 1.8)).
29 | pub fn query(
30 | address: &SocketAddr,
31 | timeout_settings: Option,
32 | request_settings: Option,
33 | ) -> GDResult {
34 | if let Ok(response) = query_java(address, timeout_settings, request_settings) {
35 | return Ok(response);
36 | }
37 |
38 | if let Ok(response) = query_bedrock(address, timeout_settings) {
39 | return Ok(JavaResponse::from_bedrock_response(response));
40 | }
41 |
42 | if let Ok(response) = query_legacy(address, timeout_settings) {
43 | return Ok(response);
44 | }
45 |
46 | Err(AutoQuery.into())
47 | }
48 |
49 | /// Query a Java Server.
50 | pub fn query_java(
51 | address: &SocketAddr,
52 | timeout_settings: Option,
53 | request_settings: Option,
54 | ) -> GDResult {
55 | Java::query(address, timeout_settings, request_settings)
56 | }
57 |
58 | /// Query a (Java) Legacy Server (1.6 -> 1.4 -> Beta 1.8).
59 | pub fn query_legacy(address: &SocketAddr, timeout_settings: Option) -> GDResult {
60 | if let Ok(response) = query_legacy_specific(LegacyGroup::V1_6, address, timeout_settings) {
61 | return Ok(response);
62 | }
63 |
64 | if let Ok(response) = query_legacy_specific(LegacyGroup::V1_4, address, timeout_settings) {
65 | return Ok(response);
66 | }
67 |
68 | if let Ok(response) = query_legacy_specific(LegacyGroup::VB1_8, address, timeout_settings) {
69 | return Ok(response);
70 | }
71 |
72 | Err(AutoQuery.into())
73 | }
74 |
75 | /// Query a specific (Java) Legacy Server.
76 | pub fn query_legacy_specific(
77 | group: LegacyGroup,
78 | address: &SocketAddr,
79 | timeout_settings: Option,
80 | ) -> GDResult {
81 | match group {
82 | LegacyGroup::V1_6 => LegacyV1_6::query(address, timeout_settings),
83 | LegacyGroup::V1_4 => LegacyV1_4::query(address, timeout_settings),
84 | LegacyGroup::VB1_8 => LegacyVB1_8::query(address, timeout_settings),
85 | }
86 | }
87 |
88 | /// Query a Bedrock Server.
89 | pub fn query_bedrock(address: &SocketAddr, timeout_settings: Option) -> GDResult {
90 | Bedrock::query(address, timeout_settings)
91 | }
92 |
--------------------------------------------------------------------------------
/crates/lib/src/games/minetest/mod.rs:
--------------------------------------------------------------------------------
1 | /// The implementation.
2 | /// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/minetest.js)
3 | pub mod protocol;
4 | /// All types used by the implementation.
5 | pub mod types;
6 |
7 | pub use protocol::*;
8 | pub use types::*;
9 |
--------------------------------------------------------------------------------
/crates/lib/src/games/minetest/protocol.rs:
--------------------------------------------------------------------------------
1 | use crate::minetest::Response;
2 | use crate::{minetest_master_server, GDErrorKind, GDResult, TimeoutSettings};
3 | use std::net::IpAddr;
4 |
5 | pub fn query(address: &IpAddr, port: Option) -> GDResult { query_with_timeout(address, port, &None) }
6 |
7 | pub fn query_with_timeout(
8 | address: &IpAddr,
9 | port: Option,
10 | timeout_settings: &Option,
11 | ) -> GDResult {
12 | let address = address.to_string();
13 | let port = port.unwrap_or(30000);
14 |
15 | let servers = minetest_master_server::query(timeout_settings.unwrap_or_default())?;
16 | for server in servers.list {
17 | if server.ip == address && server.port == port {
18 | return Ok(server.into());
19 | }
20 | }
21 |
22 | Err(GDErrorKind::AutoQuery.context("Server not found in the master query list."))
23 | }
24 |
--------------------------------------------------------------------------------
/crates/lib/src/games/minetest/types.rs:
--------------------------------------------------------------------------------
1 | use crate::minetest_master_server::Server;
2 | use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer};
3 | use crate::protocols::GenericResponse;
4 | use serde::{Deserialize, Serialize};
5 |
6 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
7 | pub struct Player {
8 | pub name: String,
9 | }
10 |
11 | impl CommonPlayer for Player {
12 | fn as_original(&self) -> GenericPlayer { GenericPlayer::Minetest(self) }
13 |
14 | fn name(&self) -> &str { &self.name }
15 | }
16 |
17 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
18 | pub struct Response {
19 | pub name: String,
20 | pub description: String,
21 | pub game_version: String,
22 | pub players_maximum: u32,
23 | pub players_online: u32,
24 | pub has_password: Option,
25 | pub players: Vec,
26 | pub id: String,
27 | pub ip: String,
28 | pub port: u16,
29 | pub creative: Option,
30 | pub damage: bool,
31 | pub game_time: u32,
32 | pub lag: Option,
33 | pub proto_max: u16,
34 | pub proto_min: u16,
35 | pub pvp: bool,
36 | pub uptime: u32,
37 | pub url: Option,
38 | pub update_time: u32,
39 | pub start: u32,
40 | pub clients_top: u32,
41 | pub updates: u32,
42 | pub pop_v: f32,
43 | pub geo_continent: Option,
44 | pub ping: f32,
45 | }
46 |
47 | impl From for Response {
48 | fn from(server: Server) -> Self {
49 | Self {
50 | name: server.name,
51 | description: server.description,
52 | game_version: server.version,
53 | players_maximum: server.clients_max,
54 | players_online: server.total_clients,
55 | has_password: server.password,
56 | players: server
57 | .clients_list
58 | .unwrap_or_default()
59 | .into_iter()
60 | .map(|name| Player { name })
61 | .collect(),
62 | ip: server.address,
63 | creative: server.creative,
64 | damage: server.damage,
65 | game_time: server.game_time,
66 | id: server.gameid,
67 | lag: server.lag,
68 | port: server.port,
69 | proto_max: server.proto_max,
70 | proto_min: server.proto_min,
71 | pvp: server.pvp,
72 | uptime: server.uptime,
73 | url: server.url,
74 | update_time: server.update_time,
75 | start: server.start,
76 | clients_top: server.clients_top,
77 | updates: server.updates,
78 | pop_v: server.pop_v,
79 | geo_continent: server.geo_continent,
80 | ping: server.ping,
81 | }
82 | }
83 | }
84 |
85 | impl CommonResponse for Response {
86 | fn as_original(&self) -> GenericResponse { GenericResponse::Minetest(self) }
87 |
88 | fn name(&self) -> Option<&str> { Some(&self.name) }
89 |
90 | fn description(&self) -> Option<&str> { Some(&self.description) }
91 |
92 | fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
93 |
94 | fn players_maximum(&self) -> u32 { self.players_maximum }
95 |
96 | fn players_online(&self) -> u32 { self.players_online }
97 |
98 | fn has_password(&self) -> Option { self.has_password }
99 |
100 | fn players(&self) -> Option> {
101 | Some(
102 | self.players
103 | .iter()
104 | .map(|p| p as &dyn CommonPlayer)
105 | .collect(),
106 | )
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/crates/lib/src/games/mod.rs:
--------------------------------------------------------------------------------
1 | //! Currently supported games.
2 |
3 | #[cfg(feature = "tls")]
4 | pub mod epic;
5 | pub mod gamespy;
6 | pub mod quake;
7 | pub mod unreal2;
8 | pub mod valve;
9 |
10 | #[cfg(all(feature = "tls", feature = "serde", feature = "services"))]
11 | pub mod minetest;
12 |
13 | #[cfg(feature = "tls")]
14 | pub use epic::*;
15 | pub use gamespy::*;
16 | pub use quake::*;
17 | pub use unreal2::*;
18 | pub use valve::*;
19 |
20 | #[cfg(all(feature = "tls", feature = "serde", feature = "services"))]
21 | pub use minetest::*;
22 |
23 | /// Battalion 1944
24 | pub mod battalion1944;
25 | /// Eco
26 | pub mod eco;
27 | /// Frontlines: Fuel of War
28 | pub mod ffow;
29 | /// Just Cause 2: Multiplayer
30 | pub mod jc2m;
31 | /// Mindustry
32 | pub mod mindustry;
33 | /// Minecraft
34 | pub mod minecraft;
35 | /// Savage 2
36 | pub mod savage2;
37 | /// The Ship
38 | pub mod theship;
39 |
40 | pub mod types;
41 | pub use types::*;
42 |
43 | pub mod query;
44 | pub use query::*;
45 |
46 | #[cfg(feature = "game_defs")]
47 | mod definitions;
48 |
49 | #[cfg(feature = "game_defs")]
50 | pub use definitions::GAMES;
51 |
--------------------------------------------------------------------------------
/crates/lib/src/games/quake.rs:
--------------------------------------------------------------------------------
1 | //! Quake game query modules
2 |
3 | use crate::protocols::quake::game_query_mod;
4 |
5 | game_query_mod!(quake1, "Quake 1", one, 27500);
6 | game_query_mod!(quake2, "Quake 2", two, 27910);
7 | game_query_mod!(q3a, "Quake 3 Arena", three, 27960);
8 | game_query_mod!(sof2, "Soldier of Fortune 2", three, 20100);
9 | game_query_mod!(warsow, "Warsow", three, 44400);
10 |
--------------------------------------------------------------------------------
/crates/lib/src/games/query.rs:
--------------------------------------------------------------------------------
1 | //! Generic query functions
2 |
3 | use std::net::{IpAddr, SocketAddr};
4 |
5 | #[cfg(all(feature = "services", feature = "tls", feature = "serde"))]
6 | use crate::games::minetest;
7 | use crate::games::types::Game;
8 | use crate::games::{eco, ffow, jc2m, mindustry, minecraft, savage2, theship};
9 | use crate::protocols;
10 | use crate::protocols::gamespy::GameSpyVersion;
11 | use crate::protocols::quake::QuakeVersion;
12 | use crate::protocols::types::{CommonResponse, ExtraRequestSettings, ProprietaryProtocol, Protocol, TimeoutSettings};
13 | use crate::GDResult;
14 |
15 | /// Make a query given a game definition
16 | #[inline]
17 | pub fn query(game: &Game, address: &IpAddr, port: Option) -> GDResult> {
18 | query_with_timeout_and_extra_settings(game, address, port, None, None)
19 | }
20 |
21 | /// Make a query given a game definition and timeout settings
22 | #[inline]
23 | pub fn query_with_timeout(
24 | game: &Game,
25 | address: &IpAddr,
26 | port: Option,
27 | timeout_settings: Option,
28 | ) -> GDResult> {
29 | query_with_timeout_and_extra_settings(game, address, port, timeout_settings, None)
30 | }
31 |
32 | /// Make a query given a game definition, timeout settings, and extra settings
33 | pub fn query_with_timeout_and_extra_settings(
34 | game: &Game,
35 | address: &IpAddr,
36 | port: Option,
37 | timeout_settings: Option,
38 | extra_settings: Option,
39 | ) -> GDResult> {
40 | let socket_addr = SocketAddr::new(*address, port.unwrap_or(game.default_port));
41 | Ok(match &game.protocol {
42 | Protocol::Valve(engine) => {
43 | protocols::valve::query(
44 | &socket_addr,
45 | *engine,
46 | extra_settings
47 | .or_else(|| Option::from(game.request_settings.clone()))
48 | .map(ExtraRequestSettings::into),
49 | timeout_settings,
50 | )
51 | .map(Box::new)?
52 | }
53 | #[cfg(feature = "tls")]
54 | Protocol::Epic(credentials) => {
55 | protocols::epic::query_with_timeout(credentials.clone(), &socket_addr, timeout_settings).map(Box::new)?
56 | }
57 | Protocol::Gamespy(version) => {
58 | match version {
59 | GameSpyVersion::One => protocols::gamespy::one::query(&socket_addr, timeout_settings).map(Box::new)?,
60 | GameSpyVersion::Two => protocols::gamespy::two::query(&socket_addr, timeout_settings).map(Box::new)?,
61 | GameSpyVersion::Three => {
62 | protocols::gamespy::three::query(&socket_addr, timeout_settings).map(Box::new)?
63 | }
64 | }
65 | }
66 | Protocol::Quake(version) => {
67 | match version {
68 | QuakeVersion::One => protocols::quake::one::query(&socket_addr, timeout_settings).map(Box::new)?,
69 | QuakeVersion::Two => protocols::quake::two::query(&socket_addr, timeout_settings).map(Box::new)?,
70 | QuakeVersion::Three => protocols::quake::three::query(&socket_addr, timeout_settings).map(Box::new)?,
71 | }
72 | }
73 | Protocol::Unreal2 => {
74 | protocols::unreal2::query(
75 | &socket_addr,
76 | &extra_settings
77 | .map(ExtraRequestSettings::into)
78 | .unwrap_or_default(),
79 | timeout_settings,
80 | )
81 | .map(Box::new)?
82 | }
83 | Protocol::PROPRIETARY(protocol) => {
84 | match protocol {
85 | ProprietaryProtocol::Savage2 => {
86 | savage2::query_with_timeout(address, port, timeout_settings).map(Box::new)?
87 | }
88 | ProprietaryProtocol::TheShip => {
89 | theship::query_with_timeout(address, port, timeout_settings).map(Box::new)?
90 | }
91 | ProprietaryProtocol::FFOW => ffow::query_with_timeout(address, port, timeout_settings).map(Box::new)?,
92 | ProprietaryProtocol::JC2M => jc2m::query_with_timeout(address, port, timeout_settings).map(Box::new)?,
93 | ProprietaryProtocol::Mindustry => mindustry::query(address, port, &timeout_settings).map(Box::new)?,
94 | ProprietaryProtocol::Minecraft(version) => {
95 | match version {
96 | Some(minecraft::Server::Java) => {
97 | minecraft::protocol::query_java(
98 | &socket_addr,
99 | timeout_settings,
100 | extra_settings.map(ExtraRequestSettings::into),
101 | )
102 | .map(Box::new)?
103 | }
104 | Some(minecraft::Server::Bedrock) => {
105 | minecraft::protocol::query_bedrock(&socket_addr, timeout_settings).map(Box::new)?
106 | }
107 | Some(minecraft::Server::Legacy(group)) => {
108 | minecraft::protocol::query_legacy_specific(*group, &socket_addr, timeout_settings)
109 | .map(Box::new)?
110 | }
111 | None => {
112 | minecraft::protocol::query(
113 | &socket_addr,
114 | timeout_settings,
115 | extra_settings.map(ExtraRequestSettings::into),
116 | )
117 | .map(Box::new)?
118 | }
119 | }
120 | }
121 | ProprietaryProtocol::Eco => {
122 | eco::query_with_timeout_and_extra_settings(
123 | address,
124 | port,
125 | &timeout_settings,
126 | extra_settings.map(ExtraRequestSettings::into),
127 | )
128 | .map(Box::new)?
129 | }
130 | #[cfg(all(feature = "services", feature = "tls", feature = "serde"))]
131 | ProprietaryProtocol::Minetest => {
132 | minetest::query_with_timeout(address, port, &timeout_settings).map(Box::new)?
133 | }
134 | }
135 | }
136 | })
137 | }
138 |
--------------------------------------------------------------------------------
/crates/lib/src/games/savage2/mod.rs:
--------------------------------------------------------------------------------
1 | /// The implementation.
2 | /// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/savage2.js)
3 | pub mod protocol;
4 | /// All types used by the implementation.
5 | pub mod types;
6 |
7 | pub use protocol::*;
8 | pub use types::*;
9 |
--------------------------------------------------------------------------------
/crates/lib/src/games/savage2/protocol.rs:
--------------------------------------------------------------------------------
1 | use crate::buffer::{Buffer, Utf8Decoder};
2 | use crate::games::savage2::types::Response;
3 | use crate::protocols::types::TimeoutSettings;
4 | use crate::socket::{Socket, UdpSocket};
5 | use crate::GDResult;
6 | use byteorder::LittleEndian;
7 | use std::net::{IpAddr, SocketAddr};
8 |
9 | pub fn query(address: &IpAddr, port: Option) -> GDResult { query_with_timeout(address, port, None) }
10 |
11 | pub fn query_with_timeout(
12 | address: &IpAddr,
13 | port: Option,
14 | timeout_settings: Option,
15 | ) -> GDResult {
16 | let addr = &SocketAddr::new(*address, port.unwrap_or(11235));
17 | let mut socket = UdpSocket::new(addr, &timeout_settings)?;
18 | socket.send(&[0x01])?;
19 | let data = socket.receive(None)?;
20 | let mut buffer = Buffer::::new(&data);
21 |
22 | buffer.move_cursor(12)?;
23 |
24 | Ok(Response {
25 | name: buffer.read_string::(None)?,
26 | players_online: buffer.read::()?,
27 | players_maximum: buffer.read::()?,
28 | time: buffer.read_string::(None)?,
29 | map: buffer.read_string::(None)?,
30 | next_map: buffer.read_string::(None)?,
31 | location: buffer.read_string::(None)?,
32 | players_minimum: buffer.read::()?,
33 | game_mode: buffer.read_string::(None)?,
34 | protocol_version: buffer.read_string::(None)?,
35 | level_minimum: buffer.read::()?,
36 | })
37 | }
38 |
--------------------------------------------------------------------------------
/crates/lib/src/games/savage2/types.rs:
--------------------------------------------------------------------------------
1 | use crate::protocols::types::CommonResponse;
2 | use crate::protocols::GenericResponse;
3 | #[cfg(feature = "serde")]
4 | use serde::{Deserialize, Serialize};
5 |
6 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
7 | #[derive(Debug, Clone, PartialEq, Eq)]
8 | pub struct Response {
9 | pub name: String,
10 | pub players_online: u8,
11 | pub players_maximum: u8,
12 | pub players_minimum: u8,
13 | pub time: String,
14 | pub map: String,
15 | pub next_map: String,
16 | pub location: String,
17 | pub game_mode: String,
18 | pub protocol_version: String,
19 | pub level_minimum: u8,
20 | }
21 |
22 | impl CommonResponse for Response {
23 | fn as_original(&self) -> GenericResponse { GenericResponse::Savage2(self) }
24 |
25 | fn name(&self) -> Option<&str> { Some(&self.name) }
26 | fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
27 | fn map(&self) -> Option<&str> { Some(&self.map) }
28 | fn players_maximum(&self) -> u32 { self.players_maximum.into() }
29 | fn players_online(&self) -> u32 { self.players_online.into() }
30 | }
31 |
--------------------------------------------------------------------------------
/crates/lib/src/games/theship/mod.rs:
--------------------------------------------------------------------------------
1 | /// The implementation.
2 | /// Reference: [server queries](https://developer.valvesoftware.com/wiki/Server_queries)
3 | pub mod protocol;
4 | /// All types used by the implementation.
5 | pub mod types;
6 |
7 | pub use protocol::*;
8 | pub use types::*;
9 |
--------------------------------------------------------------------------------
/crates/lib/src/games/theship/protocol.rs:
--------------------------------------------------------------------------------
1 | use crate::games::theship::types::Response;
2 | use crate::protocols::types::TimeoutSettings;
3 | use crate::protocols::valve;
4 | use crate::protocols::valve::Engine;
5 | use crate::GDResult;
6 | use std::net::{IpAddr, SocketAddr};
7 |
8 | pub fn query(address: &IpAddr, port: Option) -> GDResult { query_with_timeout(address, port, None) }
9 |
10 | pub fn query_with_timeout(
11 | address: &IpAddr,
12 | port: Option,
13 | timeout_settings: Option,
14 | ) -> GDResult {
15 | let valve_response = valve::query(
16 | &SocketAddr::new(*address, port.unwrap_or(27015)),
17 | Engine::new(2400),
18 | None,
19 | timeout_settings,
20 | )?;
21 |
22 | Response::new_from_valve_response(valve_response)
23 | }
24 |
--------------------------------------------------------------------------------
/crates/lib/src/games/theship/types.rs:
--------------------------------------------------------------------------------
1 | use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer};
2 | use crate::protocols::valve::{get_optional_extracted_data, Server, ServerPlayer};
3 | use crate::protocols::{valve, GenericResponse};
4 | use crate::GDErrorKind::PacketBad;
5 | use crate::GDResult;
6 | use std::collections::HashMap;
7 |
8 | #[cfg(feature = "serde")]
9 | use serde::{Deserialize, Serialize};
10 |
11 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
12 | #[derive(Debug, Clone, PartialEq, PartialOrd)]
13 | pub struct TheShipPlayer {
14 | pub name: String,
15 | pub score: i32,
16 | pub duration: f32,
17 | pub deaths: u32,
18 | pub money: u32,
19 | }
20 |
21 | impl TheShipPlayer {
22 | pub fn new_from_valve_player(player: &ServerPlayer) -> GDResult {
23 | Ok(Self {
24 | name: player.name.clone(),
25 | score: player.score,
26 | duration: player.duration,
27 | deaths: player.deaths.ok_or(PacketBad)?,
28 | money: player.money.ok_or(PacketBad)?,
29 | })
30 | }
31 | }
32 |
33 | impl CommonPlayer for TheShipPlayer {
34 | fn as_original(&self) -> GenericPlayer { GenericPlayer::TheShip(self) }
35 |
36 | fn name(&self) -> &str { &self.name }
37 | fn score(&self) -> Option { Some(self.score) }
38 | }
39 |
40 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
41 | #[derive(Debug, Clone, PartialEq)]
42 | pub struct Response {
43 | pub protocol_version: u8,
44 | pub name: String,
45 | pub map: String,
46 | pub game_mode: String,
47 | pub game_version: String,
48 | pub players: Vec,
49 | pub players_online: u8,
50 | pub players_maximum: u8,
51 | pub players_bots: u8,
52 | pub server_type: Server,
53 | pub has_password: bool,
54 | pub vac_secured: bool,
55 | pub port: Option,
56 | pub steam_id: Option,
57 | pub tv_port: Option,
58 | pub tv_name: Option,
59 | pub keywords: Option,
60 | pub rules: HashMap,
61 | pub mode: u8,
62 | pub witnesses: u8,
63 | pub duration: u8,
64 | }
65 |
66 | impl CommonResponse for Response {
67 | fn as_original(&self) -> GenericResponse { GenericResponse::TheShip(self) }
68 |
69 | fn name(&self) -> Option<&str> { Some(&self.name) }
70 | fn map(&self) -> Option<&str> { Some(&self.map) }
71 | fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
72 | fn players_maximum(&self) -> u32 { self.players_maximum.into() }
73 | fn players_online(&self) -> u32 { self.players_online.into() }
74 | fn players_bots(&self) -> Option { Some(self.players_bots.into()) }
75 | fn has_password(&self) -> Option { Some(self.has_password) }
76 |
77 | fn players(&self) -> Option> {
78 | Some(
79 | self.players
80 | .iter()
81 | .map(|p| p as &dyn CommonPlayer)
82 | .collect(),
83 | )
84 | }
85 | }
86 |
87 | impl Response {
88 | pub fn new_from_valve_response(response: valve::Response) -> GDResult {
89 | let (port, steam_id, tv_port, tv_name, keywords) = get_optional_extracted_data(response.info.extra_data);
90 |
91 | let the_unwrapped_ship = response.info.the_ship.ok_or(PacketBad)?;
92 |
93 | Ok(Self {
94 | protocol_version: response.info.protocol_version,
95 | name: response.info.name,
96 | map: response.info.map,
97 | game_mode: response.info.game_mode,
98 | game_version: response.info.game_version,
99 | players_online: response.info.players_online,
100 | players: response
101 | .players
102 | .ok_or(PacketBad)?
103 | .iter()
104 | .map(TheShipPlayer::new_from_valve_player)
105 | .collect::>>()?,
106 | players_maximum: response.info.players_maximum,
107 | players_bots: response.info.players_bots,
108 | server_type: response.info.server_type,
109 | has_password: response.info.has_password,
110 | vac_secured: response.info.vac_secured,
111 | port,
112 | steam_id,
113 | tv_port,
114 | tv_name,
115 | keywords,
116 | rules: response.rules.ok_or(PacketBad)?,
117 | mode: the_unwrapped_ship.mode,
118 | witnesses: the_unwrapped_ship.witnesses,
119 | duration: the_unwrapped_ship.duration,
120 | })
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/crates/lib/src/games/types.rs:
--------------------------------------------------------------------------------
1 | //! Game related types
2 |
3 | use crate::protocols::types::{ExtraRequestSettings, Protocol};
4 |
5 | #[cfg(feature = "serde")]
6 | use serde::{Deserialize, Serialize};
7 |
8 | /// Definition of a game
9 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
10 | #[derive(Debug, Clone, PartialEq, Eq)]
11 | pub struct Game {
12 | /// Full name of the game
13 | pub name: &'static str,
14 | /// Default port used by game
15 | pub default_port: u16,
16 | /// The protocol the game's query uses
17 | pub protocol: Protocol,
18 | /// Request settings.
19 | pub request_settings: ExtraRequestSettings,
20 | }
21 |
--------------------------------------------------------------------------------
/crates/lib/src/games/unreal2.rs:
--------------------------------------------------------------------------------
1 | //! Unreal2 game query modules
2 |
3 | use crate::protocols::unreal2::game_query_mod;
4 |
5 | game_query_mod!(darkesthour, "Darkest Hour: Europe '44-'45 (2008)", 7758);
6 | game_query_mod!(devastation, "Devastation (2003)", 7778);
7 | game_query_mod!(killingfloor, "Killing Floor", 7708);
8 | game_query_mod!(redorchestra, "Red Orchestra", 7759);
9 | game_query_mod!(ut2003, "Unreal Tournament 2003", 7758);
10 | game_query_mod!(ut2004, "Unreal Tournament 2004", 7778);
11 |
--------------------------------------------------------------------------------
/crates/lib/src/games/valve.rs:
--------------------------------------------------------------------------------
1 | //! Valve game query modules
2 |
3 | use crate::protocols::valve::game_query_mod;
4 |
5 | game_query_mod!(abioticfactor, "Abiotic Factor", Engine::new(427_410), 27015);
6 | game_query_mod!(
7 | a2oa,
8 | "ARMA 2: Operation Arrowhead",
9 | Engine::new(33930),
10 | 2304
11 | );
12 | game_query_mod!(arma3, "ARMA 3", Engine::new(107_410), 2303);
13 | game_query_mod!(basedefense, "Base Defense", Engine::new(632_730), 27015);
14 | game_query_mod!(alienswarm, "Alien Swarm", Engine::new(630), 27015);
15 | game_query_mod!(aoc, "Age of Chivalry", Engine::new(17510), 27015);
16 | game_query_mod!(
17 | aapg,
18 | "America's Army: Proving Grounds",
19 | Engine::new(203_290),
20 | 27020,
21 | GatheringSettings {
22 | players: GatherToggle::Enforce,
23 | rules: GatherToggle::Skip,
24 | check_app_id: true,
25 | }
26 | );
27 | game_query_mod!(ase, "ARK: Survival Evolved", Engine::new(346_110), 27015);
28 | game_query_mod!(
29 | asrd,
30 | "Alien Swarm: Reactive Drop",
31 | Engine::new(563_560),
32 | 2304
33 | );
34 | game_query_mod!(atlas, "ATLAS", Engine::new(834_910), 57561);
35 | game_query_mod!(avorion, "Avorion", Engine::new(445_220), 27020);
36 | game_query_mod!(
37 | ballisticoverkill,
38 | "Ballistic Overkill",
39 | Engine::new(296_300),
40 | 27016
41 | );
42 | game_query_mod!(
43 | armareforger,
44 | "Arma Reforger",
45 | Engine::new(0),
46 | 17777,
47 | GatheringSettings {
48 | players: GatherToggle::Enforce,
49 | rules: GatherToggle::Enforce,
50 | check_app_id: false,
51 | }
52 | );
53 | game_query_mod!(
54 | avp2010,
55 | "Aliens vs. Predator 2010",
56 | Engine::new(10_680),
57 | 27015
58 | );
59 | game_query_mod!(barotrauma, "Barotrauma", Engine::new(602_960), 27016);
60 | game_query_mod!(blackmesa, "Black Mesa", Engine::new(362_890), 27015);
61 | game_query_mod!(brainbread2, "BrainBread 2", Engine::new(346_330), 27015);
62 | game_query_mod!(
63 | codbo3,
64 | "Call Of Duty: Black Ops 3",
65 | Engine::new(311_210),
66 | 27017
67 | );
68 | game_query_mod!(codenamecure, "Codename CURE", Engine::new(355_180), 27015);
69 | game_query_mod!(
70 | colonysurvival,
71 | "Colony Survival",
72 | Engine::new(366_090),
73 | 27004
74 | );
75 | game_query_mod!(
76 | conanexiles,
77 | "Conan Exiles",
78 | Engine::new(440_900),
79 | 27015,
80 | GatheringSettings {
81 | players: GatherToggle::Skip,
82 | rules: GatherToggle::Enforce,
83 | check_app_id: true,
84 | }
85 | );
86 | game_query_mod!(
87 | counterstrike,
88 | "Counter-Strike",
89 | Engine::new_gold_src(false),
90 | 27015
91 | );
92 | game_query_mod!(counterstrike2, "Counter-Strike 2", Engine::new(730), 27015);
93 | game_query_mod!(creativerse, "Creativerse", Engine::new(280_790), 26901);
94 | game_query_mod!(
95 | cscz,
96 | "Counter Strike: Condition Zero",
97 | Engine::new_gold_src(false),
98 | 27015
99 | );
100 | game_query_mod!(
101 | csgo,
102 | "Counter-Strike: Global Offensive",
103 | Engine::new(730),
104 | 27015
105 | );
106 | game_query_mod!(css, "Counter-Strike: Source", Engine::new(240), 27015);
107 | game_query_mod!(dab, "Double Action: Boogaloo", Engine::new(317_360), 27015);
108 | game_query_mod!(dod, "Day of Defeat", Engine::new_gold_src(false), 27015);
109 | game_query_mod!(dods, "Day of Defeat: Source", Engine::new(300), 27015);
110 | game_query_mod!(doi, "Day of Infamy", Engine::new(447_820), 27015);
111 | game_query_mod!(dst, "Don't Starve Together", Engine::new(322_320), 27016);
112 | game_query_mod!(enshrouded, "Enshrouded", Engine::new(1_203_620), 15637);
113 | game_query_mod!(garrysmod, "Garry's Mod", Engine::new(4000), 27016);
114 | game_query_mod!(hl2d, "Half-Life 2 Deathmatch", Engine::new(320), 27015);
115 | game_query_mod!(
116 | hlds,
117 | "Half-Life Deathmatch: Source",
118 | Engine::new(360),
119 | 27015
120 | );
121 | game_query_mod!(hll, "Hell Let Loose", Engine::new(686_810), 26420);
122 | game_query_mod!(
123 | imic,
124 | "Insurgency: Modern Infantry Combat",
125 | Engine::new(17700),
126 | 27015
127 | );
128 | game_query_mod!(insurgency, "Insurgency", Engine::new(222_880), 27015);
129 | game_query_mod!(
130 | insurgencysandstorm,
131 | "Insurgency: Sandstorm",
132 | Engine::new(581_320),
133 | 27131
134 | );
135 | game_query_mod!(l4d, "Left 4 Dead", Engine::new(500), 27015);
136 | game_query_mod!(l4d2, "Left 4 Dead 2", Engine::new(550), 27015);
137 | game_query_mod!(
138 | ohd,
139 | "Operation: Harsh Doorstop",
140 | Engine::new_with_dedicated(736_590, 950_900),
141 | 27005
142 | );
143 | game_query_mod!(onset, "Onset", Engine::new(1_105_810), 7776);
144 | game_query_mod!(postscriptum, "Post Scriptum", Engine::new(736_220), 10037);
145 | game_query_mod!(
146 | projectzomboid,
147 | "Project Zomboid",
148 | Engine::new(108_600),
149 | 16261
150 | );
151 | game_query_mod!(risingworld, "Rising World", Engine::new(324_080), 4254);
152 | game_query_mod!(ror2, "Risk of Rain 2", Engine::new(632_360), 27016);
153 | game_query_mod!(rust, "Rust", Engine::new(252_490), 27015);
154 | game_query_mod!(sco, "Sven Co-op", Engine::new_gold_src(false), 27015);
155 | game_query_mod!(sdtd, "7 Days to Die", Engine::new(251_570), 26900);
156 | game_query_mod!(soulmask, "Soulmask", Engine::new(2_646_460), 27015);
157 | game_query_mod!(squad, "Squad", Engine::new(393_380), 27165);
158 | game_query_mod!(
159 | starbound,
160 | "Starbound",
161 | Engine::new(211_820),
162 | 21025,
163 | GatheringSettings {
164 | players: GatherToggle::Enforce,
165 | rules: GatherToggle::Enforce,
166 | check_app_id: false,
167 | }
168 | );
169 | game_query_mod!(teamfortress2, "Team Fortress 2", Engine::new(440), 27015);
170 | game_query_mod!(
171 | tfc,
172 | "Team Fortress Classic",
173 | Engine::new_gold_src(false),
174 | 27015
175 | );
176 | game_query_mod!(theforest, "The Forest", Engine::new(556_450), 27016);
177 | game_query_mod!(thefront, "The Front", Engine::new(2_285_150), 27015);
178 | game_query_mod!(unturned, "Unturned", Engine::new(304_930), 27015);
179 | game_query_mod!(
180 | valheim,
181 | "Valheim",
182 | Engine::new(892_970),
183 | 2457,
184 | GatheringSettings {
185 | players: GatherToggle::Enforce,
186 | rules: GatherToggle::Skip,
187 | check_app_id: true,
188 | }
189 | );
190 | game_query_mod!(vrising, "V Rising", Engine::new(1_604_030), 27016);
191 | game_query_mod!(zps, "Zombie Panic: Source", Engine::new(17_500), 27015);
192 | game_query_mod!(moe, "Myth of Empires", Engine::new(1_371_580), 12888);
193 | game_query_mod!(mordhau, "Mordhau", Engine::new(629_760), 27015);
194 | game_query_mod!(
195 | pvak2,
196 | "Pirates, Vikings, and Knights II",
197 | Engine::new(17_570),
198 | 27015
199 | );
200 | game_query_mod!(nla, "Nova-Life: Amboise", Engine::new(885_570), 27015);
201 | game_query_mod!(pixark, "PixARK", Engine::new(593_600), 27015);
202 |
--------------------------------------------------------------------------------
/crates/lib/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! Game Server Query Library.
2 | //!
3 | //! # Usage example:
4 | //!
5 | //! ## For a specific game
6 | //! ```
7 | //! use gamedig::games::teamfortress2;
8 | //!
9 | //! let response = teamfortress2::query(&"127.0.0.1".parse().unwrap(), None); // None is the default port (which is 27015), could also be Some(27015)
10 | //! match response { // Result type, must check what it is...
11 | //! Err(error) => println!("Couldn't query, error: {}", error),
12 | //! Ok(r) => println!("{:#?}", r)
13 | //! }
14 | //! ```
15 | //!
16 | //! ## Using a game definition
17 | //! ```
18 | //! use gamedig::{GAMES, query};
19 | //!
20 | //! let game = GAMES.get("teamfortress2").unwrap(); // Get a game definition, the full list can be found in src/games/mod.rs
21 | //! let response = query(game, &"127.0.0.1".parse().unwrap(), None); // None will use the default port
22 | //! match response {
23 | //! Err(error) => println!("Couldn't query, error: {}", error),
24 | //! Ok(r) => println!("{:#?}", r.as_json()),
25 | //! }
26 | //! ```
27 | //!
28 | //! # Crate features:
29 | //! Enabled by default: `games`, `game_defs`, `services`
30 | //!
31 | //! `serde` - enables serde serialization/deserialization for many gamedig types
32 | //! using serde derive.
33 | //! `games` - include games support.
34 | //! `services` - include services support.
35 | //! `game_defs` - include game definitions for programmatic access (enabled by
36 | //! default).
37 | //! `clap` - enable clap derivations for gamedig settings types.
38 | //! `tls` - enable TLS support for the HTTP client.
39 |
40 | pub mod errors;
41 | #[cfg(feature = "games")]
42 | pub mod games;
43 | pub mod protocols;
44 | #[cfg(feature = "services")]
45 | pub mod services;
46 |
47 | mod buffer;
48 | mod http;
49 | mod socket;
50 | mod utils;
51 |
52 | #[cfg(feature = "packet_capture")]
53 | pub mod capture;
54 |
55 | pub use errors::*;
56 | #[cfg(feature = "games")]
57 | pub use games::*;
58 | #[allow(unused_imports)]
59 | #[cfg(feature = "games")]
60 | pub use query::*;
61 | #[cfg(feature = "services")]
62 | pub use services::*;
63 |
64 | // Re-export types needed to call games::query::query in the root
65 | pub use protocols::types::{ExtraRequestSettings, TimeoutSettings};
66 |
--------------------------------------------------------------------------------
/crates/lib/src/protocols/epic/mod.rs:
--------------------------------------------------------------------------------
1 | /// The implementation.
2 | pub mod protocol;
3 | /// All types used by the implementation.
4 | pub mod types;
5 |
6 | pub use protocol::*;
7 | pub use types::*;
8 |
9 | /// Generate a module containing a query function for an epic (EOS) game.
10 | ///
11 | /// * `mod_name` - The name to be given to the game module (see ID naming
12 | /// conventions in CONTRIBUTING.md).
13 | /// * `pretty_name` - The full name of the game, will be used as the
14 | /// documentation for the created module.
15 | /// * `steam_app`, `default_port` - Passed through to [game_query_fn].
16 | #[cfg(feature = "games")]
17 | macro_rules! game_query_mod {
18 | ($mod_name: ident, $pretty_name: expr, $default_port: literal, $credentials: expr) => {
19 | #[doc = $pretty_name]
20 | pub mod $mod_name {
21 | use crate::protocols::epic::Credentials;
22 |
23 | crate::protocols::epic::game_query_fn!($pretty_name, $default_port, $credentials);
24 | }
25 | };
26 | }
27 |
28 | #[cfg(feature = "games")]
29 | pub(crate) use game_query_mod;
30 |
31 | /// Generate a query function for an epic (EOS) game.
32 | ///
33 | /// * `default_port` - The default port the game uses.
34 | /// * `credentials` - Credentials to access EOS.
35 | #[cfg(feature = "games")]
36 | macro_rules! game_query_fn {
37 | ($pretty_name: expr, $default_port: literal, $credentials: expr) => {
38 | crate::protocols::epic::game_query_fn! {@gen $default_port, concat!(
39 | "Make a Epic query for ", $pretty_name, ".\n\n",
40 | "If port is `None`, then the default port (", stringify!($default_port), ") will be used."), $credentials}
41 | };
42 |
43 | (@gen $default_port: literal, $doc: expr, $credentials: expr) => {
44 | #[doc = $doc]
45 | pub fn query(
46 | address: &std::net::IpAddr,
47 | port: Option,
48 | ) -> crate::GDResult {
49 | crate::protocols::epic::query(
50 | $credentials,
51 | &std::net::SocketAddr::new(*address, port.unwrap_or($default_port)),
52 | )
53 | }
54 | };
55 | }
56 |
57 | #[cfg(feature = "games")]
58 | pub(crate) use game_query_fn;
59 |
--------------------------------------------------------------------------------
/crates/lib/src/protocols/epic/protocol.rs:
--------------------------------------------------------------------------------
1 | use crate::http::HttpClient;
2 | use crate::protocols::epic::Response;
3 | use crate::GDErrorKind::{JsonParse, PacketBad};
4 | use crate::{GDResult, TimeoutSettings};
5 | use base64::prelude::BASE64_STANDARD;
6 | use base64::Engine;
7 | use serde::Deserialize;
8 | #[cfg(feature = "serde")]
9 | use serde::Serialize;
10 | use serde_json::Value;
11 | use std::net::SocketAddr;
12 |
13 | const EPIC_API_ENDPOINT: &str = "https://api.epicgames.dev";
14 |
15 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
16 | #[derive(Debug, Clone, PartialEq, Eq)]
17 | pub struct Credentials {
18 | #[cfg_attr(feature = "serde", serde(skip_deserializing, skip_serializing))]
19 | pub deployment: &'static str,
20 | #[cfg_attr(feature = "serde", serde(skip_deserializing, skip_serializing))]
21 | pub id: &'static str,
22 | #[cfg_attr(feature = "serde", serde(skip_deserializing, skip_serializing))]
23 | pub secret: &'static str,
24 | pub auth_by_external: bool,
25 | }
26 |
27 | pub struct EpicProtocol {
28 | client: HttpClient,
29 | credentials: Credentials,
30 | }
31 |
32 | #[derive(Deserialize)]
33 | struct ClientTokenResponse {
34 | access_token: String,
35 | }
36 |
37 | #[derive(Deserialize)]
38 | struct QueryResponse {
39 | sessions: Value,
40 | }
41 |
42 | macro_rules! extract_optional_field {
43 | ($value:expr, $fields:expr, $map_func:expr) => {
44 | $fields
45 | .iter()
46 | .fold(Some(&$value), |acc, &key| acc.and_then(|val| val.get(key)))
47 | .map($map_func)
48 | .flatten()
49 | };
50 | }
51 |
52 | macro_rules! extract_field {
53 | ($value:expr, $fields:expr, $map_func:expr) => {
54 | extract_optional_field!($value, $fields, $map_func)
55 | .ok_or(PacketBad.context("Field is missing or is not parsable."))?
56 | };
57 | }
58 |
59 | impl EpicProtocol {
60 | pub fn new(credentials: Credentials, timeout_settings: TimeoutSettings) -> GDResult {
61 | Ok(Self {
62 | client: HttpClient::from_url(EPIC_API_ENDPOINT, &Some(timeout_settings), None)?,
63 | credentials,
64 | })
65 | }
66 |
67 | pub fn auth_by_external(&self) -> GDResult