├── .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 | 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 |
6 | 7 | CI 8 | 9 | 10 | Latest Version 11 | 12 | 13 | Crates.io 14 | 15 | 16 | Node-GameDig Game Coverage 17 | 18 | 19 | Rust-GameDig Dependencies 20 | 21 |
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 | [![CI](https://github.com/gamedig/rust-gamedig/actions/workflows/ci.yml/badge.svg)](https://github.com/gamedig/rust-gamedig/actions) [![License:MIT](https://img.shields.io/github/license/gamedig/rust-gamedig?color=blue)](https://github.com/gamedig/rust-gamedig/blob/main/LICENSE.md) [![node coverage](https://raw.githubusercontent.com/gamedig/rust-gamedig/main/.github/badges/node.svg)](https://github.com/gamedig/node-gamedig) 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 { Ok(String::new()) } 68 | 69 | pub fn auth_by_client(&mut self) -> GDResult { 70 | let body = [ 71 | ("grant_type", "client_credentials"), 72 | ("deployment_id", self.credentials.deployment), 73 | ]; 74 | 75 | let auth_format = format!("{}:{}", self.credentials.id, self.credentials.secret); 76 | let auth_base = BASE64_STANDARD.encode(auth_format); 77 | let auth = format!("Basic {}", auth_base.as_str()); 78 | let authorization = auth.as_str(); 79 | 80 | let headers = [ 81 | ("Authorization", authorization), 82 | ("Content-Type", "application/x-www-form-urlencoded"), 83 | ]; 84 | 85 | let response = 86 | self.client 87 | .post_json_with_form::("/auth/v1/oauth/token", Some(&headers), &body)?; 88 | Ok(response.access_token) 89 | } 90 | 91 | pub fn query_raw(&mut self, address: &SocketAddr) -> GDResult { 92 | let port = address.port(); 93 | let address = address.ip().to_string(); 94 | 95 | let body = format!( 96 | "{{\"criteria\":[{{\"key\":\"attributes.ADDRESS_s\",\"op\":\"EQUAL\",\"value\":\"{}\"}}]}}", 97 | address 98 | ); 99 | let body = serde_json::from_str::(body.as_str()).map_err(|e| JsonParse.context(e))?; 100 | 101 | let token = if self.credentials.auth_by_external { 102 | self.auth_by_external()? 103 | } else { 104 | self.auth_by_client()? 105 | }; 106 | let authorization = format!("Bearer {}", token); 107 | let headers = [ 108 | ("Content-Type", "application/json"), 109 | ("Accept", "application/json"), 110 | ("Authorization", authorization.as_str()), 111 | ]; 112 | 113 | let url = format!("/matchmaking/v1/{}/filter", self.credentials.deployment); 114 | let response: QueryResponse = self.client.post_json(url.as_str(), Some(&headers), body)?; 115 | 116 | if let Value::Array(sessions) = response.sessions { 117 | if sessions.is_empty() { 118 | return Err(PacketBad.context("No servers provided.")); 119 | } 120 | 121 | for session in sessions.into_iter() { 122 | let attributes = session 123 | .get("attributes") 124 | .ok_or(PacketBad.context("Expected attributes field missing in sessions."))?; 125 | 126 | let address_match = attributes 127 | .get("ADDRESSBOUND_s") 128 | .and_then(Value::as_str) 129 | .map_or(false, |v| v == address || v == format!("0.0.0.0:{}", port)) 130 | || attributes 131 | .get("GAMESERVER_PORT_1") 132 | .and_then(Value::as_u64) 133 | .map_or(false, |v| v == port as u64); 134 | 135 | if address_match { 136 | return Ok(session); 137 | } 138 | } 139 | 140 | return Err( 141 | PacketBad.context("Servers were provided but the specified one couldn't be found amongst them.") 142 | ); 143 | } 144 | 145 | Err(PacketBad.context("Expected session field to be an array.")) 146 | } 147 | 148 | pub fn query(&mut self, address: &SocketAddr) -> GDResult { 149 | let value = self.query_raw(address)?; 150 | 151 | let build_version = extract_optional_field!(value, ["attributes", "BUILDID_s"], Value::as_str); 152 | let minor_version = extract_optional_field!(value, ["attributes", "MINORBUILDID_s"], Value::as_str); 153 | 154 | let game_version = match (build_version, minor_version) { 155 | (Some(b), Some(m)) => Some(format!("{b}.{m}")), 156 | _ => None, 157 | }; 158 | 159 | Ok(Response { 160 | name: extract_field!(value, ["attributes", "CUSTOMSERVERNAME_s"], Value::as_str).to_string(), 161 | map: extract_field!(value, ["attributes", "MAPNAME_s"], Value::as_str).to_string(), 162 | has_password: extract_field!(value, ["attributes", "SERVERPASSWORD_b"], Value::as_bool), 163 | players_online: extract_field!(value, ["totalPlayers"], Value::as_u64) as u32, 164 | players_maxmimum: extract_field!(value, ["settings", "maxPublicPlayers"], Value::as_u64) as u32, 165 | players: vec![], 166 | game_version, 167 | raw: value, 168 | }) 169 | } 170 | } 171 | 172 | pub fn query(credentials: Credentials, address: &SocketAddr) -> GDResult { 173 | query_with_timeout(credentials, address, None) 174 | } 175 | 176 | pub fn query_with_timeout( 177 | credentials: Credentials, 178 | address: &SocketAddr, 179 | timeout_settings: Option, 180 | ) -> GDResult { 181 | let mut client = EpicProtocol::new(credentials, timeout_settings.unwrap_or_default())?; 182 | client.query(address) 183 | } 184 | -------------------------------------------------------------------------------- /crates/lib/src/protocols/epic/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 | use serde_json::Value; 6 | 7 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 8 | #[derive(Debug, Clone, PartialEq)] 9 | pub struct Response { 10 | pub name: String, 11 | pub map: String, 12 | pub has_password: bool, 13 | pub players_online: u32, 14 | pub players_maxmimum: u32, 15 | pub players: Vec, 16 | pub game_version: Option, 17 | pub raw: Value, 18 | } 19 | 20 | impl CommonResponse for Response { 21 | fn as_original(&self) -> GenericResponse { GenericResponse::Epic(self) } 22 | fn name(&self) -> Option<&str> { Some(&self.name) } 23 | fn map(&self) -> Option<&str> { Some(&self.map) } 24 | fn players_maximum(&self) -> u32 { self.players_maxmimum } 25 | 26 | fn players_online(&self) -> u32 { self.players_online } 27 | 28 | fn has_password(&self) -> Option { Some(self.has_password) } 29 | 30 | fn players(&self) -> Option> { 31 | Some( 32 | self.players 33 | .iter() 34 | .map(|p| p as &dyn CommonPlayer) 35 | .collect(), 36 | ) 37 | } 38 | 39 | fn game_version(&self) -> Option<&str> { self.game_version.as_deref() } 40 | } 41 | 42 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 43 | #[derive(Debug, Clone, PartialEq)] 44 | pub struct Player { 45 | pub name: String, 46 | } 47 | 48 | impl CommonPlayer for Player { 49 | fn as_original(&self) -> GenericPlayer { GenericPlayer::Epic(self) } 50 | 51 | fn name(&self) -> &str { &self.name } 52 | } 53 | -------------------------------------------------------------------------------- /crates/lib/src/protocols/gamespy/common.rs: -------------------------------------------------------------------------------- 1 | use crate::{GDErrorKind, GDResult}; 2 | use std::collections::HashMap; 3 | 4 | pub fn has_password(server_vars: &mut HashMap) -> GDResult { 5 | let password_value = server_vars 6 | .remove("password") 7 | .ok_or_else(|| GDErrorKind::PacketBad.context("Missing password (exists) field"))? 8 | .to_lowercase(); 9 | 10 | if let Ok(has) = password_value.parse::() { 11 | return Ok(has); 12 | } 13 | 14 | let as_numeral: u8 = password_value 15 | .parse() 16 | .map_err(|e| GDErrorKind::TypeParse.context(e))?; 17 | 18 | Ok(as_numeral != 0) 19 | } 20 | -------------------------------------------------------------------------------- /crates/lib/src/protocols/gamespy/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "serde")] 2 | use serde::{Deserialize, Serialize}; 3 | 4 | pub(crate) mod common; 5 | /// The implementations. 6 | pub mod protocols; 7 | 8 | pub use protocols::*; 9 | 10 | /// Versions of the gamespy protocol 11 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 12 | #[derive(Debug, Clone, PartialEq, Eq)] 13 | pub enum GameSpyVersion { 14 | One, 15 | Two, 16 | Three, 17 | } 18 | 19 | /// Versioned response type 20 | #[cfg_attr(feature = "serde", derive(Serialize))] 21 | #[derive(Debug, Clone, PartialEq, Eq)] 22 | pub enum VersionedResponse<'a> { 23 | One(&'a one::Response), 24 | Two(&'a two::Response), 25 | Three(&'a three::Response), 26 | } 27 | 28 | /// Versioned player type 29 | #[cfg_attr(feature = "serde", derive(Serialize))] 30 | #[derive(Debug, Clone, PartialEq, Eq)] 31 | pub enum VersionedPlayer<'a> { 32 | One(&'a one::Player), 33 | Two(&'a two::Player), 34 | Three(&'a three::Player), 35 | } 36 | 37 | /// Generate a module containing a query function for a gamespy game. 38 | /// 39 | /// * `mod_name` - The name to be given to the game module (see ID naming 40 | /// conventions in CONTRIBUTING.md). 41 | /// * `pretty_name` - The full name of the game, will be used as the 42 | /// documentation for the created module. 43 | /// * `gamespy_ver`, `default_port` - Passed through to [game_query_fn]. 44 | #[cfg(feature = "games")] 45 | macro_rules! game_query_mod { 46 | ($mod_name: ident, $pretty_name: expr, $gamespy_ver: ident, $default_port: literal) => { 47 | #[doc = $pretty_name] 48 | pub mod $mod_name { 49 | crate::protocols::gamespy::game_query_fn!($gamespy_ver, $default_port); 50 | } 51 | }; 52 | } 53 | 54 | #[cfg(feature = "games")] 55 | pub(crate) use game_query_mod; 56 | 57 | // Allow generating doc comments: 58 | // https://users.rust-lang.org/t/macros-filling-text-in-comments/20473 59 | /// Generate a query function for a gamespy game. 60 | /// 61 | /// * `gamespy_ver` - The name of the [module](crate::protocols::gamespy) for 62 | /// the gamespy version the game uses. 63 | /// * `default_port` - The default port the game uses. 64 | /// 65 | /// ```rust,ignore 66 | /// use crate::protocols::gamespy::game_query_fn; 67 | /// game_query_fn!(one, 7778); 68 | /// ``` 69 | #[cfg(feature = "games")] 70 | macro_rules! game_query_fn { 71 | ($gamespy_ver: ident, $default_port: literal) => { 72 | crate::protocols::gamespy::game_query_fn! {@gen $gamespy_ver, $default_port, concat!( 73 | "Make a gamespy ", stringify!($gamespy_ver), " query with default timeout settings.\n\n", 74 | "If port is `None`, then the default port (", stringify!($default_port), ") will be used.")} 75 | }; 76 | 77 | (@gen $gamespy_ver: ident, $default_port: literal, $doc: expr) => { 78 | #[doc = $doc] 79 | pub fn query( 80 | address: &std::net::IpAddr, 81 | port: Option, 82 | ) -> crate::GDResult { 83 | crate::protocols::gamespy::$gamespy_ver::query( 84 | &std::net::SocketAddr::new(*address, port.unwrap_or($default_port)), 85 | None, 86 | ) 87 | } 88 | }; 89 | } 90 | 91 | #[cfg(feature = "games")] 92 | pub(crate) use game_query_fn; 93 | -------------------------------------------------------------------------------- /crates/lib/src/protocols/gamespy/protocols/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod one; 2 | pub mod three; 3 | pub mod two; 4 | -------------------------------------------------------------------------------- /crates/lib/src/protocols/gamespy/protocols/one/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod protocol; 2 | pub mod types; 3 | 4 | pub use protocol::*; 5 | pub use types::*; 6 | -------------------------------------------------------------------------------- /crates/lib/src/protocols/gamespy/protocols/one/types.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | #[cfg(feature = "serde")] 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::protocols::gamespy::{VersionedPlayer, VersionedResponse}; 7 | use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer}; 8 | use crate::protocols::GenericResponse; 9 | 10 | /// A player’s details. 11 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 12 | #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 13 | pub struct Player { 14 | pub name: String, 15 | pub team: Option, 16 | /// The ping from the server's perspective. 17 | pub ping: u16, 18 | pub face: Option, 19 | pub skin: Option, 20 | pub mesh: Option, 21 | pub score: i32, 22 | pub deaths: Option, 23 | pub health: Option, 24 | pub secret: Option, 25 | } 26 | 27 | impl CommonPlayer for Player { 28 | fn as_original(&self) -> GenericPlayer { GenericPlayer::Gamespy(VersionedPlayer::One(self)) } 29 | 30 | fn name(&self) -> &str { &self.name } 31 | fn score(&self) -> Option { Some(self.score) } 32 | } 33 | 34 | /// A query response. 35 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 36 | #[derive(Debug, Clone, PartialEq, Eq)] 37 | pub struct Response { 38 | pub name: String, 39 | pub map: String, 40 | pub map_title: Option, 41 | pub admin_contact: Option, 42 | pub admin_name: Option, 43 | pub has_password: bool, 44 | pub game_mode: String, 45 | pub game_version: String, 46 | pub players_maximum: u32, 47 | pub players_online: u32, 48 | pub players_minimum: Option, 49 | pub players: Vec, 50 | pub tournament: bool, 51 | pub unused_entries: HashMap, 52 | } 53 | 54 | impl CommonResponse for Response { 55 | fn as_original(&self) -> GenericResponse { GenericResponse::GameSpy(VersionedResponse::One(self)) } 56 | 57 | fn name(&self) -> Option<&str> { Some(&self.name) } 58 | fn map(&self) -> Option<&str> { Some(&self.map) } 59 | fn has_password(&self) -> Option { Some(self.has_password) } 60 | fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) } 61 | fn game_version(&self) -> Option<&str> { Some(&self.game_version) } 62 | fn players_maximum(&self) -> u32 { self.players_maximum } 63 | fn players_online(&self) -> u32 { self.players_online } 64 | 65 | fn players(&self) -> Option> { 66 | Some( 67 | self.players 68 | .iter() 69 | .map(|p| p as &dyn CommonPlayer) 70 | .collect(), 71 | ) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /crates/lib/src/protocols/gamespy/protocols/three/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod protocol; 2 | pub mod types; 3 | 4 | pub use protocol::*; 5 | pub use types::*; 6 | -------------------------------------------------------------------------------- /crates/lib/src/protocols/gamespy/protocols/three/types.rs: -------------------------------------------------------------------------------- 1 | use crate::protocols::gamespy::{VersionedPlayer, VersionedResponse}; 2 | use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer}; 3 | use crate::protocols::GenericResponse; 4 | use std::collections::HashMap; 5 | 6 | #[cfg(feature = "serde")] 7 | use serde::{Deserialize, Serialize}; 8 | 9 | /// A player’s details. 10 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 11 | #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 12 | pub struct Player { 13 | pub name: String, 14 | pub score: i32, 15 | pub ping: u16, 16 | pub team: u8, 17 | pub deaths: u32, 18 | pub skill: u32, 19 | } 20 | 21 | impl CommonPlayer for Player { 22 | fn as_original(&self) -> crate::protocols::types::GenericPlayer { 23 | GenericPlayer::Gamespy(VersionedPlayer::Three(self)) 24 | } 25 | 26 | fn name(&self) -> &str { &self.name } 27 | fn score(&self) -> Option { Some(self.score) } 28 | } 29 | 30 | /// A team's details 31 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 32 | #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 33 | pub struct Team { 34 | pub name: String, 35 | pub score: i32, 36 | } 37 | 38 | /// A query response. 39 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 40 | #[derive(Debug, Clone, PartialEq, Eq)] 41 | pub struct Response { 42 | pub name: String, 43 | pub map: String, 44 | pub has_password: bool, 45 | pub game_mode: String, 46 | pub game_version: String, 47 | pub players_maximum: u32, 48 | pub players_online: u32, 49 | pub players_minimum: Option, 50 | pub players: Vec, 51 | pub teams: Vec, 52 | pub tournament: bool, 53 | pub unused_entries: HashMap, 54 | } 55 | 56 | impl CommonResponse for Response { 57 | fn as_original(&self) -> GenericResponse { GenericResponse::GameSpy(VersionedResponse::Three(self)) } 58 | 59 | fn name(&self) -> Option<&str> { Some(&self.name) } 60 | fn map(&self) -> Option<&str> { Some(&self.map) } 61 | fn has_password(&self) -> Option { Some(self.has_password) } 62 | fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) } 63 | fn game_version(&self) -> Option<&str> { Some(&self.game_version) } 64 | fn players_maximum(&self) -> u32 { self.players_maximum } 65 | fn players_online(&self) -> u32 { self.players_online } 66 | 67 | fn players(&self) -> Option> { 68 | Some( 69 | self.players 70 | .iter() 71 | .map(|p| p as &dyn CommonPlayer) 72 | .collect(), 73 | ) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /crates/lib/src/protocols/gamespy/protocols/two/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod protocol; 2 | pub mod types; 3 | 4 | pub use protocol::*; 5 | pub use types::*; 6 | -------------------------------------------------------------------------------- /crates/lib/src/protocols/gamespy/protocols/two/types.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::protocols::gamespy::{VersionedPlayer, VersionedResponse}; 4 | use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer}; 5 | use crate::protocols::GenericResponse; 6 | 7 | #[cfg(feature = "serde")] 8 | use serde::{Deserialize, Serialize}; 9 | 10 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 11 | #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 12 | pub struct Team { 13 | pub name: String, 14 | pub score: u16, 15 | } 16 | 17 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 18 | #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 19 | pub struct Player { 20 | pub name: String, 21 | pub score: u16, 22 | pub ping: u16, 23 | pub team_index: u16, 24 | } 25 | 26 | impl CommonPlayer for Player { 27 | fn as_original(&self) -> GenericPlayer { GenericPlayer::Gamespy(VersionedPlayer::Two(self)) } 28 | 29 | fn name(&self) -> &str { &self.name } 30 | fn score(&self) -> Option { Some(self.score.into()) } 31 | } 32 | 33 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 34 | #[derive(Debug, Clone, PartialEq, Eq)] 35 | pub struct Response { 36 | pub name: String, 37 | pub map: String, 38 | pub has_password: bool, 39 | pub teams: Vec, 40 | pub players_maximum: u32, 41 | pub players_online: u32, 42 | pub players_minimum: Option, 43 | pub players: Vec, 44 | pub unused_entries: HashMap, 45 | } 46 | 47 | impl CommonResponse for Response { 48 | fn as_original(&self) -> GenericResponse { GenericResponse::GameSpy(VersionedResponse::Two(self)) } 49 | 50 | fn name(&self) -> Option<&str> { Some(&self.name) } 51 | fn map(&self) -> Option<&str> { Some(&self.map) } 52 | fn has_password(&self) -> Option { Some(self.has_password) } 53 | fn players_maximum(&self) -> u32 { self.players_maximum } 54 | fn players_online(&self) -> u32 { self.players_online } 55 | 56 | fn players(&self) -> Option> { 57 | Some( 58 | self.players 59 | .iter() 60 | .map(|p| p as &dyn CommonPlayer) 61 | .collect(), 62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /crates/lib/src/protocols/mod.rs: -------------------------------------------------------------------------------- 1 | //! Protocols that are currently implemented. 2 | //! 3 | //! A protocol will be here if it supports multiple entries, if not, its 4 | //! implementation will be in that specific needed place, a protocol can be 5 | //! independently queried. 6 | 7 | #[cfg(feature = "tls")] 8 | /// Reference: [node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/epic.js) 9 | pub mod epic; 10 | /// Reference: [node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy1.js) 11 | pub mod gamespy; 12 | /// Reference: [node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake1.js) 13 | pub mod quake; 14 | /// General types that are used by all protocols. 15 | pub mod types; 16 | /// Reference: [node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/unreal2.js) 17 | pub mod unreal2; 18 | /// Reference: [Server Query](https://developer.valvesoftware.com/wiki/Server_queries) 19 | pub mod valve; 20 | 21 | pub use types::{ExtraRequestSettings, GenericResponse, Protocol}; 22 | -------------------------------------------------------------------------------- /crates/lib/src/protocols/quake/client.rs: -------------------------------------------------------------------------------- 1 | use byteorder::LittleEndian; 2 | 3 | use crate::buffer::{Buffer, Utf8Decoder}; 4 | use crate::protocols::quake::types::Response; 5 | use crate::protocols::types::TimeoutSettings; 6 | use crate::socket::{Socket, UdpSocket}; 7 | use crate::utils::retry_on_timeout; 8 | use crate::GDErrorKind::{PacketBad, TypeParse}; 9 | use crate::{GDErrorKind, GDResult}; 10 | use std::collections::HashMap; 11 | use std::net::SocketAddr; 12 | use std::slice::Iter; 13 | 14 | pub trait QuakeClient { 15 | type Player; 16 | 17 | fn get_send_header<'a>() -> &'a str; 18 | fn get_response_header<'a>() -> &'a str; 19 | fn parse_player_string(data: Iter<&str>) -> GDResult; 20 | } 21 | 22 | /// Send request and return result buffer. 23 | /// This function will retry fetch on timeouts. 24 | fn get_data( 25 | address: &SocketAddr, 26 | timeout_settings: &Option, 27 | ) -> GDResult> { 28 | let mut socket = UdpSocket::new(address, timeout_settings)?; 29 | retry_on_timeout( 30 | TimeoutSettings::get_retries_or_default(timeout_settings), 31 | move || get_data_impl::(&mut socket), 32 | ) 33 | } 34 | 35 | /// Send request and return result buffer (without retry logic). 36 | fn get_data_impl(socket: &mut UdpSocket) -> GDResult> { 37 | socket.send( 38 | &[ 39 | &[0xFF, 0xFF, 0xFF, 0xFF], 40 | Client::get_send_header().as_bytes(), 41 | &[0x00], 42 | ] 43 | .concat(), 44 | )?; 45 | 46 | let data = socket.receive(None)?; 47 | let mut bufferer = Buffer::::new(&data); 48 | 49 | if bufferer.read::()? != u32::MAX { 50 | return Err(PacketBad.context("Expected 4294967295")); 51 | } 52 | 53 | let response_header = Client::get_response_header().as_bytes(); 54 | if !bufferer.remaining_bytes().starts_with(response_header) { 55 | Err(GDErrorKind::PacketBad)?; 56 | } 57 | 58 | bufferer.move_cursor(response_header.len() as isize)?; 59 | 60 | Ok(bufferer.remaining_bytes().to_vec()) 61 | } 62 | 63 | fn get_server_values(bufferer: &mut Buffer) -> GDResult> { 64 | let data = bufferer.read_string::(Some([0x0A]))?; 65 | let mut data_split = data.split('\\').collect::>(); 66 | if let Some(first) = data_split.first() { 67 | if first == &"" { 68 | data_split.remove(0); 69 | } 70 | } 71 | 72 | let values = data_split.chunks(2); 73 | 74 | let mut vars: HashMap = HashMap::new(); 75 | for data in values { 76 | let key = data.first(); 77 | let value = data.get(1); 78 | 79 | if let Some(k) = key { 80 | if let Some(v) = value { 81 | vars.insert((*k).to_string(), (*v).to_string()); 82 | } 83 | } 84 | } 85 | 86 | Ok(vars) 87 | } 88 | 89 | fn get_players(bufferer: &mut Buffer) -> GDResult> { 90 | let mut players: Vec = Vec::new(); 91 | 92 | // this needs to be looked at again as theres no way to check if the buffer has 93 | // a remaining null byte the original code was: 94 | // while !bufferer.is_remaining_empty() && bufferer.remaining_data() != [0x00] 95 | while !bufferer.remaining_length() == 0 { 96 | let data = bufferer.read_string::(Some([0x0A]))?; 97 | let data_split = data.split(' ').collect::>(); 98 | let data_iter = data_split.iter(); 99 | 100 | players.push(Client::parse_player_string(data_iter)?); 101 | } 102 | 103 | Ok(players) 104 | } 105 | 106 | pub fn client_query( 107 | address: &SocketAddr, 108 | timeout_settings: Option, 109 | ) -> GDResult> { 110 | let data = get_data::(address, &timeout_settings)?; 111 | let mut bufferer = Buffer::::new(&data); 112 | 113 | let mut server_vars = get_server_values(&mut bufferer)?; 114 | let players = get_players::(&mut bufferer)?; 115 | 116 | Ok(Response { 117 | name: server_vars 118 | .remove("hostname") 119 | .or_else(|| server_vars.remove("sv_hostname")) 120 | .ok_or(GDErrorKind::PacketBad)?, 121 | map: server_vars 122 | .remove("mapname") 123 | .or_else(|| server_vars.remove("map")) 124 | .ok_or(GDErrorKind::PacketBad)?, 125 | players_online: players.len() as u8, 126 | players_maximum: server_vars 127 | .remove("maxclients") 128 | .or_else(|| server_vars.remove("sv_maxclients")) 129 | .ok_or(GDErrorKind::PacketBad)? 130 | .parse() 131 | .map_err(|e| TypeParse.context(e))?, 132 | players, 133 | game_version: server_vars 134 | .remove("version") 135 | .or_else(|| server_vars.remove("*version")), 136 | unused_entries: server_vars, 137 | }) 138 | } 139 | 140 | pub fn remove_wrapping_quotes<'a>(string: &&'a str) -> &'a str { 141 | match string.starts_with('\"') && string.ends_with('\"') { 142 | false => string, 143 | true => &string[1 .. string.len() - 1], 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /crates/lib/src/protocols/quake/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "serde")] 2 | use serde::{Deserialize, Serialize}; 3 | 4 | pub mod one; 5 | pub mod three; 6 | pub mod two; 7 | 8 | /// All types used by the implementation. 9 | pub mod types; 10 | pub use types::*; 11 | 12 | mod client; 13 | 14 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 15 | #[derive(Debug, Clone, PartialEq, Eq)] 16 | pub enum QuakeVersion { 17 | One, 18 | Two, 19 | Three, 20 | } 21 | 22 | /// Generate a module containing a query function for a quake game. 23 | /// 24 | /// * `mod_name` - The name to be given to the game module (see ID naming 25 | /// conventions in CONTRIBUTING.md). 26 | /// * `pretty_name` - The full name of the game, will be used as the 27 | /// documentation for the created module. 28 | /// * `quake_ver`, `default_port` - Passed through to [game_query_fn]. 29 | #[cfg(feature = "games")] 30 | macro_rules! game_query_mod { 31 | ($mod_name: ident, $pretty_name: expr, $quake_ver: ident, $default_port: literal) => { 32 | #[doc = $pretty_name] 33 | pub mod $mod_name { 34 | crate::protocols::quake::game_query_fn!($quake_ver, $default_port); 35 | } 36 | }; 37 | } 38 | 39 | #[cfg(feature = "games")] 40 | pub(crate) use game_query_mod; 41 | 42 | // Allow generating doc comments: 43 | // https://users.rust-lang.org/t/macros-filling-text-in-comments/20473 44 | /// Generate a query function for a quake game. 45 | /// 46 | /// * `quake_ver` - The name of the [module](crate::protocols::quake) for the 47 | /// quake version the game uses. 48 | /// * `default_port` - The default port the game uses. 49 | /// 50 | /// ```rust,ignore 51 | /// use crate::protocols::quake::game_query_fn; 52 | /// game_query_fn!(one, 27500); 53 | /// ``` 54 | #[cfg(feature = "games")] 55 | macro_rules! game_query_fn { 56 | ($quake_ver: ident, $default_port: literal) => { 57 | use crate::protocols::quake::$quake_ver::Player; 58 | crate::protocols::quake::game_query_fn! {@gen $quake_ver, Player, $default_port, concat!( 59 | "Make a quake ", stringify!($quake_ver), " query with default timeout settings.\n\n", 60 | "If port is `None`, then the default port (", stringify!($default_port), ") will be used.")} 61 | }; 62 | 63 | (@gen $quake_ver: ident, $player_type: ty, $default_port: literal, $doc: expr) => { 64 | #[doc = $doc] 65 | pub fn query( 66 | address: &std::net::IpAddr, 67 | port: Option, 68 | ) -> crate::GDResult> { 69 | crate::protocols::quake::$quake_ver::query( 70 | &std::net::SocketAddr::new(*address, port.unwrap_or($default_port)), 71 | None, 72 | ) 73 | } 74 | }; 75 | } 76 | 77 | #[cfg(feature = "games")] 78 | pub(crate) use game_query_fn; 79 | -------------------------------------------------------------------------------- /crates/lib/src/protocols/quake/one.rs: -------------------------------------------------------------------------------- 1 | use crate::protocols::quake::client::{client_query, remove_wrapping_quotes, QuakeClient}; 2 | use crate::protocols::quake::Response; 3 | use crate::protocols::types::{CommonPlayer, GenericPlayer, TimeoutSettings}; 4 | use crate::GDErrorKind::TypeParse; 5 | use crate::{GDErrorKind, GDResult}; 6 | #[cfg(feature = "serde")] 7 | use serde::{Deserialize, Serialize}; 8 | use std::net::SocketAddr; 9 | use std::slice::Iter; 10 | 11 | use super::QuakePlayerType; 12 | 13 | /// Quake 1 player data. 14 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 15 | #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 16 | pub struct Player { 17 | /// Player's server id. 18 | pub id: u8, 19 | pub score: u16, 20 | pub time: u16, 21 | pub ping: u16, 22 | pub name: String, 23 | pub skin: String, 24 | pub color_primary: u8, 25 | pub color_secondary: u8, 26 | } 27 | 28 | impl QuakePlayerType for Player { 29 | fn version(response: &Response) -> super::VersionedResponse { super::VersionedResponse::One(response) } 30 | } 31 | 32 | impl CommonPlayer for Player { 33 | fn as_original(&self) -> GenericPlayer { GenericPlayer::QuakeOne(self) } 34 | 35 | fn name(&self) -> &str { &self.name } 36 | fn score(&self) -> Option { Some(self.score.into()) } 37 | } 38 | 39 | pub(crate) struct QuakeOne; 40 | impl QuakeClient for QuakeOne { 41 | type Player = Player; 42 | 43 | fn get_send_header<'a>() -> &'a str { "status" } 44 | 45 | fn get_response_header<'a>() -> &'a str { "n" } 46 | 47 | fn parse_player_string(mut data: Iter<&str>) -> GDResult { 48 | Ok(Player { 49 | id: match data.next() { 50 | None => Err(GDErrorKind::PacketBad)?, 51 | Some(v) => v.parse().map_err(|e| TypeParse.context(e))?, 52 | }, 53 | score: match data.next() { 54 | None => Err(GDErrorKind::PacketBad)?, 55 | Some(v) => v.parse().map_err(|e| TypeParse.context(e))?, 56 | }, 57 | time: match data.next() { 58 | None => Err(GDErrorKind::PacketBad)?, 59 | Some(v) => v.parse().map_err(|e| TypeParse.context(e))?, 60 | }, 61 | ping: match data.next() { 62 | None => Err(GDErrorKind::PacketBad)?, 63 | Some(v) => v.parse().map_err(|e| TypeParse.context(e))?, 64 | }, 65 | name: match data.next() { 66 | None => Err(GDErrorKind::PacketBad)?, 67 | Some(v) => remove_wrapping_quotes(v).to_string(), 68 | }, 69 | skin: match data.next() { 70 | None => Err(GDErrorKind::PacketBad)?, 71 | Some(v) => remove_wrapping_quotes(v).to_string(), 72 | }, 73 | color_primary: match data.next() { 74 | None => Err(GDErrorKind::PacketBad)?, 75 | Some(v) => v.parse().map_err(|e| TypeParse.context(e))?, 76 | }, 77 | color_secondary: match data.next() { 78 | None => Err(GDErrorKind::PacketBad)?, 79 | Some(v) => v.parse().map_err(|e| TypeParse.context(e))?, 80 | }, 81 | }) 82 | } 83 | } 84 | 85 | pub fn query(address: &SocketAddr, timeout_settings: Option) -> GDResult> { 86 | client_query::(address, timeout_settings) 87 | } 88 | -------------------------------------------------------------------------------- /crates/lib/src/protocols/quake/three.rs: -------------------------------------------------------------------------------- 1 | use crate::protocols::quake::client::{client_query, QuakeClient}; 2 | use crate::protocols::quake::two::QuakeTwo; 3 | use crate::protocols::quake::Response; 4 | use crate::protocols::types::TimeoutSettings; 5 | use crate::GDResult; 6 | use std::net::SocketAddr; 7 | use std::slice::Iter; 8 | 9 | pub use crate::protocols::quake::two::Player; 10 | 11 | struct QuakeThree; 12 | impl QuakeClient for QuakeThree { 13 | type Player = Player; 14 | 15 | fn get_send_header<'a>() -> &'a str { "getstatus" } 16 | 17 | fn get_response_header<'a>() -> &'a str { "statusResponse\n" } 18 | 19 | fn parse_player_string(data: Iter<&str>) -> GDResult { QuakeTwo::parse_player_string(data) } 20 | } 21 | 22 | pub fn query(address: &SocketAddr, timeout_settings: Option) -> GDResult> { 23 | client_query::(address, timeout_settings) 24 | } 25 | -------------------------------------------------------------------------------- /crates/lib/src/protocols/quake/two.rs: -------------------------------------------------------------------------------- 1 | use crate::protocols::quake::client::{client_query, remove_wrapping_quotes, QuakeClient}; 2 | use crate::protocols::quake::one::QuakeOne; 3 | use crate::protocols::quake::Response; 4 | use crate::protocols::types::{CommonPlayer, GenericPlayer, TimeoutSettings}; 5 | use crate::GDErrorKind::TypeParse; 6 | use crate::{GDErrorKind, GDResult}; 7 | #[cfg(feature = "serde")] 8 | use serde::{Deserialize, Serialize}; 9 | use std::net::SocketAddr; 10 | use std::slice::Iter; 11 | 12 | use super::QuakePlayerType; 13 | 14 | /// Quake 2 player data. 15 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 16 | #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 17 | pub struct Player { 18 | pub score: i32, 19 | pub ping: u16, 20 | pub name: String, 21 | pub address: Option, 22 | } 23 | 24 | impl QuakePlayerType for Player { 25 | fn version(response: &Response) -> super::VersionedResponse { 26 | super::VersionedResponse::TwoAndThree(response) 27 | } 28 | } 29 | 30 | impl CommonPlayer for Player { 31 | fn as_original(&self) -> GenericPlayer { GenericPlayer::QuakeTwo(self) } 32 | 33 | fn name(&self) -> &str { &self.name } 34 | 35 | fn score(&self) -> Option { Some(self.score) } 36 | } 37 | 38 | pub(crate) struct QuakeTwo; 39 | impl QuakeClient for QuakeTwo { 40 | type Player = Player; 41 | 42 | fn get_send_header<'a>() -> &'a str { QuakeOne::get_send_header() } 43 | 44 | fn get_response_header<'a>() -> &'a str { "print\n" } 45 | 46 | fn parse_player_string(mut data: Iter<&str>) -> GDResult { 47 | Ok(Player { 48 | score: match data.next() { 49 | None => Err(GDErrorKind::PacketBad)?, 50 | Some(v) => v.parse().map_err(|e| TypeParse.context(e))?, 51 | }, 52 | ping: match data.next() { 53 | None => Err(GDErrorKind::PacketBad)?, 54 | Some(v) => v.parse().map_err(|e| TypeParse.context(e))?, 55 | }, 56 | name: match data.next() { 57 | None => Err(GDErrorKind::PacketBad)?, 58 | Some(v) => remove_wrapping_quotes(v).to_string(), 59 | }, 60 | address: data.next().map(|v| remove_wrapping_quotes(v).to_string()), 61 | }) 62 | } 63 | } 64 | 65 | pub fn query(address: &SocketAddr, timeout_settings: Option) -> GDResult> { 66 | client_query::(address, timeout_settings) 67 | } 68 | -------------------------------------------------------------------------------- /crates/lib/src/protocols/quake/types.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "serde")] 2 | use serde::{Deserialize, Serialize}; 3 | use std::collections::HashMap; 4 | 5 | use crate::protocols::{ 6 | types::{CommonPlayer, CommonResponse}, 7 | GenericResponse, 8 | }; 9 | 10 | /// General server information's. 11 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 12 | #[derive(Debug, Clone, PartialEq, Eq)] 13 | pub struct Response

{ 14 | /// Name of the server. 15 | pub name: String, 16 | /// Map name. 17 | pub map: String, 18 | /// Current online players. 19 | pub players: Vec

, 20 | /// Number of players on the server. 21 | pub players_online: u8, 22 | /// Maximum number of players the server reports it can hold. 23 | pub players_maximum: u8, 24 | /// The server version. 25 | pub game_version: Option, 26 | /// Other server entries that weren't used. 27 | pub unused_entries: HashMap, 28 | } 29 | 30 | pub trait QuakePlayerType: Sized + CommonPlayer { 31 | fn version(response: &Response) -> VersionedResponse; 32 | } 33 | 34 | impl CommonResponse for Response

{ 35 | fn as_original(&self) -> GenericResponse { GenericResponse::Quake(P::version(self)) } 36 | 37 | fn name(&self) -> Option<&str> { Some(&self.name) } 38 | fn game_version(&self) -> Option<&str> { self.game_version.as_deref() } 39 | fn map(&self) -> Option<&str> { Some(&self.map) } 40 | fn players_maximum(&self) -> u32 { self.players_maximum.into() } 41 | fn players_online(&self) -> u32 { self.players_online.into() } 42 | 43 | fn players(&self) -> Option> { 44 | Some( 45 | self.players 46 | .iter() 47 | .map(|p| p as &dyn CommonPlayer) 48 | .collect(), 49 | ) 50 | } 51 | } 52 | 53 | /// Versioned response type 54 | #[cfg_attr(feature = "serde", derive(Serialize))] 55 | #[derive(Debug, Clone, PartialEq, Eq)] 56 | pub enum VersionedResponse<'a> { 57 | One(&'a Response), 58 | TwoAndThree(&'a Response), 59 | } 60 | -------------------------------------------------------------------------------- /crates/lib/src/protocols/unreal2/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 a valve 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 | /// * `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) => { 19 | #[doc = $pretty_name] 20 | pub mod $mod_name { 21 | crate::protocols::unreal2::game_query_fn!($default_port); 22 | } 23 | }; 24 | } 25 | 26 | #[cfg(feature = "games")] 27 | pub(crate) use game_query_mod; 28 | 29 | // Allow generating doc comments: 30 | // https://users.rust-lang.org/t/macros-filling-text-in-comments/20473 31 | /// Generate a query function for a valve game. 32 | /// 33 | /// * `default_port` - The default port the game uses. 34 | #[cfg(feature = "games")] 35 | macro_rules! game_query_fn { 36 | ($default_port: literal) => { 37 | crate::protocols::unreal2::game_query_fn! {@gen $default_port, concat!( 38 | "Make a Unreal2 query for with default timeout settings and default extra request settings.\n\n", 39 | "If port is `None`, then the default port (", stringify!($default_port), ") will be used.")} 40 | }; 41 | 42 | (@gen $default_port: literal, $doc: expr) => { 43 | #[doc = $doc] 44 | pub fn query( 45 | address: &std::net::IpAddr, 46 | port: Option, 47 | ) -> crate::GDResult { 48 | crate::protocols::unreal2::query( 49 | &std::net::SocketAddr::new(*address, port.unwrap_or($default_port)), 50 | &crate::protocols::unreal2::GatheringSettings::default(), 51 | None, 52 | ) 53 | } 54 | }; 55 | } 56 | 57 | #[cfg(feature = "games")] 58 | pub(crate) use game_query_fn; 59 | -------------------------------------------------------------------------------- /crates/lib/src/protocols/valve/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 a valve 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, $engine: expr, $default_port: literal) => { 19 | crate::protocols::valve::game_query_mod!( 20 | $mod_name, 21 | $pretty_name, 22 | $engine, 23 | $default_port, 24 | GatheringSettings::default() 25 | ); 26 | }; 27 | 28 | ($mod_name: ident, $pretty_name: expr, $engine: expr, $default_port: literal, $gathering_settings: expr) => { 29 | #[doc = $pretty_name] 30 | pub mod $mod_name { 31 | #[allow(unused_imports)] 32 | use crate::protocols::{ 33 | types::GatherToggle, 34 | valve::{Engine, GatheringSettings}, 35 | }; 36 | 37 | crate::protocols::valve::game_query_fn!($pretty_name, $engine, $default_port, $gathering_settings); 38 | } 39 | }; 40 | } 41 | 42 | #[cfg(feature = "games")] 43 | pub(crate) use game_query_mod; 44 | 45 | // Allow generating doc comments: 46 | // https://users.rust-lang.org/t/macros-filling-text-in-comments/20473 47 | /// Generate a query function for a valve game. 48 | /// 49 | /// * `engine` - The [Engine] that the game uses. 50 | /// * `default_port` - The default port the game uses. 51 | /// 52 | /// ```rust,ignore 53 | /// use crate::protocols::valve::game_query_fn; 54 | /// game_query_fn!(TEAMFORTRESS2, 27015); 55 | /// ``` 56 | #[cfg(feature = "games")] 57 | macro_rules! game_query_fn { 58 | ($pretty_name: expr, $engine: expr, $default_port: literal, $gathering_settings: expr) => { 59 | // TODO: By using $gathering_settings, also add to doc if a game doesnt respond to certain gathering settings 60 | crate::protocols::valve::game_query_fn!{@gen $engine, $default_port, concat!( 61 | "Make a valve query for ", $pretty_name, " with default timeout settings and default extra request settings.\n\n", 62 | "If port is `None`, then the default port (", stringify!($default_port), ") will be used."), $gathering_settings} 63 | }; 64 | 65 | (@gen $engine: expr, $default_port: literal, $doc: expr, $gathering_settings: expr) => { 66 | #[doc = $doc] 67 | pub fn query(address: &std::net::IpAddr, port: Option) -> crate::GDResult { 68 | let valve_response = crate::protocols::valve::query( 69 | &std::net::SocketAddr::new(*address, port.unwrap_or($default_port)), 70 | $engine, 71 | Some($gathering_settings), 72 | None, 73 | )?; 74 | 75 | Ok(crate::protocols::valve::game::Response::new_from_valve_response(valve_response)) 76 | } 77 | }; 78 | } 79 | 80 | #[cfg(feature = "games")] 81 | pub(crate) use game_query_fn; 82 | -------------------------------------------------------------------------------- /crates/lib/src/services/minetest_master_server/mod.rs: -------------------------------------------------------------------------------- 1 | /// The implementation. 2 | pub mod service; 3 | /// All types used by the implementation. 4 | pub mod types; 5 | 6 | pub use service::*; 7 | pub use types::*; 8 | -------------------------------------------------------------------------------- /crates/lib/src/services/minetest_master_server/service.rs: -------------------------------------------------------------------------------- 1 | use crate::http::HttpClient; 2 | use crate::minetest_master_server::types::Response; 3 | use crate::{GDResult, TimeoutSettings}; 4 | 5 | pub fn query(timeout_settings: TimeoutSettings) -> GDResult { 6 | let mut client = HttpClient::from_url( 7 | "https://servers.minetest.net", 8 | &Some(timeout_settings), 9 | None, 10 | )?; 11 | 12 | client.get_json("/list", None) 13 | } 14 | -------------------------------------------------------------------------------- /crates/lib/src/services/minetest_master_server/types.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug, PartialEq)] 4 | pub struct Server { 5 | pub address: String, 6 | pub clients: u32, 7 | pub clients_list: Option>, 8 | pub clients_max: u32, 9 | pub creative: Option, 10 | pub damage: bool, 11 | pub description: String, 12 | pub game_time: u32, 13 | pub gameid: String, 14 | pub lag: Option, 15 | pub name: String, 16 | pub password: Option, 17 | pub port: u16, 18 | pub proto_max: u16, 19 | pub proto_min: u16, 20 | pub pvp: bool, 21 | pub uptime: u32, 22 | pub url: Option, 23 | pub version: String, 24 | pub ip: String, 25 | pub update_time: u32, 26 | pub start: u32, 27 | pub clients_top: u32, 28 | pub updates: u32, 29 | pub total_clients: u32, 30 | pub pop_v: f32, 31 | pub geo_continent: Option, 32 | pub ping: f32, 33 | } 34 | 35 | #[derive(Serialize, Deserialize, Debug)] 36 | pub struct ServersClients { 37 | pub servers: u32, 38 | pub clients: u32, 39 | } 40 | 41 | #[derive(Serialize, Deserialize, Debug)] 42 | pub struct Response { 43 | pub total: ServersClients, 44 | pub total_max: ServersClients, 45 | pub list: Vec, 46 | } 47 | -------------------------------------------------------------------------------- /crates/lib/src/services/mod.rs: -------------------------------------------------------------------------------- 1 | //! Services that are currently implemented. 2 | 3 | /// Reference: [Master Server Query Protocol](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol) 4 | pub mod valve_master_server; 5 | 6 | /// Reference: [Node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/minetest.js) 7 | #[cfg(all(feature = "serde", feature = "tls"))] 8 | pub mod minetest_master_server; 9 | -------------------------------------------------------------------------------- /crates/lib/src/services/valve_master_server/mod.rs: -------------------------------------------------------------------------------- 1 | /// The implementation. 2 | pub mod service; 3 | /// All types used by the implementation. 4 | pub mod types; 5 | 6 | pub use service::*; 7 | pub use types::*; 8 | -------------------------------------------------------------------------------- /crates/lib/src/services/valve_master_server/service.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | buffer::Buffer, 3 | socket::{Socket, UdpSocket}, 4 | valve_master_server::{Region, SearchFilters}, 5 | GDErrorKind::PacketBad, 6 | GDResult, 7 | }; 8 | 9 | use std::net::{IpAddr, Ipv4Addr, SocketAddr}; 10 | 11 | use byteorder::BigEndian; 12 | 13 | /// The default master ip, which is the one for Source. 14 | pub fn default_master_address() -> SocketAddr { 15 | SocketAddr::new(IpAddr::V4(Ipv4Addr::new(208, 64, 201, 194)), 27011) // hl2master.steampowered.com 16 | } 17 | 18 | fn construct_payload(region: Region, filters: &Option, last_ip: &str, last_port: u16) -> Vec { 19 | let filters_bytes: Vec = filters 20 | .as_ref() 21 | .map_or_else(|| vec![0x00], SearchFilters::to_bytes); 22 | 23 | let region_byte = &[region as u8]; 24 | 25 | [ 26 | // Packet has to begin with the character '1' 27 | &[0x31], 28 | // The region byte is next 29 | region_byte, 30 | // The last fetched ip as a string 31 | last_ip.as_bytes(), 32 | // Followed by an ':' 33 | b":", 34 | // And the port, as a string 35 | last_port.to_string().as_bytes(), 36 | // Which needs to end with a NULL byte 37 | &[0x00], 38 | // Then the filters 39 | &filters_bytes, 40 | ] 41 | .concat() 42 | } 43 | 44 | /// The implementation, use this if you want to keep the same socket. 45 | pub struct ValveMasterServer { 46 | socket: UdpSocket, 47 | } 48 | 49 | impl ValveMasterServer { 50 | /// Construct a new struct. 51 | pub fn new(master_address: &SocketAddr) -> GDResult { 52 | let socket = UdpSocket::new(master_address, &None)?; 53 | 54 | Ok(Self { socket }) 55 | } 56 | 57 | /// Make just a single query, providing `0.0.0.0` as the last ip and `0` as 58 | /// the last port will give the initial packet. 59 | pub fn query_specific( 60 | &mut self, 61 | region: Region, 62 | search_filters: &Option, 63 | last_address_ip: &str, 64 | last_address_port: u16, 65 | ) -> GDResult> { 66 | let payload = construct_payload(region, search_filters, last_address_ip, last_address_port); 67 | self.socket.send(&payload)?; 68 | 69 | let received_data = self.socket.receive(Some(1400))?; 70 | let mut buf = Buffer::::new(&received_data); 71 | 72 | if buf.read::()? != u32::MAX || buf.read::()? != 26122 { 73 | return Err(PacketBad.context("Expected 4294967295 followed by 26122")); 74 | } 75 | 76 | let mut ips: Vec<(IpAddr, u16)> = Vec::new(); 77 | 78 | while buf.remaining_length() > 0 { 79 | let ip = IpAddr::V4(Ipv4Addr::new( 80 | buf.read::()?, 81 | buf.read::()?, 82 | buf.read::()?, 83 | buf.read::()?, 84 | )); 85 | let port = buf.read::()?; 86 | 87 | ips.push((ip, port)); 88 | } 89 | 90 | Ok(ips) 91 | } 92 | 93 | /// Make a complete query. 94 | pub fn query(&mut self, region: Region, search_filters: Option) -> GDResult> { 95 | let mut ips: Vec<(IpAddr, u16)> = Vec::new(); 96 | 97 | let mut exit_fetching = false; 98 | let mut last_ip: String = "0.0.0.0".to_string(); 99 | let mut last_port: u16 = 0; 100 | 101 | while !exit_fetching { 102 | let new_ips = self.query_specific(region, &search_filters, last_ip.as_str(), last_port)?; 103 | 104 | match new_ips.last() { 105 | None => exit_fetching = true, 106 | Some((latest_ip, latest_port)) => { 107 | let mut remove_last = false; 108 | 109 | let latest_ip_string = latest_ip.to_string(); 110 | if latest_ip_string == "0.0.0.0" && *latest_port == 0 { 111 | exit_fetching = true; 112 | remove_last = true; 113 | } else if latest_ip_string == last_ip && *latest_port == last_port { 114 | exit_fetching = true; 115 | } else { 116 | last_ip = latest_ip_string; 117 | last_port = *latest_port; 118 | } 119 | 120 | ips.extend(new_ips); 121 | if remove_last { 122 | ips.pop(); 123 | } 124 | } 125 | } 126 | } 127 | 128 | Ok(ips) 129 | } 130 | } 131 | 132 | /// Take only the first response of (what would be a) complete query. This is 133 | /// faster as it results in less packets being sent, received and processed but 134 | /// yields less ips. 135 | pub fn query_singular(region: Region, search_filters: Option) -> GDResult> { 136 | let mut master_server = ValveMasterServer::new(&default_master_address())?; 137 | 138 | let mut ips = master_server.query_specific(region, &search_filters, "0.0.0.0", 0)?; 139 | 140 | if let Some((last_ip, last_port)) = ips.last() { 141 | if last_ip.to_string() == "0.0.0.0" && *last_port == 0 { 142 | ips.pop(); 143 | } 144 | } 145 | 146 | Ok(ips) 147 | } 148 | 149 | /// Make a complete query. 150 | pub fn query(region: Region, search_filters: Option) -> GDResult> { 151 | let mut master_server = ValveMasterServer::new(&default_master_address())?; 152 | 153 | master_server.query(region, search_filters) 154 | } 155 | -------------------------------------------------------------------------------- /crates/lib/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::GDErrorKind::{PacketOverflow, PacketReceive, PacketSend, PacketUnderflow}; 2 | use crate::GDResult; 3 | use std::cmp::Ordering; 4 | 5 | pub fn error_by_expected_size(expected: usize, size: usize) -> GDResult<()> { 6 | match size.cmp(&expected) { 7 | Ordering::Greater => Err(PacketOverflow.into()), 8 | Ordering::Less => Err(PacketUnderflow.into()), 9 | Ordering::Equal => Ok(()), 10 | } 11 | } 12 | 13 | pub const fn u8_lower_upper(n: u8) -> (u8, u8) { (n & 15, n >> 4) } 14 | 15 | /// Run a closure `retry_count+1` times while it returns [PacketReceive] or 16 | /// [PacketSend] errors, returning the first success, other Error, or after 17 | /// `retry_count+1` tries the last [PacketReceive] or [PacketSend] error. 18 | pub fn retry_on_timeout(mut retry_count: usize, mut fetch: impl FnMut() -> GDResult) -> GDResult { 19 | let mut last_err = PacketReceive.context("Retry count was 0"); 20 | retry_count += 1; 21 | while retry_count > 0 { 22 | last_err = match fetch() { 23 | Ok(r) => return Ok(r), 24 | Err(e) if e.kind == PacketReceive || e.kind == PacketSend => e, 25 | Err(e) => return Err(e), 26 | }; 27 | retry_count -= 1; 28 | } 29 | Err(last_err) 30 | } 31 | 32 | /// Run gather_fn based on the value of gather_toggle. 33 | /// 34 | /// # Parameters 35 | /// - `gather_toggle` should be an expression resolving to a 36 | /// [crate::protocols::types::GatherToggle]. 37 | /// - `gather_fn` should be an expression that returns a [crate::GDResult]. 38 | /// 39 | /// # States 40 | /// - [DontGather](crate::protocols::types::GatherToggle::DontGather) - Don't 41 | /// run gather function, returns None. 42 | /// - [AttemptGather](crate::protocols::types::GatherToggle::AttemptGather) - 43 | /// Runs the gather function, if it returns an error return None, else return 44 | /// Some. 45 | /// - [Required](crate::protocols::types::GatherToggle::Required) - Runs the 46 | /// gather function, if it returns an error propagate it using the `?` 47 | /// operator, else return Some. 48 | /// 49 | /// # Examples 50 | /// 51 | /// ```ignore,Doctests cannot access private items 52 | /// use gamedig::protocols::types::GatherToggle; 53 | /// use gamedig::utils::maybe_gather; 54 | /// 55 | /// let query_fn = || { Err("Query error") }; 56 | /// 57 | /// // query_fn() is not called 58 | /// let response = maybe_gather!(GatherToggle::DontGather, query_fn()); 59 | /// assert!(response.is_none()); 60 | /// 61 | /// // query_fn() is called but Err is converted to None 62 | /// let response = maybe_gather!(GatherToggle::AttemptGather, query_fn()); 63 | /// assert!(response.is_none()); 64 | /// 65 | /// // query_fn() is called and Err is propagated. 66 | /// let response = maybe_gather!(GatherToggle::Required, query_fn()); 67 | /// unreachable!(); 68 | /// ``` 69 | macro_rules! maybe_gather { 70 | ($gather_toggle: expr, $gather_fn: expr) => { 71 | match $gather_toggle { 72 | crate::protocols::types::GatherToggle::Skip => None, 73 | crate::protocols::types::GatherToggle::Try => $gather_fn.ok(), 74 | crate::protocols::types::GatherToggle::Enforce => Some($gather_fn?), 75 | } 76 | }; 77 | } 78 | 79 | pub(crate) use maybe_gather; 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::retry_on_timeout; 84 | use crate::{ 85 | protocols::types::GatherToggle, 86 | GDError, 87 | GDErrorKind::{self, PacketBad, PacketReceive, PacketSend}, 88 | GDResult, 89 | }; 90 | 91 | #[test] 92 | fn u8_lower_upper() { 93 | assert_eq!(super::u8_lower_upper(171), (11, 10)); 94 | } 95 | 96 | #[test] 97 | fn error_by_expected_size() { 98 | assert!(super::error_by_expected_size(69, 69).is_ok()); 99 | assert!(super::error_by_expected_size(69, 68).is_err()); 100 | assert!(super::error_by_expected_size(69, 70).is_err()); 101 | } 102 | 103 | #[test] 104 | fn retry_success_on_first() { 105 | let r = retry_on_timeout(0, || Ok(())); 106 | assert!(r.is_ok()); 107 | } 108 | 109 | #[test] 110 | fn retry_no_success() { 111 | let r: GDResult<()> = retry_on_timeout(100, || Err(PacketSend.context("test"))); 112 | assert!(r.is_err()); 113 | assert_eq!(r.unwrap_err().kind, PacketSend); 114 | } 115 | 116 | #[test] 117 | fn retry_success_on_third() { 118 | let mut i = 0u8; 119 | let r = retry_on_timeout(2, || { 120 | i += 1; 121 | if i < 3 { 122 | Err(PacketReceive.context("test")) 123 | } else { 124 | Ok(()) 125 | } 126 | }); 127 | assert!(r.is_ok()); 128 | } 129 | 130 | #[test] 131 | fn retry_success_on_third_but_less_retries() { 132 | let mut i = 0u8; 133 | let r = retry_on_timeout(1, || { 134 | i += 1; 135 | if i < 3 { 136 | Err(PacketReceive.context("test")) 137 | } else { 138 | Ok(()) 139 | } 140 | }); 141 | assert!(r.is_err()); 142 | assert_eq!(r.unwrap_err().kind, PacketReceive); 143 | } 144 | 145 | #[test] 146 | fn retry_with_non_timeout_error() { 147 | let mut i = 0u8; 148 | let r = retry_on_timeout(50, || { 149 | i += 1; 150 | match i { 151 | 1 => Err(PacketSend.context("test")), 152 | 2 => Err(PacketBad.context("test")), 153 | _ => Ok(()), 154 | } 155 | }); 156 | assert!(r.is_err()); 157 | assert_eq!(r.unwrap_err().kind, PacketBad); 158 | } 159 | 160 | fn gather_success(n: i32) -> GDResult { Ok(n) } 161 | 162 | fn gather_fail(err: &'static str) -> GDResult { Err(GDErrorKind::PacketSend.context(err)) } 163 | 164 | #[test] 165 | fn gather_success_dont_gather() -> GDResult<()> { 166 | let result = maybe_gather!(GatherToggle::Skip, gather_success(5)); 167 | assert!(result.is_none()); 168 | Ok(()) 169 | } 170 | 171 | #[test] 172 | fn gather_success_attempt_gather() -> GDResult<()> { 173 | let result = maybe_gather!(GatherToggle::Try, gather_success(10)); 174 | assert_eq!(result, Some(10)); 175 | Ok(()) 176 | } 177 | 178 | #[test] 179 | fn gather_success_required() -> GDResult<()> { 180 | let result = maybe_gather!(GatherToggle::Enforce, gather_success(15)); 181 | assert_eq!(result, Some(15)); 182 | Ok(()) 183 | } 184 | 185 | #[test] 186 | fn gather_fail_dont_gather() -> GDResult<()> { 187 | let result = maybe_gather!(GatherToggle::Skip, gather_fail("dont")); 188 | assert!(result.is_none()); 189 | Ok(()) 190 | } 191 | 192 | #[test] 193 | fn gather_fail_attempt_gather() -> GDResult<()> { 194 | let result = maybe_gather!(GatherToggle::Try, gather_fail("attempt")); 195 | assert!(result.is_none()); 196 | Ok(()) 197 | } 198 | 199 | #[test] 200 | fn gather_fail_required() { 201 | let inner = || { 202 | let result = maybe_gather!(GatherToggle::Enforce, gather_fail("required")); 203 | assert_eq!(result, Some(10)); 204 | Ok::<(), GDError>(()) 205 | }; 206 | assert!(inner().is_err()); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /crates/lib/tests/game_ids.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(test, feature = "game_defs"))] 2 | 3 | use gamedig::GAMES; 4 | 5 | use gamedig_id_tests::test_game_name_rules; 6 | 7 | #[test] 8 | fn check_definitions_match_name_rules() { 9 | let wrong = test_game_name_rules(GAMES.entries().map(|(id, game)| (id.to_owned(), game.name))); 10 | assert!(wrong.is_empty()); 11 | } 12 | --------------------------------------------------------------------------------