diff --git a/.actrc b/.actrc deleted file mode 100644 index 8af6fb7..0000000 --- a/.actrc +++ /dev/null @@ -1,8 +0,0 @@ -# Configuration file for act (run github actions locally using docker) -# https://github.com/nektos/act - -# Swap docker image for the one containing the rust toolchain --P ubuntu-latest=ghcr.io/catthehacker/ubuntu:rust-latest - -# Load custom event --e .github/.act-event.json diff --git a/.github/.act-event.json b/.github/.act-event.json deleted file mode 100644 index 22e66fc..0000000 --- a/.github/.act-event.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "act": true, - "repository": { - "default_branch": "main" - } -} diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index fbe8429..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: Bug report -about: Create a report for a found bug -title: '' -labels: bug -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**Steps To Reproduce** -Please provide the steps to reproduce the behavior (if not possible, describe as many details as possible). - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots or Data** -If applicable, add screenshots/data to help explain your problem. - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index a9ae259..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Feature request -about: Suggest a feature -title: '' -labels: enhancement -assignees: '' - ---- - -**What is this feature about?** -Shortly explain what your requested feature is about. - -**Additional context/references** -Add any other context or references about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/help-regarding-code-protocol-errors.md b/.github/ISSUE_TEMPLATE/help-regarding-code-protocol-errors.md deleted file mode 100644 index 88c286f..0000000 --- a/.github/ISSUE_TEMPLATE/help-regarding-code-protocol-errors.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: Help regarding code/protocol errors -about: Use this if you can't figure out how to use something. -title: '' -labels: '' -assignees: '' - ---- - -**This issue shall be made only if you have already gone through the docs, have you done it?** -Please state if there is something confusing regarding the docs (eg. location or wording). - -**What's you problem?** -State as concise as possible what you want to do and can't do. - -**Suggestions to make this clearer** -Mention how could stuff be improved so that someone doesn't have the same problem as you (eg. Error should give more information). diff --git a/.github/badges/.gitkeep b/.github/badges/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/.github/badges/node.svg b/.github/badges/node.svg deleted file mode 100644 index 8b8ae49..0000000 --- a/.github/badges/node.svg +++ /dev/null @@ -1,20 +0,0 @@ - - Node game coverage: 23.46% - - - - - - - - - - - - - \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index bdb8e27..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/.github/workflows" - schedule: - interval: "daily" - - - package-ecosystem: "cargo" - directory: "/crates/cli" - schedule: - interval: "daily" - - - package-ecosystem: "cargo" - directory: "/crates/lib" - schedule: - interval: "daily" diff --git a/.github/labeler.yml b/.github/labeler.yml deleted file mode 100644 index 76326b3..0000000 --- a/.github/labeler.yml +++ /dev/null @@ -1,18 +0,0 @@ -ci: - - .github/workflows/** - - .github/labeler.yml - - .actrc - - .pre-commit-config.yaml - -protocol: - - crates/lib/src/protocols/** - -game: - - crates/lib/src/games/** - -cli: - - crates/cli/** - -crate: - - Cargo.toml - - Cargo.lock diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml deleted file mode 100644 index 5f71714..0000000 --- a/.github/workflows/audit.yml +++ /dev/null @@ -1,20 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/softprops/github-actions-schemas/master/workflow.json -name: Security audit -on: - push: - paths: - - "**/Cargo.toml" - - "**/Cargo.lock" -jobs: - security_audit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - - name: Generate Cargo.lock # https://github.com/rustsec/audit-check/issues/27 - run: cargo generate-lockfile - - - name: Audit Check - uses: rustsec/audit-check@v2.0.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 89dbef3..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,199 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/softprops/github-actions-schemas/master/workflow.json -name: CI - -on: - push: - branches: ["main"] - paths: - - "**.rs" # Any rust file - - "**/Cargo.toml" # Any Cargo.toml - - ".rustfmt.toml" - - ".github/workflows/ci.yml" # This action - pull_request: - branches: ["main"] - paths: - - "**.rs" # Any rust file - - "**/Cargo.toml" # Any Cargo.toml - - ".rustfmt.toml" - - ".github/workflows/ci.yml" # This action - -env: - CARGO_TERM_COLOR: always - -jobs: - # First check that we can build EVERYTHING and that tests pass - build_first: - name: "Build, check, and test with all features" - runs-on: ubuntu-latest - outputs: - cli: ${{ steps.filter.outputs.cli }} - lib: ${{ steps.filter.outputs.lib }} - steps: - - uses: actions/checkout@v5 - - uses: Swatinem/rust-cache@v2 - with: - shared-key: "cargo-deps" - cache-targets: false - - name: Run Build - run: cargo check --verbose --workspace --bins --lib --examples --all-features - - name: Run Tests - run: cargo test --verbose --workspace --bins --lib --examples --tests --all-features - # Check what paths were modified so we only run the required tests - - uses: dorny/paths-filter@v3 - id: filter - with: - filters: | - cli: - - 'crates/cli/**' - lib: - - 'crates/lib/**' - - # If we were able to build then test different feature combinations compile with the library - build_lib: - runs-on: ubuntu-latest - needs: ["build_first"] - # Only run if library files were modified - if: ${{ needs.build_first.outputs.lib == 'true' }} - strategy: - fail-fast: false - matrix: - include: - - build_type: "" - build_name: "Default" - - build_type: "--no-default-features" - build_name: "No features" - - build_type: "--no-default-features --features games" - build_name: "Just games" - - build_type: "--no-default-features --features services" - build_name: "Just Services" - - build_type: "--no-default-features --features game_defs" - build_name: "Just Game definitions" - - build_type: "--no-default-features --features serde" - build_name: "Just serde" - name: "Build library ${{ matrix.build_name }}" - steps: - - uses: actions/checkout@v5 - - uses: Swatinem/rust-cache@v2 - with: - shared-key: "cargo-deps" - cache-targets: false - save-if: false - - name: Run Build - run: cargo check -p gamedig --verbose --lib --examples --tests ${{ matrix.build_type }} - - # If we were able to build then test different feature combinations compile with the CLI - build_cli: - runs-on: ubuntu-latest - needs: ["build_first"] - # Only run if CLI files were modified - if: ${{ needs.build_first.outputs.cli == 'true' }} - strategy: - fail-fast: false - matrix: - include: - - build_type: "" - build_name: "Default" - - build_type: "--no-default-features" - build_name: "No features" - name: "Build CLI ${{ matrix.build_name }}" - steps: - - uses: actions/checkout@v5 - - uses: Swatinem/rust-cache@v2 - with: - shared-key: "cargo-deps" - cache-targets: false - save-if: false - - name: Run Build - run: cargo check -p gamedig_cli --verbose --bins --examples --tests ${{ matrix.build_type }} - - # If we were able to build then test the MSRV compiles (for the libary as not enforced for CLI) - build_msrv: - name: "Build using MSRV (lib only)" - runs-on: ubuntu-latest - needs: ["build_first"] - # Only run if library files were modified - if: ${{ needs.build_first.outputs.lib == 'true' }} - # Unfortunate hard-coding of rustup directory so that rust-cache caches it - env: - RUSTUP_HOME: /home/runner/.rustup - steps: - # Act's rust runner has rustup in a different place - - if: ${{ env.ACT }} - run: mkdir -p /home/runner && ln -s /usr/share/rust/.rustup /home/runner/.rustup - - uses: actions/checkout@v5 - - uses: Swatinem/rust-cache@v2 - with: - cache-targets: false - cache-directories: ${{ env.RUSTUP_HOME }}/toolchains - - name: Install MSRV - uses: actions-rs/toolchain@v1 - with: - toolchain: 1.85.1 - override: true - - name: Run MSRV - run: cargo check -p gamedig - # Check the code is formatted properly - formatting: - name: "Check code formatting" - runs-on: ubuntu-latest - # Unfortunate hard-coding of rustup directory so that rust-cache caches it - env: - RUSTUP_HOME: /home/runner/.rustup - steps: - # Act's rust runner has rustup in a different place - - if: ${{ env.ACT }} - run: mkdir -p /home/runner && ln -s /usr/share/rust/.rustup /home/runner/.rustup - - uses: actions/checkout@v5 - - uses: Swatinem/rust-cache@v2 - with: - cache-targets: false - cache-directories: ${{ env.RUSTUP_HOME }}/toolchains - - name: Install Formatting nightly - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly-2025-04-19 - components: rustfmt - override: true - - name: Run Formatting check - run: cargo fmt --check --verbose - - # If we were able to build then lint the codebase with clippy - clippy: - name: "Run clippy tests" - runs-on: ubuntu-latest - needs: ["build_first"] - steps: - - uses: actions/checkout@v5 - - uses: Swatinem/rust-cache@v2 - with: - shared-key: "cargo-deps" - cache-targets: false - save-if: false - # Run github actions version of clippy that adds annotations - - name: Run Clippy - uses: actions-rs/clippy-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --workspace --bins --lib --examples --tests --all-features - if: ${{ !env.ACT }} # skip during local actions testing - # Run clippy binary - - name: Run clippy (local) - run: cargo clippy --verbose --workspace --bins --lib --examples --tests --all-features - if: ${{ env.ACT }} # only run during local actions testing - - # If we were able to build then test that rustdoc (and rustdoc examples) compile - doc: - name: "Check rustdoc" - runs-on: ubuntu-latest - needs: ["build_first"] - steps: - - uses: actions/checkout@v5 - - uses: Swatinem/rust-cache@v2 - with: - shared-key: "cargo-deps" - cache-targets: false - save-if: false - - name: "Run cargo doc" - run: cargo doc --workspace - env: - RUSTDOCFLAGS: "-D warnings" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index c39c0db..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - schedule: - - cron: '0 0 1 * *' # monthly on the 1st at 00:00 UTC - -jobs: - analyze: - name: Analyze (rust) - runs-on: ubuntu-latest - permissions: - security-events: write - packages: read - actions: read - contents: read - - strategy: - fail-fast: false - matrix: - include: - - language: rust - build-mode: none - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Build - run: cargo build --release - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml deleted file mode 100644 index 6f3f4c2..0000000 --- a/.github/workflows/labeler.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: "Pull Request Labeler" -on: - - pull_request_target - -jobs: - triage: - permissions: - contents: read - pull-requests: write - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: actions/labeler@v4 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" - dot: true diff --git a/.github/workflows/node-badge.yml b/.github/workflows/node-badge.yml deleted file mode 100644 index 0fccbcc..0000000 --- a/.github/workflows/node-badge.yml +++ /dev/null @@ -1,63 +0,0 @@ -# Based on: https://github.com/emibcn/badge-action/blob/master/.github/workflows/test.yml -name: "Generate node comparison badge" - -on: - push: - paths: - - "crates/lib/src/games/definitions.rs" - - ".github/workflows/node-badge.yml" - - ".github/workflows/scripts/node-badge.mjs" - branches: - - "main" # Limit badge commits to only happen on the main branch - schedule: # This runs on the default branch only, it could still trigger on PRs but only if they develop on default branch and enable actions. - - cron: "34 3 * * 2" # Update once a week in case node-gamedig has changed - workflow_dispatch: - -jobs: - badge: - runs-on: "ubuntu-latest" - name: Create node comparison badge - env: - BADGE_PATH: ".github/badges/node.svg" - steps: - - name: Extract branch name - shell: bash - run: echo "branch=${GITHUB_REF#refs/heads/}" >> "${GITHUB_OUTPUT}" - id: extract_branch - - - uses: actions/checkout@v5 - - uses: actions/checkout@v5 - with: - repository: "gamedig/node-gamedig" - path: "node-gamedig" - sparse-checkout: | - lib/games.js - package.json - - - name: Calculate comparison - id: comparison - run: node .github/workflows/scripts/node-badge.mjs - - - name: Generate the badge SVG image - uses: emibcn/badge-action@v2.0.3 - id: badge - with: - label: "Node game coverage" - status: "${{ steps.comparison.outputs.percent }}%" - color: "0f80c1" - path: ${{ env.BADGE_PATH }} - - - name: "Commit badge" - continue-on-error: true - run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - git add "${BADGE_PATH}" - git commit -m "Add/Update badge" - - - name: Push badge commit - uses: ad-m/github-push-action@master - if: ${{ success() }} - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - branch: ${{ steps.extract_branch.outputs.branch }} diff --git a/.github/workflows/scripts/node-badge.mjs b/.github/workflows/scripts/node-badge.mjs deleted file mode 100644 index 0e6aa46..0000000 --- a/.github/workflows/scripts/node-badge.mjs +++ /dev/null @@ -1,63 +0,0 @@ -"use strict"; - -//! Calculate the percentage of games from node that we support -// Expects node-gamedig checkout out in git root /node-gamedig -// Expects the generic example to output a list of game IDs when no arguments are provided - -import process from "node:process"; -import { closeSync, openSync, writeSync } from "node:fs"; -import { spawnSync } from "node:child_process"; - -const setOutput = (key, value) => { - const file = openSync(process.env.GITHUB_OUTPUT, "a"); - writeSync(file, `${key}=${value}\n`); - closeSync(file); -}; - -// Get node IDs -// NOTE: Here we directly import from games to avoid loading -// unecessary parts of the library that would require us -// to install dependencies. -import { games } from "../../../node-gamedig/lib/games.js"; - -const node_ids = new Set(Object.keys(games)); -const node_total = node_ids.size; - -// Get rust IDs - -const command = spawnSync("cargo", [ - "run", - "-p", - "gamedig", - "--example", - "generic", -]); - -if (command.status !== 0) { - console.error(command.stderr.toString("utf8")); - process.exit(1); -} - -const rust_ids_pretty = command.stdout.toString("utf8"); -const rust_ids = new Set( - rust_ids_pretty - .split("\n") - .map((line) => line.split("\t")[0]) - .filter((id) => id.length > 0) -); - -// Detect missing node IDs - -for (const id of rust_ids) { - if (node_ids.delete(id)) { - rust_ids.delete(id); - } -} - -console.log("Node remains", node_ids); -console.log("Rust remains", rust_ids); - -const percent = 1 - node_ids.size / node_total; - -// Output percent to 2 decimal places -setOutput("percent", Math.round(percent * 10000) / 100); diff --git a/.gitignore b/.gitignore index e285a7a..579ce9a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,3 @@ Cargo.lock # Others .idea/ -.venv/ -.vscode/ - -test_everything.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index ba5f8e4..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,60 +0,0 @@ -# See https://pre-commit.com for more information -# See https://pre-commit.com/hooks.html for more hooks -repos: - - repo: local - hooks: - - id: clippy - name: Check clippy - language: system - files: '([.]rs|Cargo\.toml)$' - pass_filenames: false - entry: rustup run --install nightly-2025-04-19 cargo-clippy -- --workspace --all-features -- -D warnings - - - id: build-no-features - name: Check crate build with no features - language: system - files: '([.]rs|Cargo\.toml)$' - pass_filenames: false - entry: cargo check --workspace --no-default-features - - - id: build-all-features - name: Check crate builds with all features - language: system - files: '([.]rs|Cargo\.toml)$' - pass_filenames: false - entry: cargo check --workspace --all-features --lib --bins --examples - - - id: test - name: Check tests pass - language: system - files: '([.]rs|Cargo\.toml)$' - pass_filenames: false - entry: cargo test --workspace --bins --lib --examples --tests --all-features - - - id: format - name: Check rustfmt - language: system - files: '([.]rs|Cargo\.toml)$' - pass_filenames: false - entry: rustup run --install nightly-2025-04-19 cargo-fmt --check - - - id: msrv - name: Check MSRV compiles (lib only) - language: system - files: '([.]rs|Cargo\.toml)$' - pass_filenames: false - entry: rustup run --install 1.85.1 cargo check -p gamedig - - - id: docs - name: Check rustdoc compiles - language: system - files: '([.]rs|Cargo\.toml)$' - pass_filenames: false - entry: env RUSTDOCFLAGS="-D warnings" cargo doc - - - id: actions - name: Check actions work - language: system - files: '^[.]github/workflows/' - pass_filenames: false - entry: act --rm diff --git a/.rustfmt.toml b/.rustfmt.toml deleted file mode 100644 index d410c70..0000000 --- a/.rustfmt.toml +++ /dev/null @@ -1,72 +0,0 @@ -attr_fn_like_width = 70 -array_width = 60 -binop_separator = "Front" -blank_lines_lower_bound = 0 -blank_lines_upper_bound = 1 -brace_style = "PreferSameLine" -chain_width = 60 -color = "Auto" -combine_control_expr = false -comment_width = 80 -condense_wildcard_suffixes = true -control_brace_style = "AlwaysSameLine" -disable_all_formatting = false -doc_comment_code_block_width = 100 -edition = "2021" -emit_mode = "Files" -empty_item_single_line = true -error_on_line_overflow = false -error_on_unformatted = false -fn_call_width = 60 -fn_params_layout = "Tall" -fn_single_line = true -force_explicit_abi = true -force_multiline_blocks = true -format_generated_files = true -format_macro_bodies = true -format_strings = true -group_imports = "Preserve" -hard_tabs = false -show_parse_errors = true -hex_literal_case = "Preserve" -ignore = [] -indent_style = "Block" -imports_granularity = "Preserve" -imports_indent = "Block" -imports_layout = "HorizontalVertical" -inline_attribute_width = 0 -make_backup = false -match_arm_blocks = true -match_arm_leading_pipes = "Never" -match_block_trailing_comma = false -max_width = 120 -merge_derives = true -newline_style = "Auto" -normalize_comments = true -normalize_doc_attributes = false -overflow_delimited_expr = false -reorder_impl_items = false -reorder_imports = true -reorder_modules = true -required_version = "1.8.0" -short_array_element_width_threshold = 10 -single_line_if_else_max_width = 50 -skip_children = false -space_after_colon = true -space_before_colon = false -spaces_around_ranges = true -struct_field_align_threshold = 0 -struct_lit_single_line = true -struct_lit_width = 18 -struct_variant_width = 35 -tab_spaces = 4 -trailing_comma = "Vertical" -trailing_semicolon = true -type_punctuation_density = "Wide" -unstable_features = false -use_field_init_shorthand = false -use_small_heuristics = "Default" -use_try_shorthand = true -style_edition = "2021" -where_single_line = true -wrap_comments = true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index ab34b87..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,119 +0,0 @@ -# Contributing to rust-GameDig - -This project is very open to new suggestions, additions and/or changes, these -can come in the form of *discussions* about the project's state, *proposing a -new feature*, *holding a few points on why we shall do X breaking change* or -*submitting a fix*. - -## Communications - -GitHub is the place we use to track bugs and discuss new features/changes, -although we have a [Discord](https://discord.gg/NVCMn3tnxH) server for the -community, all bugs, suggestions and changes will be reported on GitHub -alongside with their backing points to ensure the transparency of the project's -development. - -## Issues - -Before opening an issue, check if there is an existing relevant issue first, -someone might just have had your issue already, or you might find something -related that could be of help. - -When opening a new issue, make sure to fill the issue template. They are made -to make the subject to be as understandable as possible, not doing so may result -in your issue not being managed right away, if you don't understand something -(be it regarding your own problem/the issue template/the library), please state -so. - -## Development - -Note before contributing that everything done here is under the [MIT](https://opensource.org/license/mit/) license. - -### Naming - -Naming is an important matter, and it shouldn't be changed unless necessary. - -Game **names** should be added as they appear on steam (or other storefront -if not listed there) with the release year appended in brackets (except when the -release year is already part of the name). -If there is a mod that needs to be added (or it adds the support for server -queries for the game), its name should be composed of the game name, a separating -**bracket**, the mod name and the release year as specified previously -(e.g. `Grand Theft Auto V - FiveM (2013)`). - -A game's **identification** is a lowercase alphanumeric string will and be forged -following these rules: - -1. Names composed of a maximum of two words (unless #4 applies) will result in an - id where the words are concatenated (`Dead Cells` -> `deadcells`), acronyms in - the name count as a single word (`S.T.A.L.K.E.R.` -> `stalker`). -2. Names of more than two words shall be made into an acronym made of the - initial - letters (`The Binding of Isaac` -> `tboi`), [hypenation composed words](https://prowritingaid.com/hyphenated-words) - don't count as a single word, but of how many parts they are made of - (`Dino D-Day`, 3 words, so `ddd`). -3. If a game has the exact name as a previously existing id's game - (`Star Wars Battlefront 2`, the 2005 and 2017 one), append the release year to - the newer id (2005 would be `swb2` (suppose we already have this one supported) - and 2017 would be `swb22017`). -4. If a new id (`Day of Dragons` -> `dod`) results in an id that already exists - (`Day of Defeat` -> `dod`), then the new name should ignore rule #2 - (`Day of Dragons` -> `dayofdragons`). -5. Roman numbering will be converted to arabic numbering (`XIV` -> `14`). -6. Unless numbers (years included) are at the end of a name, they will be considered - words. If a number is not in the first position, its entire numeric digits will be - used instead of the acronym of that number's digits (`Left 4 Dead` -> `l4d`). If the - number is in the first position the longhand (words: 5 -> five) representation of the - number will be used to create an acronym (`7 Days to Die` -> `sdtd`). Other examples: - `Team Fortress 2` -> `teamfortress2`, `Unreal Tournament 2003` -> - `unrealtournament2003`. -7. If a game supports multiple protocols, multiple entries will be done for said game - where the edition/protocol name (first disposable in this order) will be appended to - the base game id's: `` (where the protocol id will follow all - rules except #2) (Minecraft is mainly divided by 2 editions, Java and Bedrock - which will be `minecraftjava` and `minecraftbedrock` respectively, but it also has - legacy versions, which use another protocol, an example would be the one for `1.6`, - so the name would be `Legacy 1.6` which its id will be `legacy16`, resulting in the - entry of `minecraftlegacy16`). One more entry can be added by the base name of the - game, which queries in a group said supported protocols to make generic queries - easier and disposable. -8. If its actually about a mod that adds the ability for queries to be performed, - process only the mod name. - -### Making commits - -Where possible please format commits as complete atomic changes that don't rely on -any future commits. Also make sure that the commit message is as descriptive as -possible. - -To avoid CI failing when you make a PR you can use our pre-commit hooks: tests that -run before you are able to make a commit (you can skip this at any time by adding -the `-n` flag to `git commit`). - -To set this up you need the following programs installed - -- [pre-commit](https://pre-commit.com/) -- [rustup](https://rustup.rs/) -- [act](https://github.com/nektos/act) (If you want to test changes to github actions workflows) - -Once these are installed you can enable the pre-commit hook by running the following in -the root directory of the repository. - -```shell -$ pre-commit install -``` - -### Priorities - -Game suggestions will be prioritized by maintainers based on whether the game -uses a protocol already implemented in the library (games that use already -implemented protocols will be added first), except in the case where a -contribution is made with the protocol needed to implement the game. - -The same goes for protocols, if 2 were to be requested, the one implemented in -the most games will be prioritized. - -### Releases - -Currently, there is no release schedule. -Releases are made when the team decides one will be fitting to be done. diff --git a/Cargo.toml b/Cargo.toml index 0fe5ede..4c4f940 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,22 +1,14 @@ -[workspace] -members = ["crates/cli", "crates/lib", "crates/id-tests"] +[package] +name = "gamedig" +version = "0.0.0" +edition = "2021" +authors = ["CosminPerRam [cosmin.p@live.com]", "mmorrisontx [https://github.com/mmorrisontx]"] +license-file = "LICENSE.md" +description = "Check out servers with this." +homepage = "https://github.com/CosminPerRam/rust-gamedig" +documentation = "https://github.com/CosminPerRam/rust-gamedig" +repository = "https://github.com/CosminPerRam/rust-gamedig" +readme = "README.md" +keywords = ["server", "valve", "games", "checker", "status"] -# Edition 2021, uses resolver = 2 -resolver = "2" - -[profile.release] -opt-level = 3 -debug = false -rpath = true -lto = 'fat' -codegen-units = 1 - -[profile.release.package."*"] -opt-level = 3 - -# When building locally, use the local version of the library -# Comment this out when you want to resolve the library from crates.io -# This is only for crates that use gamedig as a dependency -# https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html -[patch.crates-io] -gamedig = { path = "./crates/lib" } \ No newline at end of file +[dependencies] diff --git a/GAMES.md b/GAMES.md deleted file mode 100644 index de88490..0000000 --- a/GAMES.md +++ /dev/null @@ -1,104 +0,0 @@ -A supported game is defined as a game that has been successfully tested, other games that are not present here but use -one of the implemented protocols might work too, but that isn't guaranteed. -Beware of the `Notes` column, as it contains information about query port offsets or other query -requirements/information. - -# Supported games: - -| Game | Use name | Protocol | Notes | -|------------------------------------|---------------------|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Team Fortress 2 | TEAMFORTRESS2 | Valve | | -| The Ship | THESHIP | Valve (*Altered) | | -| Counter-Strike: Global Offensive | CSGO | Valve | The server must have the cvar `host_players_show` set to `2` to get the full player list. | -| Counter-Strike: Source | CSS | Valve | | -| Day of Defeat: Source | DODS | Valve | | -| Left 4 Dead | LEFT4DEAD | Valve | | -| Left 4 Dead 2 | LEFT4DEAD2 | Valve | | -| Half-Life 2 Deathmatch | HL2D | Valve | | -| Alien Swarm | ALIENSWARM | Valve | | -| Alien Swarm: Reactive Drop | ASRD | Valve | | -| Insurgency | INSURGENCY | Valve | | -| Insurgency: Sandstorm | INSURGENCYSANDSTORM | Valve | Query port offset: 1. | -| Insurgency: Modern Infantry Combat | IMIC | Valve | | -| Counter-Strike: Condition Zero | CSCZ | Valve GoldSrc | | -| Day of Defeat | DOD | Valve GoldSrc | | -| Minecraft | MINECRAFT | Proprietary | Bedrock edition provides a different response compared to the Java edition, query specifically for bedrock to get them, otherwise, only matching fields will be provided. | -| 7 Days To Die | SD2D | Valve | | -| ARK: Survival Evolved | ASE | Valve | | -| Unturned | UNTURNED | Valve | | -| The Forest | THEFOREST | Valve GoldSrc | Query port offset: 1. | -| Team Fortress Classic | TFC | Valve | | -| Sven Co-op | SCO | Valve GoldSrc | | -| Rust | RUST | Valve | | -| Counter-Strike | COUNTERSTRIKE | Valve GoldSrc | | -| Arma 2: Operation Arrowhead | A2OA | Valve | Query port offset: 1. | -| Arma 3 | ARMA3 | Valve | | -| Day of Infamy | DOI | Valve | | -| Half-Life Deathmatch: Source | HLDS | Valve | | -| Risk of Rain 2 | ROR2 | Valve | Query port offset: 1. | -| Battalion 1944 | BATTALION1944 | Valve | Query port offset: 3. It is strongly recommended to also query the rules, as it sends basic server info in them. | -| Black Mesa | BLACKMESA | Valve | | -| Project Zomboid | PROJECTZOMBOID | Valve | | -| Age of Chivalry | AOC | Valve | | -| Don't Starve Together | DST | Valve | Query port is 27016. | -| Colony Survival | COLONYSURVIVAL | Valve | | -| Onset | ONSET | Valve | Query port is 7776. | -| Codename CURE | CODENAMECURE | Valve | | -| Ballistic Overkill | BALLISTICOVERKILL | Valve | Query port is 27016. | -| BrainBread 2 | BRAINBREAD2 | Valve | | -| Avorion | AVORION | Valve | Query port is 27020. | -| Operation: Harsh Doorstop | OHD | Valve | Query port is 27005. | -| V Rising | VRISING | Valve | Query port is 27016. | -| Unreal Tournament | UNREALTOURNAMENT | GameSpy 1 | Query Port offset: 1. | -| Battlefield 1942 | B1942 | GameSpy 1 | Query port is 23000. | -| Serious Sam | SERIOUSSAM | GameSpy 1 | Query Port offset: 1. | -| Frontlines: Fuel of War | FFOW | Valve (*Altered) | Query Port offset: 2. | -| Crysis Wars | CRYSISWARS | GameSpy 3 | | -| Quake 2 | QUAKE2 | Quake 2 | | -| Quake 1 | QUAKE1 | Quake 1 | | -| Quake 3: Arena | QUAKE3 | Quake 3 | | -| Hell Let Loose | HLL | Valve Protocol | Query port is 26420. Note that on this port it might not send players data, as there might be another query port that does send players data. | -| Soldier of Fortune 2 | SOF2 | Quake 3 | | -| Halo: Combat Evolved | HCE | GameSpy 2 | | -| Just Cause 2: Multiplayer | JC2M | GameSpy 3 (*Altered) | | -| Warsow | WARSOW | Quake 3 | | -| Creativerse | CREATIVERSE | Valve | Query Port offset: 1. | -| Garry's Mod | GARRYSMOD | Valve | | -| Barotrauma | BAROTRAUMA | Valve | Query Port offset: 1. | -| Valheim | VALHEIM | Valve | Query Port offset: 1. Does not respond to the A2S rules. | -| The Front | THEFRONT | Valve | Responds with wrong values on `name` (gives out a SteamID instead of the server name) and `players_maximum` (always 200). | -| Conan Exiles | CONANEXILES | Valve | Does not respond to the players query. | -| Darkest Hour: Europe '44-'45 | DARKESTHOUR | Unreal2 | Query port offset: 1 | -| Devastation | DEVASTATION | Unreal2 | Query port offset: 1 | -| Killing Floor | KILLINGFLOOR | Unreal2 | Query port offset: 1 | -| Red Orchestra | REDORCHESTRA | Unreal2 | Query port offset: 1 | -| Unreal Tournament 2003 | UT2003 | Unreal2 | Query port offset: 1 | -| Unreal Tournament 2004 | UT2004 | Unreal2 | Query port offset: 1 | -| Post Scriptum | POSTSCRIPTUM | Valve | | -| Squad | SQUAD | Valve | | -| Savage 2 | SAVAGE2 | Proprietary | | -| Rising World | RISINGWORLD | Valve | Query port offset: -1 | -| ATLAS | ATLAS | Valve | Query port offset: 51800 | -| America's Army: Proving Grounds | AAPG | Valve | Query port: 27020. Does not respond to the rules query. | -| Base Defense | BASEDEFENSE | Valve | Query port: 27015. Does not respond to the rules query. | -| Zombie Panic: Source | ZPS | Valve | Query port: 27015. | -| Call Of Duty: Black Ops 3 | CODBO3 | Valve | Query port: 27017. | -| Counter-Strike 2 | COUNTERSTRIKE2 | Valve | | -| Double Action: Boogaloo | DAB | Valve | | -| Mordhau | MORDHAU | Valve | | -| Enshrouded | ENSHROUDED | Valve | | -| Myth of Empires | MOE | Valve | | -| Pirates, Vikings, and Knights II | PVAK2 | Valve | | -| PixARK | PIXARK | Valve | | -| Ark: Survival Ascended | ASA | Epic | Available on the 'tls' feature | -| Aliens vs. Predator 2010 | AVP | Valve | | -| Arma Reforger | ARMAREFORGER | Valve | | -| Nova-Life: Amboise | NLA | Valve | | -| Abiotic Factor | ABIOTICFACTOR | Valve | | -| Soulmask | SOULMASK | Valve | | -| Starbound | STARBOUND | Valve | | -| Minetest | MINETEST | Proprietary | Available on the 'tls', 'serde' and 'services' feature | - -## Planned to add support: - -_ diff --git a/LICENSE.md b/LICENSE.md index 27fe7df..7fa2763 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 - 2026 GameDig Organization & Contributors +Copyright (c) 2022 CosminPerRam Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/PROTOCOLS.md b/PROTOCOLS.md deleted file mode 100644 index abf10d1..0000000 --- a/PROTOCOLS.md +++ /dev/null @@ -1,18 +0,0 @@ -A protocol is defined as proprietary if it is being used only for a single scope (or series, like Minecraft). - -# Supported protocols: - -| Name | For | Proprietary? | Documentation reference | Notes | -|---------------------------|-------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Valve Protocol | Games | No | [Server Queries](https://developer.valvesoftware.com/wiki/Server_queries) | In some cases, the players details query might contain some 0-length named players. Multi-packet decompression not tested. | -| Minecraft | Games | Yes | Java: [List Server Protocol](https://wiki.vg/Server_List_Ping)
Bedrock: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/minecraftbedrock.js) | | -| GameSpy | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy1.js) Two: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy2.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy3.js) | These protocols are not really standardized, gamedig tries to get the most common fields amongst its supported games, if there are parsing problems, use the `query_vars` function. | -| Quake | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake1.js) Two: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake2.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake3.js) | | -| Just Cause 2: Multiplayer | Games | Yes | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/jc2mp.js) | -| Unreal 2 | Games | No | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/unreal2.js) | Sometimes servers send strings that node-gamedig would treat as latin1 that are UTF-8 encoded, when this happens the remove color code breaks because latin1 decodes the colour sequences differently. Some games provide additional info at the end of the server info packet, this is not currently handled (see the node implementation). Some games use a bot player to denote the team names, this is not currently handled. | -| Savage 2 | Games | Yes | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/savage2.js) | | -| Epic | Games | No | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/epic.js) | Available only on the 'tls' feature. | - -## Planned to add support: - -_ diff --git a/README.md b/README.md index 32f0d7c..c4c8038 100644 --- a/README.md +++ b/README.md @@ -1,118 +1 @@ -

rust-GameDig

- -
The fast library for querying game servers/services.
- -
- - CI - - - Latest Version - - - Crates.io - - - Node-GameDig Game Coverage - - - Rust-GameDig Dependencies - -
- -
- This library brings what - - node-GameDig - - does (and not only), to pure Rust! -
- -**Warning**: This project goes through frequent API breaking changes and hasn't been thoroughly tested. - -## Community - -Checkout the GameDig Community Discord Server [here](https://discord.gg/NVCMn3tnxH). -Note that it isn't be a replacement for GitHub issues, if you have found a problem -within the library or want to request a feature, it's better to do so here rather than -on Discord. - -## Usage - -Minimum Supported Rust Version is `1.85.1` and the code is cross-platform. - -Pick a game/service/protocol (check the [GAMES](GAMES.md), [SERVICES](SERVICES.md) and [PROTOCOLS](PROTOCOLS.md) files -to see the currently supported ones), provide the ip and the port (be aware that some game servers use a separate port -for the info queries, the port can also be optional if the server is running the default ports) then query on it. - -[Team Fortress 2](https://store.steampowered.com/app/440/Team_Fortress_2/) query example: - -```rust -use gamedig::games::teamfortress2; - -fn main() { - let response = teamfortress2::query(&"127.0.0.1".parse().unwrap(), None); - // None is the default port (which is 27015), could also be Some(27015) - - match response { // Result type, must check what it is... - Err(error) => println!("Couldn't query, error: {}", error), - Ok(r) => println!("{:#?}", r) - } -} -``` - -Response (note that some games have a different structure): - -```json5 -{ - protocol: 17, - name: "Team Fortress 2 Dedicated Server.", - map: "ctf_turbine", - game: "tf2", - appid: 440, - players_online: 0, - players_details: [], - players_maximum: 69, - players_bots: 0, - server_type: Dedicated, - has_password: false, - vac_secured: true, - version: "7638371", - port: Some(27015), - steam_id: Some(69753253289735296), - tv_port: None, - tv_name: None, - keywords: Some( - "alltalk,nocrits" - ), - rules: [ - "mp_autoteambalance" - : - "1", - "mp_maxrounds" - : - "5", - //.... - ] -} -``` - -Want to see more examples? Checkout the [examples](crates/lib/examples) folder. - -## Command Line Interface - -The library also has an [official CLI](https://crates.io/crates/gamedig_cli) that you can use, it has -MSRV of `1.85.1`. - -## Documentation - -The documentation is available at [docs.rs](https://docs.rs/gamedig/latest/gamedig/). -Curious about the history and what changed between versions? -Everything is in the changelogs file: [lib](crates/lib/CHANGELOG.md) and [cli](crates/lib/CHANGELOG.md). - -## Contributing - -If you want to see your favorite game/service being supported here, open an issue, and I'll prioritize it (or do a pull -request if you want to implement it yourself)! - -Before contributing please read [CONTRIBUTING](CONTRIBUTING.md). +# rust-gamedig \ No newline at end of file diff --git a/RESPONSES.md b/RESPONSES.md deleted file mode 100644 index 14a7948..0000000 --- a/RESPONSES.md +++ /dev/null @@ -1,78 +0,0 @@ -Every protocol has its own response type(s), below is a listing of the overlapping fields on these responses. - -If a cell is blank it doesn't exist, otherwise it contains the type of that data in the current column's response type. -In the case that a field that performs the same function exists in the current column's response type that name is -annotated in brackets. - -# Response table - -| Field | Generic | GameSpy(1) | GameSpy(2) | GameSpy(3) | Minecraft(Java) | Minecraft(Bedrock) | Valve | Quake | Unreal2 | Epic | Proprietary: FFOW | Proprietary: TheShip | Proprietary: JC2MP | Proprietary: Savage 2 | Proprietary: Minetest | -|----------------------|----------|------------|------------|------------|-----------------|--------------------|---------------|-----------|------------|----------|-------------------|----------------------|--------------------|-----------------------|-----------------------| -| name | `Option` | `String` | `String` | `String` | | `String` | `String` | `String` | `String` | `String` | `String` | `String` | `String` | `String` | `String` | -| description | `Option` | | | | `String` | | | | | | `String` | | `String` | | `String` | -| game_mode | `Option` | `String` | | `String` | | `Option` | `String` | | `String` | | `String` | `String` | | `String` | | -| game_version | `Option` | `String` | | `String` | `String` | | `String` | `String` | | `String` | `String` | `String` | `String` | | `String` | -| map | `Option` | `String` | `String` | `String` | | `Option` | `String` | `String` | `String` | `String` | `String` | `String` | | `String` | | -| players_maxmimum | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u32` | `u32` | `u8` | `u8` | `u32` | `u8` | `u32` | -| players_online | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u32` | `u32` | `u8` | `u8` | `u32` | `u8` | `u32` | -| players_bots | `Option` | | | | | | `u8` | | | | | `u8` | | | | -| has_password | `Option` | `bool` | `bool` | `bool` | | | `bool` | | | `bool` | `bool` | `bool` | `bool` | | `Option` | -| players_minimum | | `Option` | `Option` | `Option` | | | | | | | | | | `u8` | | -| players | | `Vec` | `Vec` | `Vec` | `Option>` | | `Option>` | `Vec ` | `Vec` | `Vec` | | `Vec` | `Vec` | | `Vec` | -| tournament | | `bool` | | `bool` | | | | | | | | | | | | -| unused_entries | | `Hashmap` | | `HashMap` | | | | `HashMap` | | | | | | | | -| teams | | | `Vec` | `Vec` | | | | | | | | | | | | -| protocol_version | | | | | `i32` | `String` | `u8` | | | | `u8` | `u8` | | `String` | | -| server_type | | | | | `Server` | `Server` | `Server` | | | | | `Server` | | | | -| rules | | | | | | | `Option>` | | `HashMap>` | | | `HashMap` | | | | -| environment_type | | | | | | | `Environment` | | | | `Environment` | | | | | -| vac_secured | | | | | | | `bool` | | | | `bool` | `bool` | | | | -| map_title | | `Option` | | | | | | | | | | | | | | -| admin_contact | | `Option` | | | | | | | | | | | | | | -| admin_name | | `Option` | | | | | | | | | | | | | | -| favicon | | | | | `Option` | | | | | | | | | | | -| previews_chat | | | | | `Option` | | | | | | | | | | | -| enforces_secure_chat | | | | | `Option` | | | | | | | | | | | -| edition | | | | | | `String` | | | | | | | | | | -| id | | | | | | `String` | | | `String` | | | | | | `String` | -| the_ship | | | | | | | `Option` | | | | | | | | | -| is_mod | | | | | | | `bool` | | | | | | | | | -| extra_data | | | | | | | `Option` | | | | | | | | | -| mod_data | | | | | | | `Option` | | | | | | | | | -| folder | | | | | | | `String` | | | | | | | | | -| appid | | | | | | | `u32` | | | | | | | | | -| active_mod | | | | | | | | | | | `String` | | | | | -| round | | | | | | | | | | | `u8` | | | | | -| rounds_maximum | | | | | | | | | | | `u8` | | | | | -| time_left | | | | | | | | | | | `u16` | | | | | -| port | | | | | | | | | `u32` | | | `Option` | | | `u32` | -| steam_id | | | | | | | | | | | | `Option` | | | | -| tv_port | | | | | | | | | | | | `Option` | | | | -| tv_name | | | | | | | | | | | | `Option` | | | | -| keywords | | | | | | | | | | | | `Option` | | | | -| mode | | | | | | | | | | | | `u8` | | | | -| witnesses | | | | | | | | | | | | `u8` | | | | -| duration | | | | | | | | | | | | `u8` | | | | -| query_port | | | | | | | | | `u32` | | | | | | | -| ip | | | | | | | | | `String` | | | | | | `String` | -| mutators | | | | | | | | | `HashSet` | | | | | | | -| next_map | | | | | | | | | | | | | | `String` | | -| location | | | | | | | | | | | | | | `String` | | -| level_minimum | | | | | | | | | | | | | | `String` | | -| time | | | | | | | | | | | | | | `String` | | -| creative | | | | | | | | | | | | | | | `Option` | -| damage | | | | | | | | | | | | | | | `bool` | -| game_time | | | | | | | | | | | | | | | `u32` | -| lag | | | | | | | | | | | | | | | `Option` | -| proto_max | | | | | | | | | | | | | | | `u16` | -| proto_min | | | | | | | | | | | | | | | `u16` | -| pvp | | | | | | | | | | | | | | | `bool` | -| uptime | | | | | | | | | | | | | | | `u32` | -| url | | | | | | | | | | | | | | | `Option` | -| update_time | | | | | | | | | | | | | | | `u32` | -| start | | | | | | | | | | | | | | | `u32` | -| clients_top | | | | | | | | | | | | | | | `u32` | -| updates | | | | | | | | | | | | | | | `u32` | -| pop_v | | | | | | | | | | | | | | | `f32` | -| geo_continent | | | | | | | | | | | | | | | `Option` | -| ping | | | | | | | | | | | | | | | `f32` | diff --git a/SERVICES.md b/SERVICES.md deleted file mode 100644 index 5c00adb..0000000 --- a/SERVICES.md +++ /dev/null @@ -1,10 +0,0 @@ -# Supported services: - -| Name | Documentation reference | -|------------------------|-------------------------------------------------------------------------------------------------------| -| Valve Master Server | [Master Server Query Protocol](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol) | -| MineTest Master Server | [Node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/minetest.js) | - -## Planned to add support: - -TeamSpeak diff --git a/VERSIONS.md b/VERSIONS.md deleted file mode 100644 index 7db63b2..0000000 --- a/VERSIONS.md +++ /dev/null @@ -1,31 +0,0 @@ -# MSRV (Minimum Supported Rust Version) - -Current: `1.85.1` - -Places to update: - -- `Cargo.toml` -- `README.md` -- `crates/lib/README.md` -- `.github/workflows/ci.yml` -- `.pre-commit-config.yaml` - -# rustfmt version - -Current: `1.8.0` - -Places to update: - -- `.rustfmt.toml` -- The nightly rust version - -# The nightly rust version - -The toolchain version used to run rustfmt in CI - -Current: `nightly-2025-04-19` - -Places to update: - -- `.github/workflows/ci.yml` -- `.pre-commit-config.yaml` diff --git a/crates/cli/.cargo/config.toml b/crates/cli/.cargo/config.toml deleted file mode 100644 index b53c97e..0000000 --- a/crates/cli/.cargo/config.toml +++ /dev/null @@ -1,10 +0,0 @@ -[profile.release] -opt-level = 'z' -debug = false -rpath = true -lto = 'fat' -codegen-units = 1 -strip = 'debuginfo' - -[profile.release.package."*"] -opt-level = 'z' diff --git a/crates/cli/CHANGELOG.md b/crates/cli/CHANGELOG.md deleted file mode 100644 index 2598475..0000000 --- a/crates/cli/CHANGELOG.md +++ /dev/null @@ -1,76 +0,0 @@ -Who knows what the future holds... - -# X.Y.Z - DD/MM/YYYY - -Nothing... yet. - -# 0.5.0 - 22/02/2026 - -### Breaking Changes: - -- MSRV has been updated to `1.85.1` to match the latest `gamedig` version. - -### Changes: - -- Updated dependencies - -# 0.4.0 - 24/08/2025 - -### Breaking Changes: - -- MSRV has been updated to `1.82.0` to match the latest `gamedig` version. - -### Changes: - -- Some minor clippy fixes -- Updated dependencies - -# 0.3.0 - 23/04/2025 - -### Changes: - -- CLI now uses `gamedig` v0.7.0 (To update, run `cargo install gamedig_cli`). - -### Breaking Changes: - -- MSRV has been updated to `1.81.0` to match the latest `gamedig` version. - -# 0.2.1 - 05/12/2024 - -Dependencies: -- `gamedig`: `v0.6.0 -> v0.6.1` - -# 0.2.0 - 26/11/2024 - -### Breaking Changes: - -- Restructured the release flow to be more consistent (GitHub releases will no longer be available, use cargo instead). -- Changed crate name from `gamedig-cli` to `gamedig_cli` to align with recommended naming conventions. -- The CLI now requires a minimum Rust version of `1.74.1`. - -# 0.1.1 - 15/07/2024 - -### Changes: - -- Dependency updates (by @cainthebest) - - `gamedig`: `v0.5.0 -> v0.5.1` - - `clap`: `v4.1.11 -> v4.5.4` - - `quick-xml`: `v0.31.0 -> v0.36.0` - - `webbrowser`: `v0.8.12 -> v1.0.0` - -# 0.1.0 - 15/03/2024 - -### Changes: - -- Added the CLI (by @cainthebest). -- Added DNS lookup support (by @Douile). -- Added JSON output option (by @Douile). -- Added BSON output in hex or base64 (by @cainthebest). -- Added XML output option (by @cainthebest). -- Added ExtraRequestSettings as CLI arguments (by @Douile). -- Added TimeoutSettings as CLI argument (by @Douile). -- Added Comprehensive end-user documentation for the CLI interface (by @Douile & @cainthebest). -- Tweaked compile-time flags to allow for a more preformant binary (by @cainthebest). -- Added client for socket capture, dev tools are not included by default (by @Douile). -- Added license information to the CLI (by @cainthebest). -- Added source code information to the CLI (by @cainthebest). diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml deleted file mode 100644 index 474450a..0000000 --- a/crates/cli/Cargo.toml +++ /dev/null @@ -1,52 +0,0 @@ -[package] -name = "gamedig_cli" -authors = ["rust-GameDig contributors [https://github.com/gamedig/rust-gamedig/contributors]"] -description = "A command line interface for gamedig" -license = "MIT" -version = "0.5.0" -edition = "2021" -default-run = "gamedig_cli" -homepage = "https://gamedig.github.io/" -repository = "https://github.com/gamedig/rust-gamedig" -readme = "README.md" -keywords = ["server", "query", "game", "check", "status"] -rust-version = "1.85.1" -categories = ["command-line-interface"] - -[features] -default = ["json", "bson", "xml", "browser"] - -# Tools -packet_capture = ["gamedig/packet_capture"] - -# Output formats -bson = ["dep:serde", "dep:bson", "dep:hex", "dep:base64", "gamedig/serde"] -json = ["dep:serde", "dep:serde_json", "gamedig/serde"] -xml = ["dep:serde", "dep:serde_json", "dep:quick-xml", "gamedig/serde"] - -# Misc -browser = ["dep:webbrowser"] - -[dependencies] -# Core Dependencies -thiserror = "2.0.18" -clap = { version = "4.5.60", default-features = false, features = ["derive"] } -gamedig = { version = "0.9.0", default-features = false, features = ["clap", "games", "game_defs"] } - -# Feature Dependencies -# Serialization / Deserialization -serde = { version = "1", optional = true, default-features = false } - -# BSON -bson = { version = "2.15", optional = true, default-features = false } -base64 = { version = "0.22", optional = true, default-features = false, features = ["std"] } -hex = { version = "0.4.3", optional = true, default-features = false } - -# JSON -serde_json = { version = "1", optional = true, default-features = false } - -# XML -quick-xml = { version = "0.39.2", optional = true, default-features = false } - -# Browser -webbrowser = { version = "1.1.0", optional = true, default-features = false } diff --git a/crates/cli/LICENSE.md b/crates/cli/LICENSE.md deleted file mode 100644 index 27fe7df..0000000 --- a/crates/cli/LICENSE.md +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 - 2026 GameDig Organization & Contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/crates/cli/README.md b/crates/cli/README.md deleted file mode 100644 index 8c87577..0000000 --- a/crates/cli/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Rust GameDig CLI - -The official [rust-GameDig](https://crates.io/crates/gamedig) Command Line Interface. - -[![CI](https://github.com/gamedig/rust-gamedig/actions/workflows/ci.yml/badge.svg)](https://github.com/gamedig/rust-gamedig/actions) [![License:MIT](https://img.shields.io/github/license/gamedig/rust-gamedig?color=blue)](https://github.com/gamedig/rust-gamedig/blob/main/LICENSE.md) [![node coverage](https://raw.githubusercontent.com/gamedig/rust-gamedig/main/.github/badges/node.svg)](https://github.com/gamedig/node-gamedig) - -## Installation - -You can install the CLI via `cargo`: - -```sh -cargo install gamedig_cli -``` - -or - -```sh -cargo install gamedig_cli --git https://github.com/gamedig/rust-gamedig.git -``` - -## Usage - -Running `gamedig_cli` without any arguments will display the usage information. You can also use the `--help` (or `-h`) flag to see detailed usage instructions. - -Here's also a quick rundown of a simple query with the `json-pretty` format: - -Pick a game/service/protocol (check -the [GAMES](https://github.com/gamedig/rust-gamedig/blob/main/GAMES.md), [SERVICES](https://github.com/gamedig/rust-gamedig/blob/main/SERVICES.md) -and [PROTOCOLS](https://github.com/gamedig/rust-gamedig/blob/main/PROTOCOLS.md) files to see the currently supported -ones), provide the ip and the port (be aware that some game servers use a separate port for the info queries, the port -can also be optional if the server is running the default ports) then query on it. - -[Team Fortress 2](https://store.steampowered.com/app/440/Team_Fortress_2/) query example: - -```sh -gamedig_cli query -g teamfortress2 -i 127.0.0.1 -f json-pretty -``` - -What we are doing here: - -- `-g` (or `--game`) specifies the game. -- `-i` (or `--ip`) target ip. -- `-f` (or `--format`) our preferred format. - -Note: We haven't specified a port (via `-p` or `--port`), so the default one for the game will be used (`27015` in this -case). - -Response (note that some games have a different structure): - -```json -{ - "name": "A cool server.", - "description": null, - "game_mode": "Team Fortress", - "game_version": "8690085", - "map": "cp_foundry", - "players_maximum": 24, - "players_online": 0, - "players_bots": 0, - "has_password": false, - "players": [] -} -``` - -## Contributing - -Please read [CONTRIBUTING](https://github.com/gamedig/rust-gamedig/blob/main/CONTRIBUTING.md). diff --git a/crates/cli/src/error.rs b/crates/cli/src/error.rs deleted file mode 100644 index 1700827..0000000 --- a/crates/cli/src/error.rs +++ /dev/null @@ -1,31 +0,0 @@ -pub type Result = std::result::Result; - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("IO Error: {0}")] - Io(#[from] std::io::Error), - - #[error("Clap Error: {0}")] - Clap(#[from] clap::Error), - - #[error("Gamedig Error: {0}")] - Gamedig(#[from] gamedig::errors::GDError), - - #[cfg(any(feature = "json", feature = "xml"))] - #[error("Serde Error: {0}")] - Serde(#[from] serde_json::Error), - - #[cfg(feature = "bson")] - #[error("Bson Error: {0}")] - Bson(#[from] bson::ser::Error), - - #[cfg(feature = "xml")] - #[error("Xml Error: {0}")] - Xml(#[from] quick_xml::Error), - - #[error("Unknown Game: {0}")] - UnknownGame(String), - - #[error("Invalid hostname: {0}")] - InvalidHostname(String), -} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs deleted file mode 100644 index 50bd5a8..0000000 --- a/crates/cli/src/main.rs +++ /dev/null @@ -1,487 +0,0 @@ -use std::net::{IpAddr, ToSocketAddrs}; - -use clap::{Parser, Subcommand, ValueEnum}; -use gamedig::{ - games::*, - protocols::types::{CommonResponse, ExtraRequestSettings, TimeoutSettings}, -}; - -mod error; - -use self::error::{Error, Result}; - -const GAMEDIG_HEADER: &str = r" - - _____ _____ _ _____ _ _____ - / ____| | __ \(_) / ____| | |_ _| -| | __ __ _ _ __ ___ ___| | | |_ __ _ | | | | | | -| | |_ |/ _` | '_ ` _ \ / _ \ | | | |/ _` | | | | | | | -| |__| | (_| | | | | | | __/ |__| | | (_| | | |____| |____ _| |_ - \_____|\__,_|_| |_| |_|\___|_____/|_|\__, | \_____|______|_____| - __/ | - |___/ - - A command line interface for querying game servers. - Copyright (C) 2022 - 2024 GameDig Organization & Contributors - Licensed under the MIT license -"; - -// NOTE: For some reason without setting long_about here the doc comment for -// ExtraRequestSettings gets set as the about for the CLI. -#[derive(Debug, Parser)] -#[command(author, version, about = GAMEDIG_HEADER, long_about = None)] -struct Cli { - #[command(subcommand)] - action: Action, -} - -#[derive(Subcommand, Debug)] -enum Action { - /// Query game server information - Query { - /// Unique identifier of the game for which server information is being - /// queried. - #[arg(short, long)] - game: String, - - /// Hostname or IP address of the server. - #[arg(short, long)] - ip: String, - - /// Optional query port number for the server. If not provided the - /// default port for the game is used. - #[arg(short, long)] - port: Option, - - /// Specifies the output format - #[arg(short, long, default_value = "debug", value_enum)] - format: OutputFormat, - - /// Which response variant to use when outputting - #[arg(short, long, default_value = "generic")] - output_mode: OutputMode, - - /// Optional file path for packet capture file writer - /// - /// When set a PCAP file will be written to the location. This file can - /// be read with a tool like wireshark. The PCAP contains a log of the - /// TCP and UDP data sent/recieved by the gamedig library, it does not - /// contain an accurate representation of the real packets sent on the - /// wire as some information has to be hallucinated in order for it to - /// display nicely. - #[cfg(feature = "packet_capture")] - #[arg(short, long)] - capture: Option, - - /// Optional timeout settings for the server query - #[command(flatten, next_help_heading = "Timeouts")] - timeout_settings: Option, - - /// Optional extra settings for the server query - #[command(flatten, next_help_heading = "Query options")] - extra_options: Option, - }, - - /// Check out the source code - Source, - /// Display the MIT License information - License, -} - -#[derive(Clone, Debug, PartialEq, Eq, ValueEnum)] -enum OutputMode { - /// A generalised response that maps common fields from all game types to - /// the same name. - Generic, - /// The raw result returned from the protocol query, formatted similarly to - /// how the server returned it. - ProtocolSpecific, -} - -#[derive(Clone, Debug, PartialEq, Eq, ValueEnum)] -enum OutputFormat { - /// Human readable structured output - Debug, - /// RFC 8259 - #[cfg(feature = "json")] - JsonPretty, - /// RFC 8259 - #[cfg(feature = "json")] - Json, - /// Parser tries to be mostly XML 1.1 (RFC 7303) compliant - #[cfg(feature = "xml")] - Xml, - /// RFC 4648 section 8 - #[cfg(feature = "bson")] - BsonHex, - /// RFC 4648 section 4 - #[cfg(feature = "bson")] - BsonBase64, -} - -/// Attempt to find a game from the [library game definitions](GAMES) based on -/// its unique identifier. -/// -/// # Arguments -/// * `game_id` - A string slice containing the unique game identifier. -/// -/// # Returns -/// * Result<&'static [Game]> - On sucess returns a reference to the game -/// definition; on failure returns a [Error::UnknownGame] error. -fn find_game(game_id: &str) -> Result<&'static Game> { - // Attempt to retrieve the game from the predefined game list - GAMES - .get(game_id) - .ok_or_else(|| Error::UnknownGame(game_id.to_string())) -} - -/// Resolve an IP address by either parsing an IP address or doing a DNS lookup. -/// In the case of DNS lookup update extra request options with the hostname. -/// -/// # Arguments -/// * `host` - A string slice containing the IP address or hostname of a server -/// to resolve. -/// * `extra_options` - Mutable reference to extra options for the game query. -/// -/// # Returns -/// * `Result` - On sucess returns a resolved IP address; on failure -/// returns an [Error::InvalidHostname] error. -fn resolve_ip_or_domain>(host: T, extra_options: &mut Option) -> Result { - let host_str = host.as_ref(); - if let Ok(parsed_ip) = host_str.parse() { - Ok(parsed_ip) - } else { - set_hostname_if_missing(host_str, extra_options); - resolve_domain(host_str) - } -} - -/// Resolve a domain name to one of its IP addresses (the first one returned). -/// -/// # Arguments -/// * `domain` - A string slice containing the domain name to lookup. -/// -/// # Returns -/// * `Result` - On success, returns one of the resolved IP addresses; -/// on failure returns an [Error::InvalidHostname] error. -fn resolve_domain(domain: &str) -> Result { - // Append a dummy port to perform socket address resolution and then extract the - // IP - Ok(format!("{domain}:0") - .to_socket_addrs() - .map_err(|_| Error::InvalidHostname(domain.to_string()))? - .next() - .ok_or_else(|| Error::InvalidHostname(domain.to_string()))? - .ip()) -} - -/// Sets the hostname on extra request settings if it is not already set. -/// -/// # Arguments -/// * `host` - A string slice containing the hostname. -/// * `extra_options` - A mutable reference to optional [ExtraRequestSettings]. -fn set_hostname_if_missing(host: &str, extra_options: &mut Option) { - if let Some(extra_options) = extra_options { - if extra_options.hostname.is_none() { - // If extra_options exists but hostname is None overwrite hostname in place - extra_options.hostname = Some(host.to_string()) - } - } else { - // If extra_options is None create default settings with hostname - *extra_options = Some(ExtraRequestSettings::default().set_hostname(host.to_string())); - } -} - -/// Output the result of a query to stdout. -/// -/// # Arguments -/// * `args` - A reference to the command line options. -/// * `result` - A reference to the result of the query. -fn output_result(output_mode: OutputMode, format: OutputFormat, result: &T) { - match format { - OutputFormat::Debug => { - match output_mode { - OutputMode::Generic => output_result_debug(result.as_json()), - OutputMode::ProtocolSpecific => output_result_debug(result.as_original()), - }; - } - #[cfg(feature = "json")] - OutputFormat::JsonPretty => { - let _ = match output_mode { - OutputMode::Generic => output_result_json_pretty(result.as_json()), - OutputMode::ProtocolSpecific => output_result_json_pretty(result.as_original()), - }; - } - #[cfg(feature = "json")] - OutputFormat::Json => { - let _ = match output_mode { - OutputMode::Generic => output_result_json(result.as_json()), - OutputMode::ProtocolSpecific => output_result_json(result.as_original()), - }; - } - #[cfg(feature = "xml")] - OutputFormat::Xml => { - let _ = match output_mode { - OutputMode::Generic => output_result_xml(result.as_json()), - OutputMode::ProtocolSpecific => output_result_xml(result.as_original()), - }; - } - #[cfg(feature = "bson")] - OutputFormat::BsonHex => { - let _ = match output_mode { - OutputMode::Generic => output_result_bson_hex(result.as_json()), - OutputMode::ProtocolSpecific => output_result_bson_hex(result.as_original()), - }; - } - #[cfg(feature = "bson")] - OutputFormat::BsonBase64 => { - let _ = match output_mode { - OutputMode::Generic => output_result_bson_base64(result.as_json()), - OutputMode::ProtocolSpecific => output_result_bson_base64(result.as_original()), - }; - } - } -} - -/// Output the result using debug formatting. -/// -/// # Arguments -/// * `result` - A result that can be output using the debug formatter. -fn output_result_debug(result: R) { - println!("{result:#?}"); -} - -/// Output the result as a JSON object. -/// -/// # Arguments -/// * `result` - A serde serializable result. -#[cfg(feature = "json")] -fn output_result_json(result: T) -> Result<()> { - println!("{}", serde_json::to_string(&result)?); - - Ok(()) -} - -/// Output the result as a pretty printed JSON object. -/// -/// # Arguments -/// * `result` - A serde serializable result. -#[cfg(feature = "json")] -fn output_result_json_pretty(result: T) -> Result<()> { - println!("{}", serde_json::to_string_pretty(&result)?); - - Ok(()) -} - -/// Output the result as an XML object. -/// # Arguments -/// * `result` - A serde serializable result. -#[cfg(feature = "xml")] -fn output_result_xml(result: T) -> Result<()> { - use quick_xml::{ - events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event}, - Writer, - }; - use serde_json::Value; - - // Serialize the input `result` of generic type `T` into a JSON value. - // This step converts the Rust data structure into a JSON format, - // which will then be used to generate the corresponding XML. - let json = serde_json::to_value(result)?; - - // Initialize the XML writer with a new, empty vector to store the XML data. - let mut writer = Writer::new(Vec::new()); - - // Write the XML 1.1 declaration - writer.write_event(Event::Decl(BytesDecl::new("1.1", Some("utf-8"), None)))?; - - // Define a recursive function `json_to_xml` to convert the JSON value into XML - // format. The function takes a mutable reference to the XML writer, an - // optional key as a string slice, and a reference to the JSON value to be - // converted. - fn json_to_xml(writer: &mut Writer, key: Option<&str>, value: &Value) -> Result<()> { - match value { - // If the JSON value is an object, iterate through its properties, - // creating XML elements with corresponding keys and values. - Value::Object(obj) => { - if let Some(key) = key { - // Start an XML element for the object. - writer.write_event(Event::Start(BytesStart::new(key)))?; - } - - for (k, v) in obj { - // Recursively process each property of the object. - json_to_xml(writer, Some(k), v)?; - } - - if let Some(key) = key { - // Close the XML element for the object. - writer.write_event(Event::End(BytesEnd::new(key)))?; - } - } - - // If the JSON value is an array, iterate through its elements, - // creating XML elements for each item. - Value::Array(arr) => { - for v in arr { - // Use "item" as the default key for array elements without keys. - json_to_xml(writer, key.or(Some("item")), v)?; - } - } - - // If the JSON value is null, create an empty XML element. - Value::Null => { - if let Some(key) = key { - writer.write_event(Event::Empty(BytesStart::new(key)))?; - } - } - - // For all other JSON value types (String, Number, Bool), - // convert the value to a string and create an XML element with the text content. - // Note: We handle null strings here as well, as they are treated as a string type. - _ => { - if let Some(key) = key { - // Start the XML element with the given key. - writer.write_event(Event::Start(BytesStart::new(key)))?; - } - - // Convert the JSON value to a string, trimming quotes for non-string values. - let text_string = match value { - Value::String(s) => s.to_string(), - _ => value.to_string().trim_matches('"').to_string(), - }; - - // Create a text node with the converted string value. - writer.write_event(Event::Text(BytesText::new(&text_string)))?; - - if let Some(key) = key { - // Close the XML element. - writer.write_event(Event::End(BytesEnd::new(key)))?; - } - } - } - Ok(()) - } - - // Start the root XML element named "data". - writer.write_event(Event::Start(BytesStart::new("data")))?; - // Convert the top-level JSON value to XML. - json_to_xml(&mut writer, None, &json)?; - // Close the root XML element. - writer.write_event(Event::End(BytesEnd::new("data")))?; - - // Convert the XML data stored in the writer to a UTF-8 string. - let xml_bytes = writer.into_inner(); - let xml_string = String::from_utf8(xml_bytes).expect("Failed to convert XML bytes to UTF-8 string"); - - println!("{xml_string}"); - - Ok(()) -} - -/// Output the result as a BSON object encoded as a hex string. -/// -/// # Arguments -/// * `result` - A serde serializable result. -#[cfg(feature = "bson")] -fn output_result_bson_hex(result: T) -> Result<()> { - let bson = bson::to_bson(&result)?; - - if let bson::Bson::Document(document) = bson { - let bytes = bson::to_vec(&document)?; - - println!("{}", hex::encode(bytes)); - - Ok(()) - } else { - panic!("Failed to convert result to BSON Hex (BSON_DOCUMENT_UNAVAILABLE)"); - } -} - -/// Output the result as a BSON object encoded as a base64 string. -/// -/// # Arguments -/// * `result` - A serde serializable result. -#[cfg(feature = "bson")] -fn output_result_bson_base64(result: T) -> Result<()> { - use base64::Engine; - - let bson = bson::to_bson(&result)?; - - if let bson::Bson::Document(document) = bson { - let bytes = bson::to_vec(&document)?; - - println!("{}", base64::prelude::BASE64_STANDARD.encode(bytes)); - - Ok(()) - } else { - panic!("Failed to convert result to BSON Base64 (BSON_DOCUMENT_UNAVAILABLE)"); - } -} - -fn main() -> Result<()> { - let args = Cli::parse(); - - match args.action { - Action::Query { - game, - ip, - port, - format, - output_mode, - #[cfg(feature = "packet_capture")] - capture, - timeout_settings, - extra_options, - } => { - // Process the query command - let game = find_game(&game)?; - let mut extra_options = extra_options; - let ip = resolve_ip_or_domain(&ip, &mut extra_options)?; - - #[cfg(feature = "packet_capture")] - gamedig::capture::setup_capture(capture); - - let result = query_with_timeout_and_extra_settings(game, &ip, port, timeout_settings, extra_options)?; - output_result(output_mode, format, result.as_ref()); - } - Action::Source => { - println!("{GAMEDIG_HEADER}"); - - #[cfg(feature = "browser")] - { - // Directly offering to open the URL - println!("\nWould you like to open the GitHub repository in your default browser? [Y/n]"); - - let mut choice = String::new(); - std::io::stdin().read_line(&mut choice).unwrap(); - if choice.trim().eq_ignore_ascii_case("Y") { - if webbrowser::open("https://github.com/gamedig/rust-gamedig").is_ok() { - println!("Opening GitHub repository in default browser..."); - } else { - println!("Failed to open GitHub repository in default browser."); - println!("Please use the following URL: https://github.com/gamedig/rust-gamedig"); - } - } else { - println!("Not to worry, you can always open the repository manually"); - println!("by visiting the following URL: https://github.com/gamedig/rust-gamedig"); - } - } - - #[cfg(not(feature = "browser"))] - { - println!("\nYou can find the source code for this project at the following URL:"); - println!("https://github.com/gamedig/rust-gamedig"); - } - - println!("\nBe sure to leave a star if you like the project :)"); - } - Action::License => { - // Bake the license into the binary - // so we don't have to ship it separately - println!("{}", include_str!("../LICENSE.md")); - } - } - - Ok(()) -} diff --git a/crates/id-tests/Cargo.toml b/crates/id-tests/Cargo.toml deleted file mode 100644 index 22b9a90..0000000 --- a/crates/id-tests/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "gamedig-id-tests" -version = "0.0.1" -edition = "2021" -authors = [ - "rust-GameDig contributors [https://github.com/gamedig/rust-gamedig/contributors]", - "node-GameDig contributors [https://github.com/gamedig/node-gamedig/contributors]", -] -license = "MIT" -description = "Test if IDs match the gamedig rules" -homepage = "https://github.com/gamedig/rust-gamedig/CONTRIBUTING.md#naming" -repository = "https://github.com/gamedig/rust-gamedig" -readme = "README.md" -rust-version = "1.65.0" - -[features] -cli = ["dep:serde_json", "dep:serde"] -default = ["cli"] - -[[bin]] -name = "gamedig-id-tests" -required-features = ["cli"] - -[dependencies] -number_to_words = "0.1" -roman_numeral = "0.1" - -serde_json = { version = "1", optional = true } -serde = { version = "1", optional = true, features = ["derive"] } \ No newline at end of file diff --git a/crates/id-tests/src/lib.rs b/crates/id-tests/src/lib.rs deleted file mode 100644 index f0fe155..0000000 --- a/crates/id-tests/src/lib.rs +++ /dev/null @@ -1,451 +0,0 @@ -use std::collections::HashMap; - -mod utils; -use utils::{extract_bracketed_suffix, split_on_switch_between_alpha_numeric}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum IDRule { - IDsMustBeLowerCase, - NumbersAreTheirOwnWord, - IfFirstWordNumberNoDigits, - IfLastWordNumberMustBeAppended, - ConvertRomanNumeralsToArabic, - TwoWordsOrLessUseFullWords, - MoreThanTwoWordsMakeAcronym, - IfIDDuplicateSameGameAppendYearToNewer, - IfIDDuplicateSameGameAppendProtocol, - IfIDDuplicateNoAcronym, - IfModForQueriesProcessOnlyModName, - NoDuplicates, -} - -#[derive(Clone, Debug)] -pub struct IDFail { - pub game_id: String, - pub game_name: String, - pub expected_id: String, - pub rule_stack: Vec, -} - -impl IDFail { - fn new(game_id: String, game_name: String, expected_id: String, rule_stack: Vec) -> Self { - Self { - game_id, - game_name, - expected_id, - rule_stack, - } - } -} - -/// Test a single game against the rules -pub fn test_game_name_rule( - seen_ids: &mut HashMap>, - id: &str, - mut game: GameNameParsed, - is_mod_name: bool, -) -> Vec { - let mut wrong_ids = Vec::new(); - - let mut rule_stack = Vec::new(); - if is_mod_name { - rule_stack.push(IDRule::IfModForQueriesProcessOnlyModName); - } - - let mut suffix = String::new(); - - // A game's identification is a lowercase alphanumeric string will and be forged - // following these rules: - if id.to_lowercase().ne(id) { - wrong_ids.push(IDFail::new( - id.to_owned(), - game.name.to_owned(), - id.to_lowercase(), - vec![IDRule::IDsMustBeLowerCase], - )); - } - - // 5. Roman numbering will be converted to arabic numbering (XIV -> 14). - game.words = { - let mut is_first = true; - game.words - .into_iter() - .map(|w| { - // First word will never be a numeral - if is_first { - is_first = false; - w - } else if let Ok(number) = roman_numeral::RomanNumeral::from_string(&w) { - rule_stack.push(IDRule::ConvertRomanNumeralsToArabic); - number.get_u32().to_string() - } else { - w - } - }) - .collect() - }; - - // 6. Unless numbers are at the end of a name, they will be considered words, - // but digits will always be used instead of the acronym (counter to #2) - // (Left 4 Dead -> l4d) unless they at the start position (7 Days to Die -> - // sdtd), if they are at the end (such as sequel number or the year), always - // append them (Team Fortress 2 -> teamfortress2, Unreal Tournament 2003 -> - // unrealtournament2003). - game.words = game - .words - .into_iter() - .flat_map(|w| { - let n = split_on_switch_between_alpha_numeric(&w); - if n.len() > 1 { - rule_stack.push(IDRule::NumbersAreTheirOwnWord); - } - n - }) - .collect(); - - // If first word is number make text - if !game.words.is_empty() && game.words[0].chars().next().unwrap().is_ascii_digit() { - game.words[0] = number_to_words::number_to_words(game.words[0].parse::().unwrap(), false); - rule_stack.push(IDRule::IfFirstWordNumberNoDigits); - } - - // If last word is number append full number - if let Some(last_word) = game.words.last() { - if last_word.chars().all(|c| c.is_ascii_digit()) { - suffix += &game.words.pop().unwrap(); - rule_stack.push(IDRule::IfLastWordNumberMustBeAppended); - } - } - - let main = if game.words.len() <= 2 { - // 1. Names composed of a maximum of two words (unless #4 applies) will result - // in an id where the words are concatenated (Dead Cells -> deadcells), - // acronyms in the name count as a single word (S.T.A.L.K.E.R. -> stalker). - - rule_stack.push(IDRule::TwoWordsOrLessUseFullWords); - - game.words - .iter() - .map(|w| w.trim_matches('-').to_owned()) - .collect::>() - .join("") - } else { - // 2. Names of more than two words shall be made into an acronym made of the - // initial letters (The Binding of Isaac -> tboi), hypenation composed words - // don't count as a single word, but of how many parts they are made of (Dino - // D-Day, 3 words, so ddd). - - rule_stack.push(IDRule::MoreThanTwoWordsMakeAcronym); - - game.words - .iter() - .map(|w| w.chars().next().unwrap()) - .filter(|c| c.is_alphanumeric()) - .collect() - }; - - let mut expected_id = format!("{main}{suffix}").to_lowercase(); - - if let Some(other_game_name_words) = seen_ids.get(&expected_id) { - let mut game_names_same = other_game_name_words.len() == game.words.len(); - // Check all words in game name are the same - if game_names_same { - for (our_word, their_word) in game.words.iter().zip(other_game_name_words.iter()) { - if our_word.to_lowercase() != their_word.to_lowercase() { - game_names_same = false; - break; - } - } - } - - if game_names_same { - if let Some(year) = game.year { - // 3. If a game has the exact name as a previously existing id's game (Star Wars - // Battlefront 2, the 2005 and 2017 one), append the release year to the - // newer id (2005 would be swbf2 (suppose we already have this one supported) - // and 2017 would be swbf22017). - - rule_stack.push(IDRule::IfIDDuplicateSameGameAppendYearToNewer); - expected_id = format!("{expected_id}{year}").to_lowercase(); - } else if let Some(protocol) = game.optional_parts.first() { - // 7. If a game supports multiple protocols, multiple entries will be done for - // said game where the edition/protocol name (first disposable in this order) - // will be appended to the game name (Minecraft is divided by 2 editions, - // Java and Bedrock which will be minecraftjava and minecraftbedrock - // respectively) and one more entry can be added by the base name of the game - // which queries in a group said supported protocols to make generic queries - // easier and disposable. - - rule_stack.push(IDRule::IfIDDuplicateSameGameAppendProtocol); - - // Parse the protocol as a game name so we can remove all non-valid characters - let protocol_parsed = extract_game_parts_from_name(protocol); - - expected_id = format!("{}{}", expected_id, protocol_parsed.words.concat(),); - } - } - } - - // 4. If a new id (Day of Dragons -> dod) results in an id that already exists - // (Day of Defeat -> dod), then the new name should ignore rule #2 (Day of - // Dragons -> dayofdragons). - if seen_ids.contains_key(&expected_id) { - rule_stack.push(IDRule::IfIDDuplicateNoAcronym); - - let main = game - .words - .iter() - .map(|w| w.trim_matches('-').to_owned()) - .collect::>() - .join(""); - - expected_id = format!("{main}{suffix}").to_lowercase(); - } - - // 8. If its actually about a mod that adds the ability for queries to be - // performed, process only the mod name. - if !is_mod_name && id != expected_id { - if let Some((_, mod_game)) = game.name.split_once('-') { - let mut result = test_game_name_rule(seen_ids, id, extract_game_parts_from_name(mod_game), true); - - if result.is_empty() { - return result; - } else { - wrong_ids.append(&mut result); - } - } - } - - let duplicate = if seen_ids.insert(expected_id.clone(), game.words).is_some() { - rule_stack.push(IDRule::NoDuplicates); - true - } else { - false - }; - - // Check ID matches - if id != expected_id || duplicate { - wrong_ids.push(IDFail::new( - id.to_owned(), - game.name.to_owned(), - expected_id, - rule_stack, - )); - } - - wrong_ids -} - -#[derive(Clone, Debug)] -pub struct GameNameParsed<'a> { - name: &'a str, - words: Vec, - optional_parts: Vec<&'a str>, - year: Option, -} - -pub fn extract_game_parts_from_name(game: &str) -> GameNameParsed<'_> { - // Separate game name into words - // NOTE: we have to leave "-" in to prevent hyphenated prefixes being parsed as - // numerals - let mut optional_game_name_parts = Vec::new(); - - let (game, paren) = extract_bracketed_suffix(game); - - if let Some(paren) = paren { - optional_game_name_parts.push(paren); - } - - let mut number_accumulator: Option = None; - - // Filter map necessary to move out words - #[allow(clippy::unnecessary_filter_map)] - let game_name_words: Vec<_> = game - // First split all text on space or dash - .split_inclusive(&[' ', '-']) - // Remove whitespace surrounding words (leave in dash because it is important information) - .map(str::trim) - // If a word is entirely surrounded in brackets move it to optional parts - .filter_map(|w| { - if w.starts_with('(') && w.ends_with(')') { - optional_game_name_parts.push(w); - None - } else { - Some(w) - } - }) - // Remove all characters that aren't alphanumeric or dashses - .map(|w| { - w.replace( - |c: char| !c.is_ascii_digit() && !c.is_alphabetic() && c != '-', - "", - ) - }) - // Remove words that are empty (discounting strings that are just dashes) - .filter(|w| !w.trim_matches('-').is_empty()) - // Combine numbers that are seperated by dashes - // e.g. 44-45 = 4445 - // Panics if there is text after number with trailing dash (44-text) - .filter_map(|w| { - if number_accumulator.is_some() { - if let Some(maybe_number) = w.strip_suffix('-') { - if maybe_number.chars().all(|c| c.is_ascii_digit()) { - number_accumulator.as_mut().unwrap().push_str(maybe_number); - return None; - } else { - panic!("Text after number-"); - } - } else if w.chars().all(|c| c.is_ascii_digit()) { - let mut accumulator = number_accumulator.as_ref().unwrap().clone(); - number_accumulator = None; - accumulator.push_str(&w); - return Some(accumulator); - } else { - panic!("Text after number-"); - } - } else if let Some(maybe_number) = w.strip_suffix('-') { - if maybe_number.chars().all(|c| c.is_ascii_digit()) { - number_accumulator = Some(maybe_number.to_string()); - return None; - } - } - - Some(w) - }) - .collect(); - - let mut game_year: Option = None; - for optional_part in &optional_game_name_parts { - if let Some(game_year_text) = optional_part - .strip_prefix('(') - .and_then(|s| s.strip_suffix(')')) - { - if let Ok(year) = game_year_text.parse() { - game_year = Some(year); - break; - } - } else if let Ok(year) = optional_part.parse() { - game_year = Some(year); - break; - } - } - - GameNameParsed { - name: game, - words: game_name_words, - optional_parts: optional_game_name_parts, - year: game_year, - } -} - -/// Iterate game entries and validate the id matches current rules -pub fn test_game_name_rules<'a, I: Iterator>(games: I) -> Vec { - let mut wrong_ids = Vec::with_capacity(games.size_hint().0); - - let mut seen_ids: HashMap> = HashMap::new(); - - // We must sort games by year so that rule 3 is applied correctly - let mut sorted_games: Vec<_> = games - .map(|(id, game)| { - let game = extract_game_parts_from_name(game); - - (id, game) - }) - .collect(); - - sorted_games.sort_by(|(_, a_game), (_, b_game)| { - a_game - .year - .cmp(&b_game.year) - .then(a_game.name.len().cmp(&b_game.name.len())) - }); - - let game_count = sorted_games.len(); - - for (id, game) in sorted_games { - wrong_ids.append(&mut test_game_name_rule(&mut seen_ids, id, game, false)) - } - - if !wrong_ids.is_empty() { - for fail in &wrong_ids { - println!("{fail:#?}"); - } - let percentage = (wrong_ids.len() * 100) / game_count; - println!( - "{} ({}%) IDs didn't match naming rules", - wrong_ids.len(), - percentage - ); - } - - wrong_ids -} - -pub fn test_single_game_rule(id: &str, name: &str) -> Vec { test_game_name_rules(std::iter::once((id, name))) } - -#[cfg(test)] -mod id_tests { - use super::{test_game_name_rules, test_single_game_rule}; - #[test] - fn id_rule_one() { - assert!(test_single_game_rule("testgame", "Test Game").is_empty()); - assert!(test_single_game_rule("testgame", "TestGame").is_empty()); - - assert!(test_single_game_rule("deadcells", "Dead Cells").is_empty()); - assert!(test_single_game_rule("stalker", "S.T.A.L.K.E.R").is_empty()); - } - - #[test] - fn id_rule_two() { - assert!(test_single_game_rule("tgt", "Test Game Three").is_empty()); - assert!(test_single_game_rule("tgt", "Test Game-Three").is_empty()); - - assert!(test_single_game_rule("tboi", "The Binding of Isaac").is_empty()); - assert!(test_single_game_rule("ddd", "Dino D-Day").is_empty()); - } - - #[test] - fn id_rule_three() { - let games = vec![ - ("swb22017", "Star Wars Battlefront 2 (2017)"), - ("swb2", "Star Wars Battlefront 2 (2015)"), - ]; - assert!(test_game_name_rules(games.into_iter()).is_empty()); - } - - #[test] - fn id_rule_four() { - let games = vec![("dod", "Day of Defeat"), ("dayofdragons", "Day of Dragons")]; - assert!(test_game_name_rules(games.into_iter()).is_empty()); - } - - #[test] - fn id_rule_five() { - assert!(test_single_game_rule("gta14", "Grand Theft Auto XIV").is_empty()); - } - - #[test] - fn id_rule_six() { - assert!(test_single_game_rule("l4d", "Left 4 Dead").is_empty()); - assert!(test_single_game_rule("sdtd", "7 Days to Die").is_empty()); - assert!(test_single_game_rule("teamfortress2", "Team Fortress 2").is_empty()); - assert!(test_single_game_rule("unrealtournament2003", "Unreal Tournament 2003").is_empty()); - assert!(test_single_game_rule("dhe4445", "Darkest Hour: Europe '44-'45").is_empty()); - } - - #[test] - fn id_rule_seven() { - let games = vec![ - ("minecraft", "Minecraft"), - ("minecraftjava", "Minecraft (java)"), - ("minecraftbedrock", "Minecraft (bedrock)"), - ]; - assert!(test_game_name_rules(games.into_iter()).is_empty()); - } - - #[test] - fn id_rule_eight() { - assert!(test_single_game_rule("fivem", "Grand Theft Auto V - FiveM (2013)").is_empty()); - assert!(test_single_game_rule("jc3m", "Just Cause 3 - Multiplayer").is_empty()); - } -} diff --git a/crates/id-tests/src/main.rs b/crates/id-tests/src/main.rs deleted file mode 100644 index e01b3bf..0000000 --- a/crates/id-tests/src/main.rs +++ /dev/null @@ -1,32 +0,0 @@ -#![cfg(feature = "cli")] - -use std::collections::HashMap; - -/// Format for input games (the same as used in node-gamedig/lib/games.js). -type GamesInput = HashMap; - -#[derive(Debug, Clone, PartialEq, serde::Deserialize)] -struct Game { - name: String, -} - -use gamedig_id_tests::test_game_name_rules; - -fn main() { - let games: GamesInput = std::env::args_os().nth(1).map_or_else( - || serde_json::from_reader(std::io::stdin().lock()).unwrap(), - |file| { - let file = std::fs::OpenOptions::new().read(true).open(file).unwrap(); - - serde_json::from_reader(file).unwrap() - }, - ); - - let failed = test_game_name_rules( - games - .iter() - .map(|(key, game)| (key.as_str(), game.name.as_str())), - ); - - assert!(failed.is_empty()); -} diff --git a/crates/id-tests/src/utils.rs b/crates/id-tests/src/utils.rs deleted file mode 100644 index 71dfe71..0000000 --- a/crates/id-tests/src/utils.rs +++ /dev/null @@ -1,66 +0,0 @@ -/// Split a str when characters swap between being digits and not digits. -pub fn split_on_switch_between_alpha_numeric(text: &str) -> Vec { - if text.is_empty() { - return vec![]; - } - - let mut parts = Vec::with_capacity(text.len()); - let mut current = Vec::with_capacity(text.len()); - - let mut iter = text.chars(); - let c = iter.next().unwrap(); - let mut last_was_numeric = c.is_ascii_digit(); - current.push(c); - - for c in iter { - if c.is_ascii_digit() == last_was_numeric { - current.push(c); - } else { - parts.push(current.iter().collect()); - current.clear(); - current.push(c); - last_was_numeric = !last_was_numeric; - } - } - - parts.push(current.into_iter().collect()); - - parts -} - -#[test] -fn split_correctly() { - assert_eq!( - split_on_switch_between_alpha_numeric("2D45A"), - &["2", "D", "45", "A"] - ); -} - -#[test] -fn split_symbol_broken_numbers() { - let game_name = super::extract_game_parts_from_name("Darkest Hour: Europe '44-'45"); - assert_eq!(game_name.words, &["Darkest", "Hour", "Europe", "4445"]); -} - -/// Extract parts at end of string enclosed in brackets. -pub fn extract_bracketed_suffix(text: &str) -> (&str, Option<&str>) { - if let Some(text) = text.strip_suffix(')') { - if let Some((text, extra)) = text.rsplit_once('(') { - return (text, Some(extra)); - } - } - - (text, None) -} - -#[test] -fn extract_brackets_correctly() { - assert_eq!( - extract_bracketed_suffix("no brackets here"), - ("no brackets here", None) - ); - assert_eq!( - extract_bracketed_suffix("Game name (with protocol here)"), - ("Game name ", Some("with protocol here")) - ); -} diff --git a/crates/lib/CHANGELOG.md b/crates/lib/CHANGELOG.md deleted file mode 100644 index e63f600..0000000 --- a/crates/lib/CHANGELOG.md +++ /dev/null @@ -1,674 +0,0 @@ -Who knows what the future holds... - -# X.Y.Z - DD/MM/YYYY - -# 0.9.0 22/02/2026 - -Breaking: - -- MSRV is now `1.85.1` (was `1.82.0`), this is due to deps we rely on requiring a higher version - -Games: - -- Fixed minecraft java server side EncoderException error on query (by @paul-hansen). - -Crate: - -- Some minor clippy fixes -- Updated some dependencies - -# 0.8.0 24/08/2025 - -Breaking: - -- MSRV is now `1.82.0` (was `1.81.0`), this is due to deps we rely on requiring a higher version - -Crate: - -- Some minor clippy fixes -- Updated dependencies - -# 0.7.0 - 23/04/2025 - -Breaking: - -- MSRV is now `1.81.0` (was `1.71.1`), this is due to deps we rely on requiring a higher version - -Games: - -- Added `Arma 3` support (by @Perondas). - -Crate: - -- Some minor clippy fixes - -# 0.6.1 - 05/12/2024 - -Games: - -- Added `Starbound` support (by @Novaenia). - -Protocols: - -- Fixed enum cast error on valve when parsing uppercase envrionment and server type fields (by @Novaenia). - -# 0.6.0 - 26/11/2024 - -Breaking: - -- MSRV is now `1.71.1` (was `1.65.0`), this is due to deps we rely on requiring a higher version on linux builds (`1.65.0` is 2+ years old). - -Games: - -- [Minetest](https://www.minetest.net/) support (available on the `tls`, `serde` and `services` features) (#218 by - @CosminPerRam). -- Fixed the forest game failing when host has the client steam id (#232 by @paul-hansen). - -# 0.5.2 - 20/10/2024 - -Games: - -- [Soulmask](https://store.steampowered.com/app/2646460/Soulmask/) support (by @CosminPerRam). - -Protocols: - -- Fixed Epic (EOS) protocol to match ports on query (by @cainthebest). - -Services: - -- MineTest Master Server support (available only on the `tls` and `serde` feature) (by @CosminPerRam). - -Crate: - -- Performance improvements from clippy suggestions (by @CosminPerRam). -- Feature gate some variables so that they are not unused (by @cainthebest). -- Fixed a OOB panic that could occur when reading strings from the buffer (by @cainthebest). -- Updated `pnet_packet` from `0.34.0` to `0.35.0`. - -# 0.5.1 - 12/05/2024 - -Games: - -- [Mordhau](https://store.steampowered.com/app/629760/MORDHAU/) support. -- [Enshrouded](https://store.steampowered.com/app/1203620/Enshrouded/) support. -- [Myth of Empires](https://store.steampowered.com/app/1371580/Myth_of_Empires/) support. -- [Pirates, Vikings, and Knights II](https://store.steampowered.com/app/17570/Pirates_Vikings_and_Knights_II/) support. -- [PixARK](https://store.steampowered.com/app/593600/PixARK/) support. -- [Ark: Survival Ascended](https://store.steampowered.com/app/2399830/ARK_Survival_Ascended/) support, note: not yet in - the games definitions. -- [Aliens vs. Predator 2010](https://store.steampowered.com/app/10680/Aliens_vs_Predator/) support. -- [Arma Reforger](https://store.steampowered.com/app/1874880/Arma_Reforger/) support. -- [Nova-Life: Amboise](https://store.steampowered.com/app/885570/NovaLife_Amboise/) support. -- [Abiotic Factor](https://store.steampowered.com/app/427410/Abiotic_Factor/) support. - -Protocols: - -- Epic (EOS) support, available only on the `tls` feature. - -Crate: - -- Updated some dependencies: `crc32fast` to `1.4.0`, `clap` to `4.5.4` and `ureq` to `ureq`. - -# 0.5.0 - 15/03/2024 - -### Changes: - -Games: - -- [Valheim](https://store.steampowered.com/app/892970/Valheim/) support. -- [The Front](https://store.steampowered.com/app/2285150/The_Front/) support. -- [Conan Exiles](https://store.steampowered.com/app/440900/Conan_Exiles/) support. -- [Post Scriptum](https://store.steampowered.com/app/736220/Post_Scriptum/) support. -- [Squad](https://store.steampowered.com/app/393380/Squad/) support. -- [Savage 2](https://savage2.net/) support. -- [Rising World](https://store.steampowered.com/app/324080/Rising_World/) support. -- [ATLAS](https://store.steampowered.com/app/834910/ATLAS/) support. -- [America's Army: Proving Grounds](https://store.steampowered.com/app/203290/Americas_Army_Proving_Grounds/) support. -- [Base Defense](https://store.steampowered.com/app/632730/Base_Defense/) support. -- [Zombie Panic: Source](https://store.steampowered.com/app/17500/Zombie_Panic_Source/) support. -- Added a valve protocol query example. -- Made all of Just Cause 2: Multiplayer Response and Player fields public. -- [Mindustry](https://mindustrygame.github.io/) support. -- [Eco](https://store.steampowered.com/app/382310/Eco/) support. -- [Call Of Duty: Black Ops 3](https://store.steampowered.com/agecheck/app/311210/) support. -- [Counter-Strike 2](https://store.steampowered.com/app/730/CounterStrike_2/) support. -- [Double Action: Boogaloo](https://store.steampowered.com/app/317360/Double_Action_Boogaloo/) support. - -Crate: - -- Changed the serde feature to only enable serde derive for some types: serde and serde_json is now a dependecy by - default. - -Protocols: - -- Added the unreal2 protocol and its associated games: Darkest Hour, Devastation, Killing Floor, Red Orchestra, Unreal - Tournament 2003, Unreal Tournament 2004 (by @Douile). -- Added HTTPClient to allow use of HTTP(S) (and JSON) APIs (by @CosminPerRam & @Douile). - -Crate: - -- Added a `packet_capture` feature to capture the raw packets sent and received by the socket (by @Douile). -- Added packet emulation and socket retrevial using the `packet_capture` feature (by @Douile). -- Added PCAP writing support to the `packet_capture` feature (by @Douile & @cainthebest). -- Refactored socket to use a custom implementation of socket for packet capture when using the `packet_capture` - feature (by @Douile). - -CLI: - -- Added a CLI (by @cainthebest). -- Added DNS lookup support (by @Douile). -- Added JSON output option (by @Douile). -- Added BSON output in hex or base64 (by @cainthebest). -- Added XML output option (by @cainthebest). -- Added ExtraRequestSettings as CLI arguments (by @Douile). -- Added TimeoutSettings as CLI argument (by @Douile). -- Added Comprehensive end-user documentation for the CLI interface (by @Douile & @cainthebest). -- Tweaked compile-time flags to allow for a more preformant binary (by @cainthebest). -- Added client for socket capture, dev tools are not included by default (by @Douile). -- Added license information to the CLI (by @cainthebest). -- Added source code information to the CLI (by @cainthebest). - -### Breaking: - -Game: - -- Changed identifications of the following games as they weren't properly expecting the naming rules: -- - - Left 4 Dead: `left4dead` -> `l4d`. -- - - 7 Days to Die: `7d2d` in definitions and `sd2d` in game declaration -> `sdtd`. -- - - Quake 3 Arena: `quake3arena` -> `q3a`. -- - - Unreal tournament 2003: `ut2003` -> `unrealtournament2003` -- - - Unreal tournament 2004: `ut2004` -> `unrealtournament2004` -- - - Darkest Hour: Europe '44-'45: `darkesthour` -> `dhe4445` -- Minecraft: -- - - Legacy 1.5 and 1.3 were renamed to 1.4 and beta 1.8 respectively to show the lowest version they support, this - change includes Structs, Enum and game id renames, also removed the "v" from the game definition name. -- - - Moved the Minecraft protocol implementation in the games folder as its proprietary. - -Protocols: - -- Valve: Removed `SteamApp` due to it not being really useful at all, replaced all instances with `Engine`. - -Query: - -- Added a connection timeout to TimeoutSettings (at the moment this only applies to TCP) - - Sockets are now expected to apply timeout settings in new() - -# 0.4.1 - 13/10/2023 - -### Changes: - -Game: - -- Added [Barotrauma](https://store.steampowered.com/app/602960/Barotrauma/) support. - -Crate: - -- Added `Send` and `Sync` on `Error::source` to fix some async issues. - -Protocols: - -- Minecraft Java: Add derives to `RequestSettings` and add `new_just_hostname` that creates new settings just by - specifying - the hostname, `protocol_version` defaults to -1. - -Games: - -- Organised game modules into protocols (when protocol used by other games), - you can now access a game by its name or by its protocol name: - - `use gamedig::games::teamfortress2;` - - `use gamedig::games::valve::teamfortress2;` - -Generics: - -- Added standard derives to `ProprietaryProtocol`, `CommonResponseJson`, `CommonPlayerJson`, `TimeoutSettings` and - `ExtraRequestSettings`. - -### Breaking... - -None, yaay! - -# 0.4.0 - 07/10/2023 - -### Changes: - -Games: - -- [Creativerse](https://store.steampowered.com/app/280790/Creativerse/) support. - -Protocols: - -- Quake 2: Fixed a bug where the version tag wouldn't always be present. -- The Ship: Removed instances of using `unwrap` without handling the panics. - -Crate: - -- Updated [byteorder](https://crates.io/crates/byteorder) dependency from 1.4 to 1.5. -- Rich errors, capturing backtrace is done on `RUST_BACKTRACE=1`. (by @Douile) -- Applied some nursery Clippy lints. -- The `retries` field was added to `TimeoutSettings` that specifies the number of times to retry a failed request ( - request being individual send, receive sequence, some protocols can include multiple requests in a single query). (by - @Douile) - - By default `retries` is set to `0`, meaning no retries will be attempted - -Generics: - -- Added `ExtraRequestSettings` containing all possible extra request settings. (by @Douile) -- Added `query_with_timeout_and_extra_settings()` to allow generic queries with extra settings. (by @Douile) - -### Breaking... - -Crate: - -- The enum used for errors, `GDError` has been renamed to `GDErrorKind`. -- `GDError` is now a struct that holds its kind, the source and a backtrace. -- The `Socket::apply_timeout` method now borrows `TimeoutSettings` (`&Option`) - - To make this easier to work with a new method was added - to `TimeoutSettings`: `TimeoutSettings::get_read_and_write_or_defaults` this takes a borrowed - optional `TimeoutSettings` and returns the contained read and write durations or the default read and write - durations. - -Generics: - -- Renamed `CommonResponseJson`'s `game` field (and the function) to `game_mode`. -- Changed `players_maximum` and `players_online` (and their functions) types from `u64` to `u32`. -- Changed `score` type (and the function) of player from `u32` to `i32`. - -Games: - -- Rename some game definitions and implementations to follow a stable ID naming system. - -Protocols: - -- Valve: - -1. Renamed `protocol` to `protocol_version`. -2. Renamed `version` to `game_version`. -3. Renamed `game` to `game_mode`. -4. Fixed `player`'s `score` field being `u32` when it needed to be `i32`, as specified in the protocol. -5. Added the field `check_app_id` to `GatherSettings` which controls if the app id specified to the request and - reported by the server are the same, errors if not, enabled by default. (by @Douile) -6. Valve: Renamed SteamApp enum variants to match new definition names - -- GameSpy (1, 2, 3): - -1. Renamed `version` to `game_version`. -2. Changed `players_maximum` and `players_online` (and their functions) types from `usize` to `u32`. - -- GameSpy 1: - -1. Renamed the player's `frags` to `score` and type from `u32` to `i32`. -2. Made `Option` the following response fields `team`, `face`, `skin`, `mesh` and `secret` to fix missing fields - issues. (by @Douile) - -- Quake (1, 2): - -1. Renamed `game_type` to `game_mode`. -2. Changed `version` type from `String`to `Option`. - -- Minecraft Java - -1. Renamed `version_protocol` to `protocol_version`. -2. Renamed `version_name` to `game_version`. -3. Renamed `players_sample` to `players`. -4. Added an optional parameter, `RequestSettings`, which contains fields that are used when creating the handshake - packet (this solves some servers not responding to the query). (by @Douile) -5. Legacy versions naming has been changed to represent up to what version they can query, `LegacyBV1_8` (Beta 1.8 to - 1.3) -> `LegacyV1_3` and `LegacyV1_4` (1.4 to 1.5) -> `LegacyV1_5` (and their enums accordingly). - -- Minecraft Bedrock - -1. Renamed `version_protocol` to `protocol_version`. - -- Minecraft: - -1. Added an optional parameter, `request_settings` parameter to `query`. - -- The Ship: - -1. Renamed `protocol` to `protocol_version`. -2. Renamed `max_players` to `players_maximum` and changed its type from `u64` to `u32`. -3. Renamed `bots` to `players_bots`. and changed its type from `u64` to `u32`. -4. Renamed `players` to `players_online`. -5. Renamed `players_details` to `players`. -6. Renamed `game` to `game_mode`. -7. Added field `game_version`. -8. Changed `players_bots` type from `Option` to `Option`. -9. Changed `score` type of player from `u32` to `i32`. - -- Frontlines: Fuel of War: - -1. Renamed `game_mode` to `game`. -2. Renamed `version` to `game_version`. -3. Renamed `protocol` to `protocol_version`. -4. Renamed `game` to `game_mode`. -5. Changed `players_maximum` and `players_minimum` types from `usize` to `u32`. - -- Just Cause 2: Multiplayer: - -1. Renamed `version` to `game_version`. -2. Changed `players_maximum` and `players_minimum` types from `usize` to `u32`. - -# 0.3.0 - 18/07/2023 - -### Changes: - -Protocols: - -- GameSpy 2 support. -- Quake 2: Added Optional address field to Player. - -Generic query: - -- Added generic queries (by [@Douile](https://github.com/Douile)) which come with a common struct for the response - fields. -- The supported games list is available programmatically. - -Games: - -- [Halo: Combat Evolved](https://en.wikipedia.org/wiki/Halo:_Combat_Evolved) support. -- [Just Cause 2: Multiplayer](https://store.steampowered.com/app/259080/Just_Cause_2_Multiplayer_Mod/) support. -- [Warsow](https://warsow.net/) support. - -Internal: - -- Buffer reader rewrite, resulting in more data checks and better code quality ( - thanks [@cainthebest](https://github.com/cainthebest)). -- Better CI to never break accidentally MSRV again (thanks [@Douile](https://github.com/Douile)). - -### Breaking... - -Protocols: - -- Quake 2: Renamed the players `frags` field to `score` to be more inline with the other protocols. - -Crate: - -- `no_games` and `no_services` have been changed to `games` and `services`, this better represents that they are present - by default (by [@Douile](https://github.com/Douile)). -- Fixed crate's `rust-version`, it is now `1.60.0` (was `1.56.1`) - -# 0.2.3 - 02/06/2023 - -### Changes: - -Protocols: - -- Valve: - -1. Added standard and serde derives to `GatheringSettings`. - -- Quake 1, 2 and 3 support. - -Games: - -- [Quake 2](https://store.steampowered.com/app/2320/Quake_II/) support. -- [Quake 1](https://store.steampowered.com/app/2310/Quake/) support. -- [Quake 3: Arena](https://store.steampowered.com/app/2200/Quake_III_Arena/) support. -- [Hell Let Loose](https://store.steampowered.com/app/686810/Hell_Let_Loose/) support. -- [Soldier of Fortune 2](https://www.gog.com/en/game/soldier_of_fortune_ii_double_helix_gold_edition) support. - -### Breaking: - -- Every function that used `&str` for the address has been changed to `&IpAddr` ( - thanks [@Douile](https://github.com/Douile) for the re-re-write). -- Protocols now use `&SocketAddr` instead of `address: &str, port: u16`. - -Services: - -- Valve Master Query: - -1. Removed Filter and SearchFilters lifetimes and changed `&'a str` to `String` and `&'a [&'a str]` to `Vec` - -# 0.2.2 - 01/05/2023 - -### Changes: - -Crate: - -- General optimizations thanks to [cargo clippy](https://github.com/rust-lang/rust-clippy) - and [@cainthebest](https://github.com/cainthebest). -- Added feature `serde` which enables json serialization/deserialization for all types ( - by [@cainthebest](https://github.com/cainthebest)). -- Documentation improvements. - -Protocols: - -- GameSpy 1: Add key `admin` as a possible variable for `admin_name`. -- GameSpy 3 support. - -Games: - -- [Serious Sam](https://www.gog.com/game/serious_sam_the_first_encounter) support. -- [Frontlines: Fuel of War](https://store.steampowered.com/app/9460/Frontlines_Fuel_of_War/) support. -- [Crysis Wars](https://steamcommunity.com/app/17340) support. - -Services: - -- [Valve Master Server Query](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol) support. -- Added feature `no_services` which disables the supported services. - -### Breaking: - -Protocols: - -- Valve: Request type enums have been renamed from all caps to starting-only uppercase, ex: `INFO` to `Info` -- GameSpy 1: `players_minimum` is now an `Option` instead of an `u8` -- GameSpy 1: Is now under `protocols::gamespy::one` instead of `protocols::gamespy` - -# 0.2.1 - 03/03/2023 - -### Changes: - -Crate: - -- Added feature `no_games` which disables the supported games (useful when only the - protocols/services are needed, also saves storage space). - -Games: - -- [V Rising](https://store.steampowered.com/app/1604030/V_Rising/) support. -- [Unreal Tournament](https://en.wikipedia.org/wiki/Unreal_Tournament) support. -- [Battlefield 1942](https://www.ea.com/games/battlefield/battlefield-1942) support. - -Protocols: - -- Valve: - -1. Reversed (from `0.1.0`) "Players with no name are no more added to the `players_details` field.", also added a note - in the [protocols](PROTOCOLS.md) file regarding this. -2. Fixed querying while multiple challenge responses might happen. - -- GameSpy 1 support. - -### Breaking: - -None. - -# 0.2.0 - 18/02/2023 - -### Changes: - -Games: - -- [Don't Starve Together](https://store.steampowered.com/app/322330/Dont_Starve_Together/) support. -- [Colony Survival](https://store.steampowered.com/app/366090/Colony_Survival/) support. -- [Onset](https://store.steampowered.com/app/1105810/Onset/) support. -- [Codename CURE](https://store.steampowered.com/app/355180/Codename_CURE/) support. -- [Ballistic Overkill](https://store.steampowered.com/app/296300/Ballistic_Overkill/) support. -- [BrainBread 2](https://store.steampowered.com/app/346330/BrainBread_2/) support. -- [Avorion](https://store.steampowered.com/app/445220/Avorion/) support. -- [Operation: Harsh Doorstop](https://store.steampowered.com/app/736590/Operation_Harsh_Doorstop/) support. - -Protocols: - -- Valve: - -1. `appid` is now a field in the `Response` struct. - -### Breaking: - -Protocols: - -- Valve: - due to some games being able to host a server from within the game AND from a dedicated server, - if you were to query one of them, the query would fail for the other one, as the `SteamID` enum - for that game could specify only one id. - -1. `SteamID` is now `SteamApp`, was an u32 enum, and now it's a simple enum. -2. `App` is now `Engine`, the `Source` enum's structure has been changed from `Option` to - `Option>`, where the first parameter is the game app id and the second is - the dedicated server app id (if there is one). - -# 0.1.0 - 17/01/2023 - -### Changes: - -Games: - -- [Risk of Rain 2](https://store.steampowered.com/app/632360/Risk_of_Rain_2/) support. -- [Battalion 1944](https://store.steampowered.com/app/489940/BATTALION_Legacy/) support. -- [Black Mesa](https://store.steampowered.com/app/362890/Black_Mesa/) support. -- [Project Zomboid](https://store.steampowered.com/app/108600/Project_Zomboid/) support. -- [Age of Chivalry](https://store.steampowered.com/app/17510/Age_of_Chivalry/) support. - -Protocols: - -- Valve: Players with no name are no more added to the `players_details` field. -- Valve: Split packets are now appending in the correct order. - -Crate: - -- `MSRV` is now `1.56.1` (was `1.58.1`) - -### Breaking: - -Protocols: - -- Valve: The rules field is now a `HashMap` instead of a `Vec` (where the `ServerRule` - structure had a name and a value fields). -- Valve: Structs that contained the `players`, `max_players` and `bots` fields have been renamed - to `players_online`, `players_maximum` and `players_bots` respectively. -- Minecraft: Structs that contained the `online_players`, `max_players` and `sample_players` fields have been renamed - to `players_online`, `players_maximum` and `players_sample` respectively. -- Minecraft: The Java query response struct named `Response` has been renamed to `JavaResponse`. - -Errors: - -- Besides the `BadGame` error, now no other errors returns details about what happened (as it was quite pointless). - -Crate: - -- `package.metadata.msrv` has been replaced with `package.rust-version` - -# 0.0.7 - 03/01/2023 - -### Changes: - -[Minecraft](https://www.minecraft.com) bedrock edition support. -Fix Minecraft legacy v1.6 max/online players count being reversed. -Added `query_legacy_specific` method to the Minecraft protocol. - -### Breaking: - -Removed `query_specific` from the mc protocol in favor of `query_java`, `query_legacy` and `query_legacy_specific`. -Some public functions that are meant to be used only internally were made private. - -# 0.0.6 - 28/11/2022 - -[Minecraft](https://www.minecraft.com) support (bedrock not supported yet). -[7 Days To Die](https://store.steampowered.com/app/251570/7_Days_to_Die/) support. -[ARK: Survival Evolved](https://store.steampowered.com/app/346110/ARK_Survival_Evolved/) support. -[Unturned](https://store.steampowered.com/app/304930/Unturned/) support. -[The Forest](https://store.steampowered.com/app/242760/The_Forest/) support. -[Team Fortress Classic](https://store.steampowered.com/app/20/Team_Fortress_Classic/) support. -[Sven Co-op](https://store.steampowered.com/app/225840/Sven_Coop/) support. -[Rust](https://store.steampowered.com/app/252490/Rust/) support. -[Counter-Strike](https://store.steampowered.com/app/10/CounterStrike/) support. -[Arma 2: Operation Arrowhead](https://store.steampowered.com/app/33930/Arma_2_Operation_Arrowhead/) support. -[Day of Infamy](https://store.steampowered.com/app/447820/Day_of_Infamy/) support. -[Half-Life Deathmatch: Source](https://store.steampowered.com/app/360/HalfLife_Deathmatch_Source/) support. -Successfully tested `Alien Swarm` and `Insurgency: Modern Infantry Combat`. -Restored rules response for `Counter-Strike: Global Offensive` (note: for a full player list response, the -cvar `host_players_show` must be set to `2`). -Increased Valve Protocol `PACKET_SIZE` from 1400 to 6144 (because some games send larger packets than the specified -protocol size). -Removed DNS resolving as it was not needed. -Valve Protocol minor optimizations. - -# 0.0.5 - 15/11/2022 - -Added `SocketBind` error, regarding failing to bind a socket. -Socket custom timeout capability (with an error if provided durations are zero). -Because of this, a parameter similar to GatherSettings has been added on the Valve Protocol Query. -Support for GoldSrc split packets and obsolete A2S_INFO response. -Changed the Valve Protocol app parameter to represent the engine responses. -It is now an enum of: - -- `Source(Option)` - A Source response with optionally, the id (if the id is present and the response id is not the - same, the query fails), if it isn't provided, find it. -- `GoldSrc(bool)` - A GoldSrc response with the option to enforce the obsolete A2S_INFO response. - -Fixed Source multi-packet response crash due to when a certain app with a certain protocol doesn't have the Size -field. -Reduced Valve Protocol `PACKET_SIZE` to be as specified from 2048 to 1400. -[Counter-Strike: Condition Zero](https://store.steampowered.com/app/80/CounterStrike_Condition_Zero/) implementation. -[Day of Defeat](https://store.steampowered.com/app/30/Day_of_Defeat/) implementation. -Games besides CSGO and TS now have the same response structure. - -# 0.0.4 - 23/10/2022 - -Queries now support DNS resolve. -Changed uses a bit, example: from `use gamedig::valve::ValveProtocol::query` -to `use gamedig::protocols::valve::query`. -Changed Valve Protocol Query parameters to (ip, port, app, gather_settings), changes include: - -- the app is now optional, being None means to anonymously query the server. -- gather_settings is now also an optional, being None means all query settings. - -Valve Protocol now supports querying anonymous apps (see previous lines). -Better bad game error. -[Alien Swarm](https://store.steampowered.com/app/630/Alien_Swarm/) implementation (not tested). -[Alien Swarm: Reactive Drop](https://store.steampowered.com/app/563560/Alien_Swarm_Reactive_Drop/) implementation. -[Insurgency](https://store.steampowered.com/app/222880/Insurgency/) implementation. -[Insurgency: Sandstorm](https://store.steampowered.com/app/581320/Insurgency_Sandstorm/) implementation. -[Insurgency: Modern Infantry Combat](https://store.steampowered.com/app/17700/INSURGENCY_Modern_Infantry_Combat/) -implementation (not tested). - -# 0.0.3 - 22/10/2022 - -Valve protocol now properly supports multi-packet responses (compressed ones not tested). -CSGO, TF2 and TS now have independent Responses, if you want a generic one, query the protocol. -[Counter Strike: Source](https://store.steampowered.com/app/240/CounterStrike_Source/) implementation (if protocol is 7, -queries with multi-packet responses will crash). -[Day of Defeat: Source](https://store.steampowered.com/app/300/Day_of_Defeat_Source/) implementation. -[Garry's Mod](https://store.steampowered.com/app/4000/Garrys_Mod/) implementation. -[Half-Life 2 Deathmatch](https://store.steampowered.com/app/320/HalfLife_2_Deathmatch/) implementation. -[Left 4 Dead](https://store.steampowered.com/app/500/Left_4_Dead/) implementation. -[Left 4 Dead 2](https://store.steampowered.com/app/550/Left_4_Dead_2/) implementation. - -# 0.0.2 - 20/10/2022 - -Further implementation of the Valve protocol (PLAYERS and RULES queries). -[Counter Strike: Global Offensive](https://store.steampowered.com/app/730/CounterStrike_Global_Offensive/) -implementation. -[The Ship](https://developer.valvesoftware.com/wiki/The_Ship) implementation. -The library now has error handling. - -# 0.0.1 - 16/10/2022 - -The first usable version of the crate, yay! -It brings: -Initial implementation of the [Valve server query protocol](https://developer.valvesoftware.com/wiki/Server_queries). -Initial [Team Fortress 2](https://en.wikipedia.org/wiki/Team_Fortress_2) support. - -# 0.0.0 - 15/10/2022 - -The first *markdown*, the crate is unusable as it doesn't contain anything helpful. diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml deleted file mode 100644 index cb4aaee..0000000 --- a/crates/lib/Cargo.toml +++ /dev/null @@ -1,78 +0,0 @@ -[package] -name = "gamedig" -version = "0.9.0" -edition = "2021" -authors = [ - "rust-GameDig contributors [https://github.com/gamedig/rust-gamedig/contributors]", - "node-GameDig contributors [https://github.com/gamedig/node-gamedig/contributors]", -] -license = "MIT" -description = "Query game servers and not only." -homepage = "https://gamedig.github.io/" -documentation = "https://docs.rs/gamedig/latest/gamedig/" -repository = "https://github.com/gamedig/rust-gamedig" -readme = "README.md" -keywords = ["server", "query", "game", "check", "status"] -rust-version = "1.85.1" -categories = ["parser-implementations", "parsing", "network-programming", "encoding"] - -[features] -default = ["games", "services", "game_defs"] - -# Enable query functions for specific games -games = [] -# Enable game definitions for use with the generic query functions -game_defs = ["dep:phf", "games"] - -# Enable service querying -services = [] - -# Enable serde derivations for our types -serde = [] - -# Enable clap derivations for our types -clap = ["dep:clap"] -packet_capture = ["dep:pcap-file", "dep:pnet_packet", "dep:lazy_static"] - -# Enable TLS for HTTP Client -tls = ["ureq/tls"] - -[dependencies] -url = "2.5.8" -byteorder = "1.5.0" -bzip2-rs = "0.1.2" -crc32fast = "1.5.0" -base64 = "0.22.1" -encoding_rs = "0.8.35" -serde_json = { version = "1.0.149" } -serde = { version = "1.0.228", features = ["derive"] } -ureq = { version = "2.12.1", default-features = false, features = ["gzip", "json"] } -phf = { version = "0.13.1", optional = true, features = ["macros"] } -clap = { version = "4.5.60", optional = true, features = ["derive"] } -pcap-file = { version = "2.0.0", optional = true } -pnet_packet = { version = "0.35.0", optional = true } -lazy_static = { version = "1.5.0", optional = true } - -[dev-dependencies] -gamedig-id-tests = { path = "../id-tests", default-features = false } - -# Examples -[[example]] -name = "minecraft" -required-features = ["games"] - -[[example]] -name = "teamfortress2" -required-features = ["games"] - -[[example]] -name = "valve_master_server_query" -required-features = ["services"] - -[[example]] -name = "test_eco" -required-features = ["games"] - -[[example]] -name = "generic" -required-features = ["games", "game_defs"] diff --git a/crates/lib/README.md b/crates/lib/README.md deleted file mode 100644 index 6f4e1f7..0000000 --- a/crates/lib/README.md +++ /dev/null @@ -1,114 +0,0 @@ -

rust-GameDig

- -
The fast library for querying game servers/services.
- - - -
- This library brings what - - node-GameDig - - does (and not only), to pure Rust! -
- -**Warning**: This project goes through frequent API breaking changes and hasn't been thoroughly tested. - -## Community - -Checkout the GameDig Community Discord Server [here](https://discord.gg/NVCMn3tnxH). -Note that it isn't be a replacement for GitHub issues, if you have found a problem -within the library or want to request a feature, it's better to do so here rather than -on Discord. - -## Usage - -Minimum Supported Rust Version is `1.85.1` and the code is cross-platform. - -Pick a game/service/protocol (check -the [GAMES](https://github.com/gamedig/rust-gamedig/blob/main/GAMES.md), [SERVICES](https://github.com/gamedig/rust-gamedig/blob/main/SERVICES.md) -and [PROTOCOLS](https://github.com/gamedig/rust-gamedig/blob/main/PROTOCOLS.md) files to see the currently supported -ones), provide the ip and the port (be aware that some game servers use a separate port for the info queries, the port -can also be optional if the server is running the default ports) then query on it. - -[Team Fortress 2](https://store.steampowered.com/app/440/Team_Fortress_2/) query example: - -```rust -use gamedig::games::teamfortress2; - -fn main() { - let response = teamfortress2::query(&"127.0.0.1".parse().unwrap(), None); - // None is the default port (which is 27015), could also be Some(27015) - - match response { // Result type, must check what it is... - Err(error) => println!("Couldn't query, error: {}", error), - Ok(r) => println!("{:#?}", r) - } -} -``` - -Response (note that some games have a different structure): - -```json5 -{ - protocol: 17, - name: "Team Fortress 2 Dedicated Server.", - map: "ctf_turbine", - game: "tf2", - appid: 440, - players_online: 0, - players_details: [], - players_maximum: 69, - players_bots: 0, - server_type: Dedicated, - has_password: false, - vac_secured: true, - version: "7638371", - port: Some(27015), - steam_id: Some(69753253289735296), - tv_port: None, - tv_name: None, - keywords: Some( - "alltalk,nocrits" - ), - rules: [ - "mp_autoteambalance" - : - "1", - "mp_maxrounds" - : - "5", - //.... - ] -} -``` - -Want to see more examples? Checkout -the [examples](https://github.com/gamedig/rust-gamedig/tree/main/crates/lib/examples) folder. - -## Documentation - -The documentation is available at [docs.rs](https://docs.rs/gamedig/latest/gamedig/). -Curious about the history and what changed between versions? Everything is in -the [CHANGELOG](https://github.com/gamedig/rust-gamedig/blob/main/crates/lib/CHANGELOG.md) file. - -## Contributing - -If you want to see your favorite game/service being supported here, open an issue, and I'll prioritize it (or do a pull -request if you want to implement it yourself)! - -Before contributing please read [CONTRIBUTING](https://github.com/gamedig/rust-gamedig/blob/main/CONTRIBUTING.md). - diff --git a/crates/lib/examples/generic.rs b/crates/lib/examples/generic.rs deleted file mode 100644 index d54a0b3..0000000 --- a/crates/lib/examples/generic.rs +++ /dev/null @@ -1,129 +0,0 @@ -use gamedig::{ - protocols::types::CommonResponse, - query_with_timeout_and_extra_settings, - ExtraRequestSettings, - GDResult, - Game, - TimeoutSettings, - GAMES, -}; - -use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; - -/// Make a query given the name of a game -/// The `game` argument is taken from the [GAMES](gamedig::GAMES) map. -fn generic_query( - game: &Game, - addr: &IpAddr, - port: Option, - timeout_settings: Option, - extra_settings: Option, -) -> GDResult> { - println!("Querying {:#?} with game {:#?}.", addr, game); - - let response = query_with_timeout_and_extra_settings(game, addr, port, timeout_settings, extra_settings)?; - println!("Response: {:#?}", response.as_json()); - - let common = response.as_original(); - println!("Common response: {:#?}", common); - - Ok(response) -} - -fn main() { - let mut args = std::env::args().skip(1); - - // Handle arguments - if let Some(game_name) = args.next() { - let hostname = args.next().expect("Must provide an address"); - // Use to_socket_addrs to resolve hostname to IP - let addr: SocketAddr = format!("{}:0", hostname) - .to_socket_addrs() - .unwrap() - .next() - .expect("Could not lookup host"); - let port: Option = args.next().map(|s| s.parse().unwrap()); - - let timeout_settings = TimeoutSettings::new( - TimeoutSettings::default().get_read(), - TimeoutSettings::default().get_write(), - TimeoutSettings::default().get_connect(), - 2, - ) - .unwrap(); - - let game = GAMES - .get(&game_name) - .expect("Game doesn't exist, run without arguments to see a list of games"); - - let extra_settings = game - .request_settings - .clone() - .set_hostname(hostname.to_string()) - .set_check_app_id(false); - - generic_query( - game, - &addr.ip(), - port, - Some(timeout_settings), - Some(extra_settings), - ) - .unwrap(); - } else { - // Without arguments print a list of games - for (name, game) in GAMES.entries() { - println!("{}\t{}", name, game.name); - } - } -} - -#[cfg(test)] -mod test { - use gamedig::{protocols::types::TimeoutSettings, GAMES}; - use std::{ - net::{IpAddr, Ipv4Addr}, - time::Duration, - }; - - use super::generic_query; - - const ADDR: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST); - - fn test_game(game_name: &str) { - let timeout_settings = Some( - TimeoutSettings::new( - Some(Duration::from_nanos(1)), - Some(Duration::from_nanos(1)), - Some(Duration::from_nanos(1)), - 0, - ) - .unwrap(), - ); - - let game = GAMES - .get(game_name) - .expect("Game doesn't exist, run without arguments to see a list of games"); - - assert!(generic_query(game, &ADDR, None, timeout_settings, None).is_err()); - } - - #[test] - fn battlefield1942() { test_game("battlefield1942"); } - - #[test] - fn minecraft() { test_game("minecraft"); } - - #[test] - fn teamfortress2() { test_game("teamfortress2"); } - - #[test] - fn quake2() { test_game("quake2"); } - - #[test] - fn all_games() { - for game_name in GAMES.keys() { - test_game(game_name); - } - } -} diff --git a/crates/lib/examples/minecraft.rs b/crates/lib/examples/minecraft.rs deleted file mode 100644 index ca49a5e..0000000 --- a/crates/lib/examples/minecraft.rs +++ /dev/null @@ -1,31 +0,0 @@ -use gamedig::minecraft; -use gamedig::minecraft::types::RequestSettings; - -fn main() { - // or Some(), None is the default protocol port (which is 25565 for java - // and 19132 for bedrock) - let response = minecraft::query(&"127.0.0.1".parse().unwrap(), None); - // This will fail if no server is available locally! - - match response { - Err(error) => println!("Couldn't query, error: {}", error), - Ok(r) => println!("{:#?}", r), - } - - // This is an example to query a server with a hostname to be specified in the - // packet. Passing -1 on the protocol_version means anything, note that - // an invalid value here might result in server not responding. - let response = minecraft::query_java( - &"209.222.114.62".parse().unwrap(), - Some(25565), - Some(RequestSettings { - hostname: "mc.hypixel.net".to_string(), - protocol_version: -1, - }), - ); - - match response { - Err(error) => println!("Couldn't query, error: {}", error), - Ok(r) => println!("{:#?}", r), - } -} diff --git a/crates/lib/examples/teamfortress2.rs b/crates/lib/examples/teamfortress2.rs deleted file mode 100644 index 787497c..0000000 --- a/crates/lib/examples/teamfortress2.rs +++ /dev/null @@ -1,12 +0,0 @@ -use gamedig::games::teamfortress2; - -fn main() { - let response = teamfortress2::query(&"127.0.0.1".parse().unwrap(), None); - // or Some(27015), None is the default protocol port (which is 27015) - - match response { - // Result type, must check what it is... - Err(error) => println!("Couldn't query, error: {}", error), - Ok(r) => println!("{:#?}", r), - } -} diff --git a/crates/lib/examples/test_eco.rs b/crates/lib/examples/test_eco.rs deleted file mode 100644 index 735fe9c..0000000 --- a/crates/lib/examples/test_eco.rs +++ /dev/null @@ -1,10 +0,0 @@ -use gamedig::games::eco; -use std::net::IpAddr; -use std::str::FromStr; - -fn main() { - let ip = IpAddr::from_str("142.132.154.69").unwrap(); - let port = 31111; - let r = eco::query(&ip, Some(port)); - println!("{:#?}", r); -} diff --git a/crates/lib/examples/valve_master_server_query.rs b/crates/lib/examples/valve_master_server_query.rs deleted file mode 100644 index 7b33ac1..0000000 --- a/crates/lib/examples/valve_master_server_query.rs +++ /dev/null @@ -1,14 +0,0 @@ -use gamedig::valve_master_server::{query, Filter, Region, SearchFilters}; - -fn main() { - let search_filters = SearchFilters::new() - .insert(Filter::RunsAppID(440)) - .insert(Filter::CanBeEmpty(false)) - .insert(Filter::CanBeFull(false)) - .insert(Filter::CanHavePassword(false)) - .insert(Filter::IsSecured(true)) - .insert(Filter::HasTags(vec!["minecraft".to_string()])); - - let ips = query(Region::Europe, Some(search_filters)).unwrap(); - println!("Servers: {:?} \n Amount: {}", ips, ips.len()); -} diff --git a/crates/lib/examples/valve_protocol_query.rs b/crates/lib/examples/valve_protocol_query.rs deleted file mode 100644 index 368453c..0000000 --- a/crates/lib/examples/valve_protocol_query.rs +++ /dev/null @@ -1,36 +0,0 @@ -use gamedig::protocols::types::GatherToggle; -use gamedig::protocols::valve; -use gamedig::protocols::valve::{Engine, GatheringSettings}; -use gamedig::TimeoutSettings; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::time::Duration; - -fn main() { - let address = &SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 27015); - let engine = Engine::Source(None); // We don't specify a steam app id, let the query try to find it. - let gather_settings = GatheringSettings { - players: GatherToggle::Enforce, // We want to query for players - rules: GatherToggle::Skip, // We don't want to query for rules - check_app_id: false, // Loosen up the query a bit by not checking app id - }; - - let read_timeout = Duration::from_secs(2); - let write_timeout = Duration::from_secs(3); - let connect_timeout = Duration::from_secs(4); - let retries = 1; // does another request if the first one fails. - let timeout_settings = TimeoutSettings::new( - Some(read_timeout), - Some(write_timeout), - Some(connect_timeout), - retries, - ) - .unwrap(); - - let response = valve::query( - address, - engine, - Some(gather_settings), - Some(timeout_settings), - ); - println!("{response:#?}"); -} diff --git a/crates/lib/src/buffer.rs b/crates/lib/src/buffer.rs deleted file mode 100644 index dff7cf0..0000000 --- a/crates/lib/src/buffer.rs +++ /dev/null @@ -1,612 +0,0 @@ -use crate::GDErrorKind::PacketBad; -use crate::GDErrorKind::PacketUnderflow; -use crate::GDResult; -use byteorder::{BigEndian, ByteOrder, LittleEndian}; -use std::{convert::TryInto, marker::PhantomData}; - -/// A struct representing a buffer with a specific byte order. -/// -/// It's comprised of a byte slice that it reads from, a cursor to keep track of -/// the current position within the byte slice, and a `PhantomData` marker to -/// bind it to a specific byte order (BigEndian or LittleEndian). -/// -/// The byte order is defined by the `B: ByteOrder` generic parameter. -pub struct Buffer<'a, B: ByteOrder> { - /// The byte slice that the buffer reads from. - data: &'a [u8], - /// The cursor marking our current position in the buffer. - cursor: usize, - /// A phantom field used to bind the `Buffer` to a specific `ByteOrder`. - _marker: PhantomData, -} - -impl<'a, B: ByteOrder> Buffer<'a, B> { - /// Creates and returns a new `Buffer` with the given data. - /// - /// The cursor is set to the start of the buffer (position 0) upon - /// initialization. - /// - /// # Arguments - /// - /// * `data` - A byte slice that the buffer will read from. - pub const fn new(data: &'a [u8]) -> Self { - Self { - data, - cursor: 0, - _marker: PhantomData, - } - } - - pub const fn current_position(&self) -> usize { self.cursor } - - /// Returns the length of the remaining bytes from the current cursor - /// position. - pub const fn remaining_length(&self) -> usize { self.data.len() - self.cursor } - - /// Returns the length of the buffer data. - pub const fn data_length(&self) -> usize { self.data.len() } - - // TODO: Look into this to make it take ownership of data, not borrowing it - // There are many instances where we transform this to a vector. - /// Returns the remaining bytes that have not been read. - pub fn remaining_bytes(&self) -> &[u8] { &self.data[self.cursor ..] } - - /// Moves the cursor forward or backward by a specified offset. - /// - /// # Arguments - /// - /// * `offset` - The amount to move the cursor. Use a negative value to move - /// backwards. - /// - /// # Errors - /// - /// Returns a `BufferError` if the attempted move would position the cursor - /// out of bounds. - pub fn move_cursor(&mut self, offset: isize) -> GDResult<()> { - // Compute the new cursor position by adding the offset to the current cursor - // position. The checked_add method is used for safe addition, - // preventing overflow and underflow. - let new_cursor = (self.cursor as isize).checked_add(offset); - - match new_cursor { - // If the addition was not successful (i.e., it resulted in an overflow or underflow), - // return an error indicating that the cursor is out of bounds. - None => Err(PacketBad.into()), - - // If the new cursor position is either less than zero (i.e., before the start of the buffer) - // or greater than the remaining length of the buffer (i.e., past the end of the buffer), - // return an error indicating that the cursor is out of bounds. - Some(x) if x < 0 || x as usize > self.data_length() => Err(PacketBad.into()), - - // If the new cursor position is within the bounds of the buffer, update the cursor - // position and return Ok. - Some(x) => { - self.cursor = x as usize; - Ok(()) - } - } - } - - /// Reads a value of type `T` from the buffer, and advances the cursor by - /// the size of `T`. - /// - /// # Type Parameters - /// - /// * `T` - The type of value to be read from the buffer. This type must - /// implement the `BufferRead` trait with the same byte order as the - /// buffer. - /// - /// # Errors - /// - /// Returns a `BufferError` if there is not enough data remaining in the - /// buffer to read a value of type `T`. - pub fn read>(&mut self) -> GDResult { - // Get the size of `T` in bytes. - let size = std::mem::size_of::(); - // Calculate remaining length of the buffer. - let remaining = self.remaining_length(); - - // If the size of `T` is larger than the remaining length, return an error - // because we don't have enough data left to read. - if size > remaining { - return Err(PacketUnderflow.context(format!( - "Size requested {size} was larger than remaining bytes {remaining}" - ))); - } - - // Slice the data array from the current cursor position for `size` amount of - // bytes. - let bytes = &self.data[self.cursor .. self.cursor + size]; - - // Move the cursor forward by `size`. - self.cursor += size; - - // Use the `read_from_buffer` function of the `BufferRead` implementation for - // `T` to convert the bytes into an instance of `T`. - T::read_from_buffer(bytes) - } - - /// Reads a string from the buffer using a specified `StringDecoder`, until - /// an optional delimiter. - /// - /// # Type Parameters - /// - /// * `D` - The type of string decoder to use. This type must implement the - /// `StringDecoder` trait with the same byte order as the buffer. - /// - /// # Arguments - /// - /// * `until` - An optional delimiter. If provided, the method will read - /// until this delimiter is encountered. If not provided, the method will - /// read until the default delimiter of the decoder. - /// - /// # Errors - /// - /// Returns a `BufferError` if there is an error decoding the string. - pub fn read_string(&mut self, until: Option) -> GDResult { - // Check if the cursor is out of bounds. - if self.cursor > self.data_length() { - return Err(PacketUnderflow.context(format!( - "Cursor position {} is out of bounds when reading string. Buffer length: {}", - self.cursor, - self.data_length() - ))); - } - - // Slice the data array from the current cursor position to the end. - let data_slice = &self.data[self.cursor ..]; - - // Use the provided delimiter if one was given, or default to the - // delimiter specified by the StringDecoder. - let delimiter = until.unwrap_or(D::DELIMITER); - - // Invoke the decode_string function of the provided StringDecoder, - // passing in the remaining data slice, the mutable reference to the - // cursor, and the delimiter. - let result = D::decode_string(data_slice, &mut self.cursor, delimiter)?; - - // If decoding was successful, return the decoded string. The cursor - // position has been updated within the decode_string call to reflect - // the new position after reading. - Ok(result) - } -} - -/// A trait that provides an interface to switch endianness. -/// -/// The trait `SwitchEndian` is used for types that have a specific -/// byte order (endianness) and can switch to another byte order. -/// The type of the switched endianness is determined by the associated -/// type `Output`. -/// -/// The associated type `Output` must implement the `ByteOrder` trait. -pub trait SwitchEndian { - type Output: ByteOrder; -} - -/// An implementation of `SwitchEndian` for `LittleEndian`. -/// -/// The switched endianness type is `BigEndian`. -impl SwitchEndian for LittleEndian { - type Output = BigEndian; -} - -/// An implementation of `SwitchEndian` for `BigEndian`. -/// -/// The switched endianness type is `LittleEndian`. -impl SwitchEndian for BigEndian { - type Output = LittleEndian; -} - -impl<'a, B: SwitchEndian + ByteOrder> Buffer<'a, B> { - /// Switches the byte order of a chunk in the buffer. - /// - /// This method consumes the buffer and returns a new buffer - /// with a chunk of the original buffer's data, starting from the - /// original cursor position and of the given size, where the byte - /// order is switched according to the implementation - /// of `SwitchEndian` for `B`. - /// - /// Note: The method also advances the cursor of the original buffer - /// by `size`. - /// - /// # Parameters - /// - /// * `size`: The size of the chunk to be taken from the original buffer. - pub fn switch_endian_chunk(&mut self, size: usize) -> GDResult> { - let old_cursor = self.cursor; - self.move_cursor(size as isize)?; - - Ok(Buffer { - data: &self.data[old_cursor .. old_cursor + size], - cursor: 0, - _marker: PhantomData, - }) - } -} - -/// A trait defining a protocol for reading values of a certain type from a -/// buffer. -/// -/// Implementors of this trait provide a method for reading their type from a -/// byte buffer with a specific byte order. -pub trait BufferRead: Sized { - fn read_from_buffer(data: &[u8]) -> GDResult; -} - -/// Macro to implement the `BufferRead` trait for byte types. -/// -/// This macro generates an implementation of the `BufferRead` trait for a -/// specified byte type. The implementation will read a single byte from the -/// buffer and convert it to the target type using the provided map function. -/// -/// # Arguments -/// -/// * `$type` - The target type to implement `BufferRead` for. -/// * `$map_func` - The function to map a byte to the target type. -macro_rules! impl_buffer_read_byte { - ($type:ty, $map_func:expr) => { - impl BufferRead for $type { - fn read_from_buffer(data: &[u8]) -> GDResult { - // Use the `first` method to get the first byte from the data array. - data.first() - // Apply the $map_func function to convert the raw byte to the $type. - .map($map_func) - // If the data array is empty (and thus `first` returns None), - // `ok_or_else` will return a BufferError. - .ok_or_else(|| PacketBad.into()) - } - } - }; -} - -/// Macro to implement the `BufferRead` trait for multi-byte types. -/// -/// This macro generates an implementation of the `BufferRead` trait for a -/// specified multi-byte type. The implementation will read the appropriate -/// number of bytes from the buffer and convert them to the target type using -/// the provided read function. -/// -/// # Arguments -/// -/// * `$type` - The target type to implement `BufferRead` for. -/// * `$read_func` - The function to read the bytes into the target type. -macro_rules! impl_buffer_read { - ($type:ty, $read_func:ident) => { - impl BufferRead for $type { - fn read_from_buffer(data: &[u8]) -> GDResult { - // Convert the byte slice into an array of the appropriate type. - let array = data.try_into().map_err(|e| { - // If conversion fails, return an error indicating the required and provided - // lengths. - PacketBad.context(e) - })?; - - // Use the provided function to read the data from the array into the given - // type. - Ok(B::$read_func(array)) - } - } - }; -} - -impl_buffer_read_byte!(u8, |&b| b); -impl_buffer_read_byte!(i8, |&b| b as i8); - -impl_buffer_read!(u16, read_u16); -impl_buffer_read!(i16, read_i16); -impl_buffer_read!(u32, read_u32); -impl_buffer_read!(i32, read_i32); -impl_buffer_read!(u64, read_u64); -impl_buffer_read!(i64, read_i64); -impl_buffer_read!(f32, read_f32); -impl_buffer_read!(f64, read_f64); - -/// A trait defining a protocol for decoding strings from a buffer. -/// -/// This trait should be implemented by types that can decode strings from a -/// byte buffer with a specific byte order and delimiter. -pub trait StringDecoder { - /// The type of the delimiter used by the decoder. - type Delimiter: AsRef<[u8]>; - - /// The default delimiter used by the decoder. - const DELIMITER: Self::Delimiter; - - /// Decodes a string from the provided byte slice, and updates the cursor - /// position accordingly. - /// - /// # Arguments - /// - /// * `data` - The byte slice to decode the string from. - /// * `cursor` - The current position in the byte slice. - /// * `delimiter` - The delimiter to use for decoding the string. - /// - /// # Errors - /// - /// Returns a `BufferError` if there is an error decoding the string. - fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult; -} - -/// A decoder for UTF-8 encoded strings. -/// -/// This decoder uses a single null byte (`0x00`) as the default delimiter. -pub struct Utf8Decoder; - -impl StringDecoder for Utf8Decoder { - type Delimiter = [u8; 1]; - - const DELIMITER: Self::Delimiter = [0x00]; - - /// Decodes a UTF-8 string from the given data, updating the cursor position - /// accordingly. - fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult { - // Find the position of the delimiter in the data. If the delimiter is not - // found, the length of the data is returned. - let position = data - // Create an iterator over the data. - .iter() - // Find the position of the delimiter - .position(|&b| b == delimiter.as_ref()[0]) - // If the delimiter is not found, use the whole data slice. - .unwrap_or(data.len()); - - // Convert the data until the found position into a UTF-8 string. - let result = std::str::from_utf8( - // Take a slice of data until the position. - &data[.. position] - ) - // If the data cannot be converted into a UTF-8 string, return an error - .map_err(|e| PacketBad.context(e))? - // Convert the resulting &str into a String - .to_owned(); - - // Update the cursor position - // The +1 is to skip the delimiter - *cursor += position + 1; - - Ok(result) - } -} - -/// A decoder for UTF-8 encoded strings prefixed by a single byte denoting the -/// string's length. -/// -/// This decoder uses a single null byte (`0x00`) as the default delimiter. -pub struct Utf8LengthPrefixedDecoder; - -impl StringDecoder for Utf8LengthPrefixedDecoder { - type Delimiter = [u8; 1]; - - const DELIMITER: Self::Delimiter = [0x00]; - - /// Decodes a UTF-8 string from the given data, updating the cursor position - /// accordingly. - fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult { - // Find the maximum length of the string - let length = *data - .first() - .ok_or_else(|| PacketBad.context("Length of string not found"))?; - - // Find the position of the delimiter in the data. If the delimiter is not - // found, the length is returned. - let position = data - // Create an iterator over the data. - .iter() - .skip(1) - .take(length as usize) - // Find the position of the delimiter - .position(|&b| b == delimiter.as_ref()[0]) - // If the delimiter is not found, use the whole data slice. - .unwrap_or(length as usize); - - // Convert the data until the found position into a UTF-8 string. - let result = std::str::from_utf8( - // Take a slice of data until the position. - &data[1 .. position + 1] - ) - // If the data cannot be converted into a UTF-8 string, return an error - .map_err(|e| PacketBad.context(e))? - // Convert the resulting &str into a String - .to_owned(); - - // Update the cursor position - // The +1 is to skip t length - *cursor += position + 1; - - Ok(result) - } -} - -/// A decoder for UTF-16 encoded strings. -/// -/// This decoder uses a pair of null bytes (`0x00, 0x00`) as the default -/// delimiter. -/// -/// # Type Parameters -/// -/// * `B` - The byte order to use when decoding the string. -pub struct Utf16Decoder { - _marker: PhantomData, -} - -impl StringDecoder for Utf16Decoder { - type Delimiter = [u8; 2]; - - const DELIMITER: Self::Delimiter = [0x00, 0x00]; - - /// Decodes a UTF-16 string from the given data, updating the cursor - /// position accordingly. - fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult { - // Try to find the position of the delimiter in the data - let position = data - // Split the data into 2-byte chunks (as UTF-16 uses 2 bytes per character) - .chunks_exact(2) - // Find the position of the delimiter - .position(|chunk| chunk == delimiter.as_ref()) - // If the delimiter is not found, use the whole data, otherwise use the position of the delimiter - .map_or(data.len(), |pos| pos * 2); - - // Create a buffer of u16 values to hold the decoded characters - let mut paired_buf: Vec = vec![0; position / 2]; - - // Decode the data into the buffer - B::read_u16_into(&data[.. position], &mut paired_buf); - - // Convert the buffer of u16 values into a String - let result = String::from_utf16(&paired_buf).map_err(|e| PacketBad.context(e))?; - - // Update the cursor position - // The +2 accounts for the delimiter - *cursor += position + 2; - - Ok(result) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use byteorder::BigEndian; - - #[test] - fn test_new_buffer() { - let data: &[u8] = &[1, 2, 3, 4]; - let buffer = Buffer::::new(data); - - assert_eq!(buffer.data, data); - assert_eq!(buffer.cursor, 0); - } - - #[test] - fn test_remaining_length() { - let data: &[u8] = &[1, 2, 3, 4]; - let mut buffer = Buffer::::new(data); - - assert_eq!(buffer.remaining_length(), 4); - - buffer.cursor = 2; - assert_eq!(buffer.remaining_length(), 2); - } - - #[test] - fn test_move_cursor() { - let data: &[u8] = &[1, 2, 3, 4]; - let mut buffer = Buffer::::new(data); - - // Test moving forward - assert!(buffer.move_cursor(2).is_ok()); - assert_eq!(buffer.cursor, 2); - - // Test moving backward - assert!(buffer.move_cursor(-1).is_ok()); - assert_eq!(buffer.cursor, 1); - - // Test moving beyond data limits - assert!(buffer.move_cursor(5).is_err()); - assert!(buffer.move_cursor(-2).is_err()); - } - - #[test] - fn test_switch_endian_chunk_le_be() { - let data = [0x01, 0x02, 0x03, 0x04]; - let mut buffer = Buffer::::new(&data[..]); - - let switched_buffer = buffer.switch_endian_chunk(2).unwrap(); - - assert_eq!(switched_buffer.data, [0x01, 0x02]); - assert_eq!(switched_buffer.cursor, 0); - - assert_eq!(buffer.remaining_bytes(), [0x03, 0x04]); - assert_eq!(buffer.cursor, 2); - } - - #[test] - fn test_switch_endian_chunk_be_le() { - let data = [0x01, 0x02, 0x03, 0x04]; - let mut buffer = Buffer::::new(&data[..]); - - let switched_buffer = buffer.switch_endian_chunk(2).unwrap(); - - assert_eq!(switched_buffer.data, [0x01, 0x02]); - assert_eq!(switched_buffer.cursor, 0); - - assert_eq!(buffer.remaining_bytes(), [0x03, 0x04]); - assert_eq!(buffer.cursor, 2); - } - - #[test] - fn test_buffer_read_u8() { - let data: &[u8] = &[1, 2, 3, 4]; - let mut buffer = Buffer::::new(data); - - let result: Result = buffer.read(); - assert_eq!(result.unwrap(), 1); - assert_eq!(buffer.cursor, 1); - } - - #[test] - fn test_buffer_read_u16() { - let data: &[u8] = &[1, 2, 3, 4]; - let mut buffer = Buffer::::new(data); - - let result: Result = buffer.read(); - assert_eq!(result.unwrap(), 0x0201); - assert_eq!(buffer.cursor, 2); - } - - #[test] - fn test_buffer_read_u16_big_endian() { - let data: &[u8] = &[1, 2, 3, 4]; - let mut buffer = Buffer::::new(data); - - let result: Result = buffer.read(); - assert_eq!(result.unwrap(), 0x0102); - assert_eq!(buffer.cursor, 2); - } - - #[test] - fn test_decode_string_utf8() { - let data: &[u8] = b"Hello\0World\0"; - let mut cursor = 0; - let delimiter = [0x00]; - - let result = Utf8Decoder::decode_string(data, &mut cursor, delimiter); - assert_eq!(result.unwrap(), "Hello"); - assert_eq!(cursor, 6); - } - - #[test] - fn test_decode_string_utf16_le() { - let data: &[u8] = &[0x48, 0x00, 0x65, 0x00, 0x00, 0x00]; - let mut cursor = 0; - let delimiter = [0x00, 0x00]; - - let result = Utf16Decoder::::decode_string(data, &mut cursor, delimiter); - assert_eq!(result.unwrap(), "He"); - assert_eq!(cursor, 6); - } - - #[test] - fn test_decode_string_utf16_be() { - let data: &[u8] = &[0x00, 0x48, 0x00, 0x65, 0x00, 0x00]; - let mut cursor = 0; - let delimiter = [0x00, 0x00]; - - let result = Utf16Decoder::::decode_string(data, &mut cursor, delimiter); - assert_eq!(result.unwrap(), "He"); - assert_eq!(cursor, 6); - } - - #[test] - fn test_buffer_underflow_error() { - let data: &[u8] = &[1, 2]; - let mut buffer = Buffer::::new(data); - - let result: Result = buffer.read(); - assert_eq!( - result.unwrap_err(), - crate::GDErrorKind::PacketUnderflow.into() - ); - } -} diff --git a/crates/lib/src/capture/mod.rs b/crates/lib/src/capture/mod.rs deleted file mode 100644 index 583ebd8..0000000 --- a/crates/lib/src/capture/mod.rs +++ /dev/null @@ -1,39 +0,0 @@ -pub(crate) mod packet; -mod pcap; -pub(crate) mod socket; -pub(crate) mod writer; - -use self::{pcap::Pcap, writer::Writer}; -use pcap_file::pcapng::{blocks::interface_description::InterfaceDescriptionBlock, PcapNgBlock, PcapNgWriter}; -use std::path::PathBuf; - -pub fn setup_capture(file_path: Option) { - if let Some(file_path) = file_path { - let file = std::fs::OpenOptions::new() - .create_new(true) - .write(true) - .open(file_path.with_extension("pcap")) - .unwrap(); - - let mut pcap_writer = PcapNgWriter::new(file).unwrap(); - - // Write headers - let _ = pcap_writer.write_block( - &InterfaceDescriptionBlock { - linktype: pcap_file::DataLink::ETHERNET, - snaplen: 0xFFFF, - options: vec![], - } - .into_block(), - ); - - let writer = Box::new(Pcap::new(pcap_writer)); - attach(writer) - } -} - -/// Attaches a writer to the capture module. -/// -/// # Errors -/// Returns an Error if the writer is already set. -fn attach(writer: Box) { crate::capture::socket::set_writer(writer); } diff --git a/crates/lib/src/capture/packet.rs b/crates/lib/src/capture/packet.rs deleted file mode 100644 index ef3ae62..0000000 --- a/crates/lib/src/capture/packet.rs +++ /dev/null @@ -1,203 +0,0 @@ -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; - -/// Size of a standard network packet. -pub(crate) const PACKET_SIZE: usize = 5012; -/// Size of an Ethernet header. -pub(crate) const HEADER_SIZE_ETHERNET: usize = 14; -/// Size of an IPv4 header. -pub(crate) const HEADER_SIZE_IP4: usize = 20; -/// Size of an IPv6 header. -pub(crate) const HEADER_SIZE_IP6: usize = 40; -/// Size of a UDP header. -pub(crate) const HEADER_SIZE_UDP: usize = 4; - -/// Represents the direction of a network packet. -#[derive(Clone, Copy, Debug, PartialEq)] -pub(crate) enum Direction { - /// Packet is outgoing (sent by us). - Send, - /// Packet is incoming (received by us). - Receive, -} - -/// Defines the protocol of a network packet. -#[derive(Clone, Copy, Debug, PartialEq)] -pub(crate) enum Protocol { - /// Transmission Control Protocol. - Tcp, - /// User Datagram Protocol. - Udp, -} - -/// Trait for handling different types of IP addresses (IPv4, IPv6). -pub(crate) trait IpAddress: Sized { - /// Creates an instance from a standard `IpAddr`, returning `None` if the - /// types are incompatible. - fn from_std(ip: IpAddr) -> Option; -} - -/// Represents a captured network packet with metadata. -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct CapturePacket<'a> { - /// Direction of the packet (Send/Receive). - pub(crate) direction: Direction, - /// Protocol of the packet (Tcp/UDP). - pub(crate) protocol: Protocol, - /// Remote socket address. - pub(crate) remote_address: &'a SocketAddr, - /// Local socket address. - pub(crate) local_address: &'a SocketAddr, -} - -impl CapturePacket<'_> { - /// Retrieves the local and remote ports based on the packet's direction. - /// - /// Returns: - /// - (u16, u16): Tuple of (source port, destination port). - pub(super) fn ports_by_direction(&self) -> (u16, u16) { - let (local, remote) = (self.local_address.port(), self.remote_address.port()); - self.direction.order(local, remote) - } - - /// Retrieves the local and remote IP addresses. - /// - /// Returns: - /// - (IpAddr, IpAddr): Tuple of (local IP, remote IP). - pub(super) fn ip_addr(&self) -> (IpAddr, IpAddr) { - let (local, remote) = (self.local_address.ip(), self.remote_address.ip()); - (local, remote) - } - - /// Retrieves IP addresses of a specific type (IPv4 or IPv6) based on the - /// packet's direction. - /// - /// Panics if the IP type of the addresses does not match the requested - /// type. - /// - /// Returns: - /// - (T, T): Tuple of (source IP, destination IP) of the specified type in - /// order. - pub(super) fn ipvt_by_direction(&self) -> (T, T) { - let (local, remote) = ( - T::from_std(self.local_address.ip()).expect("Incorrect IP type for local address"), - T::from_std(self.remote_address.ip()).expect("Incorrect IP type for remote address"), - ); - - self.direction.order(local, remote) - } -} - -impl Direction { - /// Orders two elements (source and destination) based on the packet's - /// direction. - /// - /// Returns: - /// - (T, T): Ordered tuple (source, destination). - pub(self) const fn order(&self, source: T, remote: T) -> (T, T) { - match self { - Direction::Send => (source, remote), - Direction::Receive => (remote, source), - } - } -} - -/// Implements the `IpAddress` trait for `Ipv4Addr`. -impl IpAddress for Ipv4Addr { - /// Creates an `Ipv4Addr` from a standard `IpAddr`, if it's IPv4. - fn from_std(ip: IpAddr) -> Option { - match ip { - IpAddr::V4(ipv4) => Some(ipv4), - _ => None, - } - } -} - -/// Implements the `IpAddress` trait for `Ipv6Addr`. -impl IpAddress for Ipv6Addr { - /// Creates an `Ipv6Addr` from a standard `IpAddr`, if it's IPv6. - fn from_std(ip: IpAddr) -> Option { - match ip { - IpAddr::V6(ipv6) => Some(ipv6), - _ => None, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::str::FromStr; - - // Helper function to create a SocketAddr from a string - fn socket_addr(addr: &str) -> SocketAddr { SocketAddr::from_str(addr).unwrap() } - - #[test] - fn test_ports_by_direction() { - let packet_send = CapturePacket { - direction: Direction::Send, - protocol: Protocol::Tcp, - local_address: &socket_addr("127.0.0.1:8080"), - remote_address: &socket_addr("192.168.1.1:80"), - }; - - let packet_receive = CapturePacket { - direction: Direction::Receive, - protocol: Protocol::Tcp, - local_address: &socket_addr("127.0.0.1:8080"), - remote_address: &socket_addr("192.168.1.1:80"), - }; - - assert_eq!(packet_send.ports_by_direction(), (8080, 80)); - assert_eq!(packet_receive.ports_by_direction(), (80, 8080)); - } - - #[test] - fn test_ip_addr() { - let packet_send = CapturePacket { - direction: Direction::Send, - protocol: Protocol::Tcp, - local_address: &socket_addr("127.0.0.1:8080"), - remote_address: &socket_addr("192.168.1.1:80"), - }; - - let packet_receive = CapturePacket { - direction: Direction::Receive, - protocol: Protocol::Tcp, - local_address: &socket_addr("127.0.0.1:8080"), - remote_address: &socket_addr("192.168.1.1:80"), - }; - - assert_eq!( - packet_send.ip_addr(), - ( - IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), - IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)) - ) - ); - assert_eq!( - packet_receive.ip_addr(), - ( - IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), - IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)) - ) - ); - } - - #[test] - fn test_ip_by_direction_type_specific() { - let packet = CapturePacket { - direction: Direction::Send, - protocol: Protocol::Tcp, - local_address: &socket_addr("127.0.0.1:8080"), - remote_address: &socket_addr("192.168.1.1:80"), - }; - - let ipv4_result: Result<(Ipv4Addr, Ipv4Addr), _> = - std::panic::catch_unwind(|| packet.ipvt_by_direction::()); - assert!(ipv4_result.is_ok()); - - let ipv6_result: Result<(Ipv6Addr, Ipv6Addr), _> = - std::panic::catch_unwind(|| packet.ipvt_by_direction::()); - assert!(ipv6_result.is_err()); - } -} diff --git a/crates/lib/src/capture/pcap.rs b/crates/lib/src/capture/pcap.rs deleted file mode 100644 index acb653f..0000000 --- a/crates/lib/src/capture/pcap.rs +++ /dev/null @@ -1,383 +0,0 @@ -use pcap_file::pcapng::{blocks::enhanced_packet::EnhancedPacketOption, PcapNgBlock, PcapNgWriter}; -use pnet_packet::{ - ethernet::{EtherType, MutableEthernetPacket}, - ip::{IpNextHeaderProtocol, IpNextHeaderProtocols}, - ipv4::MutableIpv4Packet, - ipv6::MutableIpv6Packet, - tcp::{MutableTcpPacket, TcpFlags}, - udp::MutableUdpPacket, - PacketSize, -}; -use std::{io::Write, net::IpAddr, time::Instant}; - -use super::packet::{ - CapturePacket, - Direction, - Protocol, - HEADER_SIZE_ETHERNET, - HEADER_SIZE_IP4, - HEADER_SIZE_IP6, - HEADER_SIZE_UDP, - PACKET_SIZE, -}; - -const BUFFER_SIZE: usize = PACKET_SIZE - HEADER_SIZE_IP6 - HEADER_SIZE_ETHERNET; - -pub(crate) struct Pcap { - writer: PcapNgWriter, - pub(crate) state: State, -} - -pub(crate) struct State { - pub(crate) start_time: Instant, - pub(crate) send_seq: u32, - pub(crate) rec_seq: u32, - pub(crate) has_sent_handshake: bool, - pub(crate) stream_count: u32, -} - -impl Pcap { - pub(crate) fn new(writer: PcapNgWriter) -> Self { - Self { - writer, - state: State::default(), - } - } - - pub(crate) fn write_transport_packet(&mut self, info: &CapturePacket, payload: &[u8]) { - let mut buffer_array: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; - let buf: &mut [u8] = &mut buffer_array[..]; - - let (source_port, dest_port) = info.ports_by_direction(); - - match info.protocol { - Protocol::Tcp => { - let buf_size = { - let mut tcp = MutableTcpPacket::new(buf).unwrap(); - tcp.set_source(source_port); - tcp.set_destination(dest_port); - tcp.set_payload(payload); - tcp.set_data_offset(5); - tcp.set_window(43440); - match info.direction { - Direction::Send => { - tcp.set_sequence(self.state.send_seq); - tcp.set_acknowledgement(self.state.rec_seq); - - self.state.send_seq = self.state.send_seq.wrapping_add(payload.len() as u32); - } - Direction::Receive => { - tcp.set_sequence(self.state.rec_seq); - tcp.set_acknowledgement(self.state.send_seq); - - self.state.rec_seq = self.state.rec_seq.wrapping_add(payload.len() as u32); - } - } - tcp.set_flags(TcpFlags::PSH | TcpFlags::ACK); - - tcp.packet_size() - }; - - self.write_transport_payload( - info, - IpNextHeaderProtocols::Tcp, - &buf[.. buf_size + payload.len()], - vec![], - ); - - let mut info = info.clone(); - let buf_size = { - let mut tcp = MutableTcpPacket::new(buf).unwrap(); - tcp.set_source(dest_port); - tcp.set_destination(source_port); - tcp.set_data_offset(5); - tcp.set_window(43440); - match &info.direction { - Direction::Send => { - tcp.set_sequence(self.state.rec_seq); - tcp.set_acknowledgement(self.state.send_seq); - - info.direction = Direction::Receive; - } - Direction::Receive => { - tcp.set_sequence(self.state.send_seq); - tcp.set_acknowledgement(self.state.rec_seq); - - info.direction = Direction::Send; - } - } - tcp.set_flags(TcpFlags::ACK); - - tcp.packet_size() - }; - - self.write_transport_payload( - &info, - IpNextHeaderProtocols::Tcp, - &buf[.. buf_size], - vec![EnhancedPacketOption::Comment("Generated TCP ACK".into())], - ); - } - Protocol::Udp => { - let buf_size = { - let mut udp = MutableUdpPacket::new(buf).unwrap(); - udp.set_source(source_port); - udp.set_destination(dest_port); - udp.set_length((payload.len() + HEADER_SIZE_UDP) as u16); - udp.set_payload(payload); - - udp.packet_size() - }; - - self.write_transport_payload( - info, - IpNextHeaderProtocols::Udp, - &buf[.. buf_size + payload.len()], - vec![], - ); - } - } - } - - /// Encode a network layer (IP) packet with a payload. - fn encode_ip_packet( - &self, - buf: &mut [u8], - info: &CapturePacket, - protocol: IpNextHeaderProtocol, - payload: &[u8], - ) -> (usize, EtherType) { - match info.ip_addr() { - (IpAddr::V4(_), IpAddr::V4(_)) => { - let (source, destination) = info.ipvt_by_direction(); - - let header_size = HEADER_SIZE_IP4 + (32 / 8); - - let mut ip = MutableIpv4Packet::new(buf).unwrap(); - ip.set_version(4); - ip.set_total_length((payload.len() + header_size) as u16); - ip.set_next_level_protocol(protocol); - // https://en.wikipedia.org/wiki/Internet_Protocol_version_4#Total_Length - - ip.set_header_length((header_size / 4) as u8); - ip.set_source(source); - ip.set_destination(destination); - ip.set_payload(payload); - ip.set_ttl(64); - ip.set_flags(pnet_packet::ipv4::Ipv4Flags::DontFragment); - - let mut options_writer = - pnet_packet::ipv4::MutableIpv4OptionPacket::new(ip.get_options_raw_mut()).unwrap(); - options_writer.set_copied(1); - options_writer.set_class(0); - options_writer.set_number(pnet_packet::ipv4::Ipv4OptionNumbers::SID); - options_writer.set_length(&[4]); - options_writer.set_data(&(self.state.stream_count as u16).to_be_bytes()); - - ip.set_checksum(pnet_packet::ipv4::checksum(&ip.to_immutable())); - - (ip.packet_size(), pnet_packet::ethernet::EtherTypes::Ipv4) - } - (IpAddr::V6(_), IpAddr::V6(_)) => { - let (source, destination) = info.ipvt_by_direction(); - - let mut ip = MutableIpv6Packet::new(buf).unwrap(); - ip.set_version(6); - ip.set_payload_length(payload.len() as u16); - ip.set_next_header(protocol); - ip.set_source(source); - ip.set_destination(destination); - ip.set_hop_limit(64); - ip.set_payload(payload); - ip.set_flow_label(self.state.stream_count); - - (ip.packet_size(), pnet_packet::ethernet::EtherTypes::Ipv6) - } - _ => unreachable!(), - } - } - - /// Encode a physical layer (ethernet) packet with a payload. - fn encode_ethernet_packet( - &self, - buf: &mut [u8], - ethertype: pnet_packet::ethernet::EtherType, - payload: &[u8], - ) -> usize { - let mut ethernet = MutableEthernetPacket::new(buf).unwrap(); - ethernet.set_ethertype(ethertype); - ethernet.set_payload(payload); - - ethernet.packet_size() - } - - /// Write a TCP handshake. - pub(crate) fn write_tcp_handshake(&mut self, info: &CapturePacket) { - let (source_port, dest_port) = (info.local_address.port(), info.remote_address.port()); - - let mut info = info.clone(); - info.direction = Direction::Send; - let mut buffer_array: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; - let buf: &mut [u8] = &mut buffer_array[..]; - // Add a generated comment to all packets - let options = vec![ - pcap_file::pcapng::blocks::enhanced_packet::EnhancedPacketOption::Comment("Generated TCP handshake".into()), - ]; - - // SYN - let buf_size = { - let mut tcp = MutableTcpPacket::new(buf).unwrap(); - self.state.send_seq = 500; - tcp.set_sequence(self.state.send_seq); - tcp.set_flags(TcpFlags::SYN); - tcp.set_source(source_port); - tcp.set_destination(dest_port); - tcp.set_window(43440); - tcp.set_data_offset(5); - - tcp.packet_size() - }; - self.write_transport_payload( - &info, - IpNextHeaderProtocols::Tcp, - &buf[.. buf_size], - options.clone(), - ); - - // SYN + ACK - info.direction = Direction::Receive; - let buf_size = { - let mut tcp = MutableTcpPacket::new(buf).unwrap(); - self.state.send_seq = self.state.send_seq.wrapping_add(1); - tcp.set_acknowledgement(self.state.send_seq); - self.state.rec_seq = 1000; - tcp.set_sequence(self.state.rec_seq); - tcp.set_flags(TcpFlags::SYN | TcpFlags::ACK); - tcp.set_source(dest_port); - tcp.set_destination(source_port); - tcp.set_window(43440); - tcp.set_data_offset(5); - - tcp.packet_size() - }; - self.write_transport_payload( - &info, - IpNextHeaderProtocols::Tcp, - &buf[.. buf_size], - options.clone(), - ); - - // ACK - info.direction = Direction::Send; - let buf_size = { - let mut tcp = MutableTcpPacket::new(buf).unwrap(); - tcp.set_sequence(self.state.send_seq); - self.state.rec_seq = self.state.rec_seq.wrapping_add(1); - tcp.set_acknowledgement(self.state.rec_seq); - tcp.set_flags(TcpFlags::ACK); - tcp.set_source(source_port); - tcp.set_destination(dest_port); - tcp.set_window(43440); - tcp.set_data_offset(5); - - tcp.packet_size() - }; - self.write_transport_payload( - &info, - IpNextHeaderProtocols::Tcp, - &buf[.. buf_size], - options, - ); - - self.state.has_sent_handshake = true; - } - - pub(crate) fn send_tcp_fin(&mut self, info: &CapturePacket) { - let mut buffer_array: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; - let buf: &mut [u8] = &mut buffer_array[..]; - let (source_port, dest_port) = info.ports_by_direction(); - - let buf_size = { - let mut tcp = MutableTcpPacket::new(buf).unwrap(); - tcp.set_source(source_port); - tcp.set_destination(dest_port); - tcp.set_data_offset(5); - tcp.set_window(43440); - - match info.direction { - Direction::Send => { - tcp.set_sequence(self.state.send_seq); - tcp.set_acknowledgement(self.state.rec_seq); - } - Direction::Receive => { - tcp.set_sequence(self.state.rec_seq); - tcp.set_acknowledgement(self.state.send_seq); - } - } - - tcp.set_flags(TcpFlags::FIN | TcpFlags::ACK); - tcp.packet_size() - }; - - self.write_transport_payload( - info, - IpNextHeaderProtocols::Tcp, - &buf[.. buf_size], - vec![EnhancedPacketOption::Comment("Generated TCP FIN".into())], - ); - - // Update sequence number - match info.direction { - Direction::Send => { - self.state.send_seq = self.state.send_seq.wrapping_add(1); - } - Direction::Receive => { - self.state.rec_seq = self.state.rec_seq.wrapping_add(1); - } - } - } - - fn write_transport_payload( - &mut self, - info: &CapturePacket, - protocol: IpNextHeaderProtocol, - payload: &[u8], - options: Vec, - ) { - let mut network_packet = vec![0; PACKET_SIZE - HEADER_SIZE_ETHERNET]; - let (network_size, ethertype) = self.encode_ip_packet(&mut network_packet, info, protocol, payload); - let network_size = network_size + payload.len(); - network_packet.truncate(network_size); - - let mut physical_packet = vec![0; PACKET_SIZE]; - let physical_size = - self.encode_ethernet_packet(&mut physical_packet, ethertype, &network_packet) + network_size; - - physical_packet.truncate(physical_size); - - self.writer - .write_block( - &pcap_file::pcapng::blocks::enhanced_packet::EnhancedPacketBlock { - original_len: physical_size as u32, - data: physical_packet.into(), - interface_id: 0, - timestamp: self.state.start_time.elapsed(), - options, - } - .into_block(), - ) - .unwrap(); - } -} - -impl Default for State { - fn default() -> Self { - Self { - start_time: Instant::now(), - send_seq: 0, - rec_seq: 0, - has_sent_handshake: false, - stream_count: 0, - } - } -} diff --git a/crates/lib/src/capture/socket.rs b/crates/lib/src/capture/socket.rs deleted file mode 100644 index 546e9b8..0000000 --- a/crates/lib/src/capture/socket.rs +++ /dev/null @@ -1,214 +0,0 @@ -use std::{marker::PhantomData, net::SocketAddr}; - -use crate::{ - capture::{ - packet::CapturePacket, - packet::{Direction, Protocol}, - writer::{Writer, CAPTURE_WRITER}, - }, - protocols::types::TimeoutSettings, - socket::{Socket, TcpSocketImpl, UdpSocketImpl}, - GDResult, -}; - -/// Sets a global capture writer for handling all packet data. -/// -/// # Panics -/// Panics if a capture writer is already set. -/// -/// # Arguments -/// * `writer` - A boxed writer that implements the `Writer` trait. -pub(crate) fn set_writer(writer: Box) { - let mut lock = CAPTURE_WRITER.lock().unwrap(); - - if lock.is_some() { - panic!("Capture writer already set"); - } - - *lock = Some(writer); -} - -/// A trait representing a provider of a network protocol. -pub(crate) trait ProtocolProvider { - /// Returns the protocol used by the provider. - fn protocol() -> Protocol; -} - -/// Represents the TCP protocol provider. -pub(crate) struct ProtocolTCP; -impl ProtocolProvider for ProtocolTCP { - fn protocol() -> Protocol { Protocol::Tcp } -} - -/// Represents the UDP protocol provider. -pub(crate) struct ProtocolUDP; -impl ProtocolProvider for ProtocolUDP { - fn protocol() -> Protocol { Protocol::Udp } -} - -/// A socket wrapper that allows capturing packets. -/// -/// # Type parameters -/// * `I` - The inner socket type. -/// * `P` - The protocol provider. -#[derive(Clone, Debug)] -pub(crate) struct WrappedCaptureSocket { - inner: I, - remote_address: SocketAddr, - _protocol: PhantomData

, -} - -impl Socket for WrappedCaptureSocket { - /// Creates a new wrapped socket for capturing packets. - /// - /// Initializes a new socket of type `I`, wrapping it to enable packet - /// capturing. Capturing is protocol-specific, as indicated by - /// the `ProtocolProvider`. - /// - /// # Arguments - /// * `address` - The address to connect the socket to. - /// * `timeout_settings` - Optional timeout settings for the socket. - /// - /// # Returns - /// A `GDResult` containing either the wrapped socket or an error. - fn new(address: &SocketAddr, timeout_settings: &Option) -> GDResult - where Self: Sized { - let v = Self { - inner: I::new(address, timeout_settings)?, - remote_address: *address, - _protocol: PhantomData, - }; - - let info = CapturePacket { - direction: Direction::Send, - protocol: P::protocol(), - remote_address: address, - local_address: &v.local_addr().unwrap(), - }; - - if let Some(writer) = CAPTURE_WRITER.lock().unwrap().as_mut() { - writer.new_connect(&info)?; - } - - Ok(v) - } - - /// Sends data over the socket and captures the packet. - /// - /// The method sends data using the inner socket and captures the sent - /// packet if a capture writer is set. - /// - /// # Arguments - /// * `data` - Data to be sent. - /// - /// # Returns - /// A result indicating success or error in sending data. - fn send(&mut self, data: &[u8]) -> GDResult<()> { - let info = CapturePacket { - direction: Direction::Send, - protocol: P::protocol(), - remote_address: &self.remote_address, - local_address: &self.local_addr().unwrap(), - }; - - if let Some(writer) = CAPTURE_WRITER.lock().unwrap().as_mut() { - writer.write(&info, data)?; - } - - self.inner.send(data) - } - - /// Receives data from the socket and captures the packet. - /// - /// The method receives data using the inner socket and captures the - /// incoming packet if a capture writer is set. - /// - /// # Arguments - /// * `size` - Optional size of data to receive. - /// - /// # Returns - /// A result containing received data or an error. - fn receive(&mut self, size: Option) -> crate::GDResult> { - let data = self.inner.receive(size)?; - let info = CapturePacket { - direction: Direction::Receive, - protocol: P::protocol(), - remote_address: &self.remote_address, - local_address: &self.local_addr().unwrap(), - }; - - if let Some(writer) = CAPTURE_WRITER.lock().unwrap().as_mut() { - writer.write(&info, &data)?; - } - - Ok(data) - } - - /// Applies timeout settings to the wrapped socket. - /// - /// Delegates the operation to the inner socket implementation. - /// - /// # Arguments - /// * `timeout_settings` - Optional timeout settings to apply. - /// - /// # Returns - /// A result indicating success or error in applying timeouts. - fn apply_timeout( - &self, - timeout_settings: &Option, - ) -> crate::GDResult<()> { - self.inner.apply_timeout(timeout_settings) - } - - /// Returns the remote port of the wrapped socket. - /// - /// Delegates the operation to the inner socket implementation. - /// - /// # Returns - /// The remote port number. - fn port(&self) -> u16 { self.inner.port() } - - /// Returns the local SocketAddr of the wrapped socket. - /// - /// Delegates the operation to the inner socket implementation. - /// - /// # Returns - /// The local SocketAddr. - fn local_addr(&self) -> std::io::Result { self.inner.local_addr() } -} - -// this seems a bad way to do this, but its safe -impl Drop for WrappedCaptureSocket { - fn drop(&mut self) { - // Construct the CapturePacket info - let info = CapturePacket { - direction: Direction::Send, - protocol: P::protocol(), - remote_address: &self.remote_address, - local_address: &self - .local_addr() - .unwrap_or_else(|_| SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), 0)), - }; - - // If a capture writer is set, close the connection and capture the packet. - if let Some(writer) = CAPTURE_WRITER.lock().unwrap().as_mut() { - let _ = writer.close_connection(&info); - } - } -} - -/// A specialized `WrappedCaptureSocket` for UDP, using `UdpSocketImpl` as -/// the inner socket and `ProtocolUDP` as the protocol provider. -/// -/// This type captures and processes UDP packets, wrapping around standard -/// UDP socket functionalities with additional packet capture -/// capabilities. -pub(crate) type CapturedUdpSocket = WrappedCaptureSocket; - -/// A specialized `WrappedCaptureSocket` for TCP, using `TcpSocketImpl` as -/// the inner socket and `ProtocolTCP` as the protocol provider. -/// -/// This type captures and processes TCP packets, wrapping around standard -/// TCP socket functionalities with additional packet capture -/// capabilities. -pub(crate) type CapturedTcpSocket = WrappedCaptureSocket; diff --git a/crates/lib/src/capture/writer.rs b/crates/lib/src/capture/writer.rs deleted file mode 100644 index d3a9db0..0000000 --- a/crates/lib/src/capture/writer.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::{io::Write, sync::Mutex}; - -use super::{ - packet::{CapturePacket, Protocol}, - pcap::Pcap, -}; -use crate::GDResult; -use lazy_static::lazy_static; - -lazy_static! { - /// A globally accessible, lazily-initialized static writer instance. - /// This writer is intended for capturing and recording network packets. - /// The writer is wrapped in a Mutex to ensure thread-safe access and modification. - pub(crate) static ref CAPTURE_WRITER: Mutex>> = Mutex::new(None); -} - -/// Trait defining the functionality for a writer that handles network packet -/// captures. This trait includes methods for writing packet data, handling new -/// connections, and closing connections. -pub(crate) trait Writer { - /// Writes a given packet's data to an underlying storage or stream. - /// - /// # Arguments - /// * `packet` - Reference to the packet being captured. - /// * `data` - The raw byte data associated with the packet. - /// - /// # Returns - /// A `GDResult` indicating the success or failure of the write operation. - fn write(&mut self, packet: &CapturePacket, data: &[u8]) -> GDResult<()>; - - /// Handles the creation of a new connection, potentially logging or - /// initializing resources. - /// - /// # Arguments - /// * `packet` - Reference to the packet indicating a new connection. - /// - /// # Returns - /// A `GDResult` indicating the success or failure of handling the new - /// connection. - fn new_connect(&mut self, packet: &CapturePacket) -> GDResult<()>; - - /// Closes a connection, handling any necessary cleanup or finalization. - /// - /// # Arguments - /// * `packet` - Reference to the packet indicating the closure of a - /// connection. - /// - /// # Returns - /// A `GDResult` indicating the success or failure of the connection closure - /// operation. - fn close_connection(&mut self, packet: &CapturePacket) -> GDResult<()>; -} - -/// Implementation of the `Writer` trait for the `Pcap` struct. -/// This implementation enables writing, connection handling, and closure -/// specific to PCAP (Packet Capture) format. -impl Writer for Pcap { - fn write(&mut self, info: &CapturePacket, data: &[u8]) -> GDResult<()> { - self.write_transport_packet(info, data); - - Ok(()) - } - - fn new_connect(&mut self, packet: &CapturePacket) -> GDResult<()> { - match packet.protocol { - Protocol::Tcp => { - self.write_tcp_handshake(packet); - } - Protocol::Udp => {} - } - - self.state.stream_count = self.state.stream_count.wrapping_add(1); - - Ok(()) - } - - fn close_connection(&mut self, packet: &CapturePacket) -> GDResult<()> { - match packet.protocol { - Protocol::Tcp => { - self.send_tcp_fin(packet); - } - Protocol::Udp => {} - } - Ok(()) - } -} diff --git a/crates/lib/src/errors/error.rs b/crates/lib/src/errors/error.rs deleted file mode 100644 index 582bbac..0000000 --- a/crates/lib/src/errors/error.rs +++ /dev/null @@ -1,143 +0,0 @@ -use crate::GDErrorKind; -use std::error::Error; -use std::fmt::Formatter; -use std::{backtrace, fmt}; - -pub(crate) type ErrorSource = Box; - -/// The GameDig error type. -/// -/// Can be created in three ways (all of which will implicitly generate a -/// backtrace): -/// -/// Directly from an [error kind](GDErrorKind) (without a -/// source). -/// -/// ``` -/// use gamedig::{GDError, GDErrorKind}; -/// let _: GDError = GDErrorKind::PacketBad.into(); -/// ``` -/// -/// [From an error kind with a source](GDErrorKind::context) (any -/// type that implements `Into>`). -/// -/// ``` -/// use gamedig::{GDError, GDErrorKind}; -/// let _: GDError = GDErrorKind::PacketBad.context("Reason the packet was bad"); -/// ``` -/// -/// Using the [new helper](GDError::new). -/// -/// ``` -/// use gamedig::{GDError, GDErrorKind}; -/// let _: GDError = GDError::new(GDErrorKind::PacketBad, Some("Reason the packet was bad".into())); -/// ``` -pub struct GDError { - pub kind: GDErrorKind, - pub source: Option, - pub backtrace: Option, -} - -impl From for GDError { - fn from(value: GDErrorKind) -> Self { - let backtrace = Some(backtrace::Backtrace::capture()); - Self { - kind: value, - source: None, - backtrace, - } - } -} - -impl PartialEq for GDError { - fn eq(&self, other: &Self) -> bool { self.kind == other.kind } -} - -impl Error for GDError { - fn source(&self) -> Option<&(dyn Error + 'static)> { self.source.as_ref().map(|err| Box::as_ref(err) as _) } -} - -impl fmt::Debug for GDError { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - writeln!(f, "GDError{{ kind={:?}", self.kind)?; - if let Some(source) = &self.source { - writeln!(f, " source={source:?}")?; - } - if let Some(backtrace) = &self.backtrace { - let bt = format!("{backtrace:#?}"); - writeln!(f, " backtrace={}", bt.replace('\n', "\n "))?; - } - writeln!(f, "}}")?; - Ok(()) - } -} - -impl fmt::Display for GDError { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{self:?}") } -} - -impl GDError { - /// Create a new error (with automatic backtrace) - pub fn new(kind: GDErrorKind, source: Option) -> Self { - let backtrace = Some(backtrace::Backtrace::capture()); - Self { - kind, - source, - backtrace, - } - } - - /// Create a new error using any type that can be converted to an error - pub fn from_error>(kind: GDErrorKind, source: E) -> Self { - Self::new(kind, Some(source.into())) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // test error trait GDError - #[test] - fn test_error_trait() { - let source: Result = "nan".parse(); - let source_err = source.unwrap_err(); - - let error_with_context = GDErrorKind::TypeParse.context(source_err.clone()); - assert!(error_with_context.source().is_some()); - assert_eq!( - format!("{}", error_with_context.source().unwrap()), - format!("{source_err}") - ); - - let error_without_context: GDError = GDErrorKind::TypeParse.into(); - assert!(error_without_context.source().is_none()); - } - - // Test creating GDError with GDError::new - #[test] - fn test_create_new() { - let error_from_new = GDError::new(GDErrorKind::InvalidInput, None); - assert!(error_from_new.backtrace.is_some()); - assert_eq!(error_from_new.kind, GDErrorKind::InvalidInput); - assert!(error_from_new.source.is_none()); - } - - // Test creating GDError with GDErrorKind::context - #[test] - fn test_create_context() { - let error_from_context = GDErrorKind::InvalidInput.context("test"); - assert!(error_from_context.backtrace.is_some()); - assert_eq!(error_from_context.kind, GDErrorKind::InvalidInput); - assert!(error_from_context.source.is_some()); - } - - // Test creating GDError with From for GDError - #[test] - fn test_create_into() { - let error_from_into: GDError = GDErrorKind::InvalidInput.into(); - assert!(error_from_into.backtrace.is_some()); - assert_eq!(error_from_into.kind, GDErrorKind::InvalidInput); - assert!(error_from_into.source.is_none()); - } -} diff --git a/crates/lib/src/errors/kind.rs b/crates/lib/src/errors/kind.rs deleted file mode 100644 index e7eb3d7..0000000 --- a/crates/lib/src/errors/kind.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::error::ErrorSource; -use crate::GDError; - -/// All GameDig Error kinds. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum GDErrorKind { - /// The received packet was bigger than the buffer size. - PacketOverflow, - /// The received packet was shorter than the expected one. - PacketUnderflow, - /// The received packet is badly formatted. - PacketBad, - /// Couldn't send the packet. - PacketSend, - /// Couldn't receieve data when it was expected. - PacketReceive, - /// Couldn't decompress data. - Decompress, - /// Couldn't create a socket connection. - SocketConnect, - /// Couldn't bind a socket. - SocketBind, - /// Invalid input to the library. - InvalidInput, - /// The server response indicated that it is a different game than the game - /// queried. - BadGame, - /// Couldn't automatically query (none of the attempted protocols were - /// successful). - AutoQuery, - /// A protocol-defined expected format was not met. - ProtocolFormat, - /// Couldn't cast a value to an enum. - UnknownEnumCast, - /// Couldn't parse a json string. - JsonParse, - /// Couldn't parse a value. - TypeParse, - /// Couldn't find the host specified. - HostLookup, -} - -impl GDErrorKind { - /// Convert error kind into a full error with a source (and implicit - /// backtrace) - /// - /// ``` - /// use gamedig::{GDErrorKind, GDResult}; - /// let _: GDResult = "thing".parse().map_err(|e| GDErrorKind::TypeParse.context(e)); - /// ``` - pub fn context>(self, source: E) -> GDError { GDError::from_error(self, source) } -} - -#[cfg(test)] -mod tests { - use super::*; - - // Testing cloning the GDErrorKind type - #[test] - fn test_cloning() { - let error = GDErrorKind::BadGame; - let cloned_error = error.clone(); - assert_eq!(error, cloned_error); - } - - // test display GDError - #[test] - fn test_display() { - let err = GDErrorKind::BadGame.context("Rust is not a game"); - assert_eq!( - format!("{err}"), - "GDError{ kind=BadGame\n source=\"Rust is not a game\"\n backtrace=\n}\n" - ); - } -} diff --git a/crates/lib/src/errors/mod.rs b/crates/lib/src/errors/mod.rs deleted file mode 100644 index ecfcc84..0000000 --- a/crates/lib/src/errors/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Every GameDig errors. - -/// The Error with backtrace. -pub mod error; -/// All defined Error kinds. -pub mod kind; -/// `GDResult`, a shorthand of `Result`. -pub mod result; - -pub use error::*; -pub use kind::*; -pub use result::*; diff --git a/crates/lib/src/errors/result.rs b/crates/lib/src/errors/result.rs deleted file mode 100644 index b7bdef1..0000000 --- a/crates/lib/src/errors/result.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::GDError; - -/// `Result` of `T` and `GDError`. -pub type GDResult = Result; - -#[cfg(test)] -mod tests { - use super::*; - use crate::GDErrorKind; - - // Testing Ok variant of the GDResult type - #[test] - fn test_gdresult_ok() { - let result: GDResult = Ok(42); - assert_eq!(result, Ok(42)); - } - - // Testing Err variant of the GDResult type - #[test] - fn test_gdresult_err() { - let result: GDResult = Err(GDErrorKind::InvalidInput.into()); - assert!(result.is_err()); - } -} diff --git a/crates/lib/src/games/battalion1944.rs b/crates/lib/src/games/battalion1944.rs deleted file mode 100644 index fc4e9b6..0000000 --- a/crates/lib/src/games/battalion1944.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::protocols::valve::Engine; -use crate::{ - protocols::valve::{self, game}, - GDErrorKind::TypeParse, - GDResult, -}; -use std::net::{IpAddr, SocketAddr}; - -pub fn query(address: &IpAddr, port: Option) -> GDResult { - let mut valve_response = valve::query( - &SocketAddr::new(*address, port.unwrap_or(7780)), - Engine::new(489_940), - None, - None, - )?; - - if let Some(rules) = &mut valve_response.rules { - if let Some(bat_max_players) = rules.get("bat_max_players_i") { - valve_response.info.players_maximum = bat_max_players.parse().map_err(|e| TypeParse.context(e))?; - rules.remove("bat_max_players_i"); - } - - if let Some(bat_player_count) = rules.get("bat_player_count_s") { - valve_response.info.players_online = bat_player_count.parse().map_err(|e| TypeParse.context(e))?; - rules.remove("bat_player_count_s"); - } - - if let Some(bat_has_password) = rules.get("bat_has_password_s") { - valve_response.info.has_password = bat_has_password == "Y"; - rules.remove("bat_has_password_s"); - } - - if let Some(bat_name) = rules.get("bat_name_s") { - valve_response.info.name.clone_from(bat_name); - rules.remove("bat_name_s"); - } - - if let Some(bat_gamemode) = rules.get("bat_gamemode_s") { - valve_response.info.game_mode.clone_from(bat_gamemode); - rules.remove("bat_gamemode_s"); - } - - rules.remove("bat_map_s"); - } - - Ok(game::Response::new_from_valve_response(valve_response)) -} diff --git a/crates/lib/src/games/definitions.rs b/crates/lib/src/games/definitions.rs deleted file mode 100644 index efd44c8..0000000 --- a/crates/lib/src/games/definitions.rs +++ /dev/null @@ -1,160 +0,0 @@ -//! Static definitions of currently supported games - -use crate::games::minecraft::types::{LegacyGroup, Server}; -use crate::protocols::{gamespy::GameSpyVersion, quake::QuakeVersion, valve::Engine, Protocol}; -use crate::Game; - -use crate::protocols::types::{GatherToggle, ProprietaryProtocol}; -use crate::protocols::valve::GatheringSettings; -use phf::{phf_map, Map}; - -macro_rules! game { - ($name: literal, $default_port: expr, $protocol: expr) => { - game!( - $name, - $default_port, - $protocol, - GatheringSettings::default().into_extra() - ) - }; - - ($name: literal, $default_port: expr, $protocol: expr, $extra_request_settings: expr) => { - Game { - name: $name, - default_port: $default_port, - protocol: $protocol, - request_settings: $extra_request_settings, - } - }; -} - -/// Map of all currently supported games -pub static GAMES: Map<&'static str, Game> = phf_map! { - // Query with all minecraft protocols - "minecraft" => game!("Minecraft", 25565, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(None))), - // Query with specific minecraft protocols - "minecraftbedrock" => game!("Minecraft (bedrock)", 19132, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Bedrock)))), - "minecraftpocket" => game!("Minecraft (pocket)", 19132, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Bedrock)))), - "minecraftjava" => game!("Minecraft (java)", 25565, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Java)))), - "minecraftlegacy16" => game!("Minecraft (legacy 1.6)", 25565, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_6))))), - "minecraftlegacy14" => game!("Minecraft (legacy 1.4)", 25565, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_4))))), - "minecraftlegacyb18" => game!("Minecraft (legacy b1.8)", 25565, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Legacy(LegacyGroup::VB1_8))))), - "aapg" => game!("America's Army: Proving Grounds", 27020, Protocol::Valve(Engine::new(203_290)), GatheringSettings { - players: GatherToggle::Enforce, - rules: GatherToggle::Skip, - check_app_id: true, - }.into_extra()), - "abioticfactor" => game!("Abiotic Factor", 27015, Protocol::Valve(Engine::new(427_410))), - "alienswarm" => game!("Alien Swarm", 27015, Protocol::Valve(Engine::new(630))), - "aoc" => game!("Age of Chivalry", 27015, Protocol::Valve(Engine::new(17510))), - "a2oa" => game!("ARMA 2: Operation Arrowhead", 2304, Protocol::Valve(Engine::new(33930))), - "arma3" => game!("ARMA 3", 2303, Protocol::Valve(Engine::new(107_410))), - "ase" => game!("ARK: Survival Evolved", 27015, Protocol::Valve(Engine::new(346_110))), - "asrd" => game!("Alien Swarm: Reactive Drop", 2304, Protocol::Valve(Engine::new(563_560))), - "armareforger" => game!("Arma Reforger", 17777, Protocol::Valve(Engine::new(1_874_880)), GatheringSettings { - players: GatherToggle::Enforce, - rules: GatherToggle::Enforce, - check_app_id: false, - }.into_extra()), - "atlas" => game!("ATLAS", 57561, Protocol::Valve(Engine::new(834_910))), - "avorion" => game!("Avorion", 27020, Protocol::Valve(Engine::new(445_220))), - "avp2010" => game!("Aliens vs. Predator 2010", 27015, Protocol::Valve(Engine::new(10_680))), - "barotrauma" => game!("Barotrauma", 27016, Protocol::Valve(Engine::new(602_960))), - "basedefense" => game!("Base Defense", 27015, Protocol::Valve(Engine::new(632_730)), GatheringSettings { - players: GatherToggle::Enforce, - rules: GatherToggle::Skip, - check_app_id: true, - }.into_extra()), - "battalion1944" => game!("Battalion 1944", 7780, Protocol::Valve(Engine::new(489_940))), - "brainbread2" => game!("BrainBread 2", 27015, Protocol::Valve(Engine::new(346_330))), - "battlefield1942" => game!("Battlefield 1942", 23000, Protocol::Gamespy(GameSpyVersion::One)), - "blackmesa" => game!("Black Mesa", 27015, Protocol::Valve(Engine::new(362_890))), - "ballisticoverkill" => game!("Ballistic Overkill", 27016, Protocol::Valve(Engine::new(296_300))), - "codbo3" => game!("Call Of Duty: Black Ops 3", 27017, Protocol::Valve(Engine::new(311_210))), - "codenamecure" => game!("Codename CURE", 27015, Protocol::Valve(Engine::new(355_180))), - "colonysurvival" => game!("Colony Survival", 27004, Protocol::Valve(Engine::new(366_090))), - "conanexiles" => game!("Conan Exiles", 27015, Protocol::Valve(Engine::new(440_900)), GatheringSettings { - players: GatherToggle::Skip, - rules: GatherToggle::Enforce, - check_app_id: true, - }.into_extra()), - "counterstrike" => game!("Counter-Strike", 27015, Protocol::Valve(Engine::new_gold_src(false))), - "counterstrike2" => game!("Counter-Strike 2", 27015, Protocol::Valve(Engine::new(730))), - "cscz" => game!("Counter Strike: Condition Zero", 27015, Protocol::Valve(Engine::new_gold_src(false))), - "csgo" => game!("Counter-Strike: Global Offensive", 27015, Protocol::Valve(Engine::new(730))), - "css" => game!("Counter-Strike: Source", 27015, Protocol::Valve(Engine::new(240))), - "creativerse" => game!("Creativerse", 26901, Protocol::Valve(Engine::new(280_790))), - "crysiswars" => game!("Crysis Wars", 64100, Protocol::Gamespy(GameSpyVersion::Three)), - "dab" => game!("Double Action: Boogaloo", 27015, Protocol::Valve(Engine::new(317_360))), - "dod" => game!("Day of Defeat", 27015, Protocol::Valve(Engine::new_gold_src(false))), - "dods" => game!("Day of Defeat: Source", 27015, Protocol::Valve(Engine::new(300))), - "doi" => game!("Day of Infamy", 27015, Protocol::Valve(Engine::new(447_820))), - "dst" => game!("Don't Starve Together", 27016, Protocol::Valve(Engine::new(322_320))), - "enshrouded" => game!("Enshrouded", 15637, Protocol::Valve(Engine::new(1_203_620))), - "ffow" => game!("Frontlines: Fuel of War", 5478, Protocol::PROPRIETARY(ProprietaryProtocol::FFOW)), - "garrysmod" => game!("Garry's Mod", 27016, Protocol::Valve(Engine::new(4000))), - "hl2d" => game!("Half-Life 2 Deathmatch", 27015, Protocol::Valve(Engine::new(320))), - "hce" => game!("Halo: Combat Evolved", 2302, Protocol::Gamespy(GameSpyVersion::Two)), - "hlds" => game!("Half-Life Deathmatch: Source", 27015, Protocol::Valve(Engine::new(360))), - "hll" => game!("Hell Let Loose", 26420, Protocol::Valve(Engine::new(686_810))), - "insurgency" => game!("Insurgency", 27015, Protocol::Valve(Engine::new(222_880))), - "imic" => game!("Insurgency: Modern Infantry Combat", 27015, Protocol::Valve(Engine::new(17700))), - "insurgencysandstorm" => game!("Insurgency: Sandstorm", 27131, Protocol::Valve(Engine::new(581_320))), - "l4d" => game!("Left 4 Dead", 27015, Protocol::Valve(Engine::new(500))), - "l4d2" => game!("Left 4 Dead 2", 27015, Protocol::Valve(Engine::new(550))), - "ohd" => game!("Operation: Harsh Doorstop", 27005, Protocol::Valve(Engine::new_with_dedicated(736_590, 950_900))), - "onset" => game!("Onset", 7776, Protocol::Valve(Engine::new(1_105_810))), - "pixark" => game!("PixARK", 27015, Protocol::Valve(Engine::new(593_600))), - "postscriptum" => game!("Post Scriptum", 10037, Protocol::Valve(Engine::new(736_220))), - "projectzomboid" => game!("Project Zomboid", 16261, Protocol::Valve(Engine::new(108_600))), - "pvak2" => game!("Pirates, Vikings, and Knights II", 27015, Protocol::Valve(Engine::new(17_570))), - "quake1" => game!("Quake 1", 27500, Protocol::Quake(QuakeVersion::One)), - "quake2" => game!("Quake 2", 27910, Protocol::Quake(QuakeVersion::Two)), - "q3a" => game!("Quake 3 Arena", 27960, Protocol::Quake(QuakeVersion::Three)), - "risingworld" => game!("Rising World", 4254, Protocol::Valve(Engine::new(324_080)), GatheringSettings { - players: GatherToggle::Enforce, - rules: GatherToggle::Skip, - check_app_id: true, - }.into_extra()), - "ror2" => game!("Risk of Rain 2", 27016, Protocol::Valve(Engine::new(632_360))), - "rust" => game!("Rust", 27015, Protocol::Valve(Engine::new(252_490))), - "savage2" => game!("Savage 2", 11235, Protocol::PROPRIETARY(ProprietaryProtocol::Savage2)), - "sco" => game!("Sven Co-op", 27015, Protocol::Valve(Engine::new_gold_src(false))), - "sdtd" => game!("7 Days to Die", 26900, Protocol::Valve(Engine::new(251_570))), - "sof2" => game!("Soldier of Fortune 2", 20100, Protocol::Quake(QuakeVersion::Three)), - "soulmask" => game!("Soulmask", 27015, Protocol::Valve(Engine::new(2_646_460))), - "serioussam" => game!("Serious Sam", 25601, Protocol::Gamespy(GameSpyVersion::One)), - "squad" => game!("Squad", 27165, Protocol::Valve(Engine::new(393_380))), - "starbound" => game!("Starbound", 21025, Protocol::Valve(Engine::new(211_820)), GatheringSettings { - players: GatherToggle::Enforce, - rules: GatherToggle::Enforce, - check_app_id: false, - }.into_extra()), - "theforest" => game!("The Forest", 27016, Protocol::Valve(Engine::new_with_dedicated(242_760, 556_450))), - "thefront" => game!("The Front", 27015, Protocol::Valve(Engine::new(2_285_150))), - "teamfortress2" => game!("Team Fortress 2", 27015, Protocol::Valve(Engine::new(440))), - "tfc" => game!("Team Fortress Classic", 27015, Protocol::Valve(Engine::new_gold_src(false))), - "theship" => game!("The Ship", 27015, Protocol::PROPRIETARY(ProprietaryProtocol::TheShip)), - "unturned" => game!("Unturned", 27015, Protocol::Valve(Engine::new(304_930))), - "unrealtournament" => game!("Unreal Tournament", 7778, Protocol::Gamespy(GameSpyVersion::One)), - "valheim" => game!("Valheim", 2457, Protocol::Valve(Engine::new(892_970)), GatheringSettings { - players: GatherToggle::Enforce, - rules: GatherToggle::Skip, - check_app_id: true, - }.into_extra()), - "vrising" => game!("V Rising", 27016, Protocol::Valve(Engine::new(1_604_030))), - "jc2m" => game!("Just Cause 2: Multiplayer", 7777, Protocol::PROPRIETARY(ProprietaryProtocol::JC2M)), - "warsow" => game!("Warsow", 44400, Protocol::Quake(QuakeVersion::Three)), - "dhe4445" => game!("Darkest Hour: Europe '44-'45 (2008)", 7758, Protocol::Unreal2), - "devastation" => game!("Devastation (2003)", 7778, Protocol::Unreal2), - "killingfloor" => game!("Killing Floor", 7708, Protocol::Unreal2), - "redorchestra" => game!("Red Orchestra", 7759, Protocol::Unreal2), - "unrealtournament2003" => game!("Unreal Tournament 2003", 7758, Protocol::Unreal2), - "unrealtournament2004" => game!("Unreal Tournament 2004", 7778, Protocol::Unreal2), - "eco" => game!("Eco", 3000, Protocol::PROPRIETARY(ProprietaryProtocol::Eco)), - "zps" => game!("Zombie Panic: Source", 27015, Protocol::Valve(Engine::new(17_500))), - "moe" => game!("Myth Of Empires", 12888, Protocol::Valve(Engine::new(1_371_580))), - "mordhau" => game!("Mordhau", 27015, Protocol::Valve(Engine::new(629_760))), - "mindustry" => game!("Mindustry", crate::games::mindustry::DEFAULT_PORT, Protocol::PROPRIETARY(ProprietaryProtocol::Mindustry)), - "nla" => game!("Nova-Life: Amboise", 27015, Protocol::Valve(Engine::new(885_570))), -}; diff --git a/crates/lib/src/games/eco/mod.rs b/crates/lib/src/games/eco/mod.rs deleted file mode 100644 index e22cf53..0000000 --- a/crates/lib/src/games/eco/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -/// The implementation. -/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/ffow.js) -pub mod protocol; -/// All types used by the implementation. -pub mod types; - -pub use protocol::*; -pub use types::*; diff --git a/crates/lib/src/games/eco/protocol.rs b/crates/lib/src/games/eco/protocol.rs deleted file mode 100644 index d55aad3..0000000 --- a/crates/lib/src/games/eco/protocol.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::eco::{EcoRequestSettings, Response, Root}; -use crate::http::HttpClient; -use crate::{GDResult, TimeoutSettings}; -use std::net::{IpAddr, SocketAddr}; - -/// Query an eco server. -#[inline] -pub fn query(address: &IpAddr, port: Option) -> GDResult { query_with_timeout(address, port, &None) } - -/// Query an eco server. -#[inline] -pub fn query_with_timeout( - address: &IpAddr, - port: Option, - timeout_settings: &Option, -) -> GDResult { - query_with_timeout_and_extra_settings(address, port, timeout_settings, None) -} - -/// Query an eco server. -pub fn query_with_timeout_and_extra_settings( - address: &IpAddr, - port: Option, - timeout_settings: &Option, - extra_settings: Option, -) -> GDResult { - let address = &SocketAddr::new(*address, port.unwrap_or(3001)); - let mut client = HttpClient::new( - address, - timeout_settings, - extra_settings.unwrap_or_default().into(), - )?; - - let response = client.get_json::("/frontpage", None)?; - - Ok(response.into()) -} diff --git a/crates/lib/src/games/eco/types.rs b/crates/lib/src/games/eco/types.rs deleted file mode 100644 index be3af6f..0000000 --- a/crates/lib/src/games/eco/types.rs +++ /dev/null @@ -1,241 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -use crate::http::{HttpProtocol, HttpSettings}; -use crate::protocols::types::{CommonPlayer, CommonResponse}; -use crate::ExtraRequestSettings; - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Root { - #[serde(rename = "Info")] - pub info: Info, -} - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Info { - #[serde(rename = "External")] - pub external: bool, - #[serde(rename = "GamePort")] - pub game_port: u32, - #[serde(rename = "WebPort")] - pub web_port: u32, - #[serde(rename = "IsLAN")] - pub is_lan: bool, - #[serde(rename = "Description")] - pub description: String, - #[serde(rename = "DetailedDescription")] - pub detailed_description: String, - #[serde(rename = "Category")] - pub category: String, - #[serde(rename = "OnlinePlayers")] - pub online_players: u32, - #[serde(rename = "TotalPlayers")] - pub total_players: u32, - #[serde(rename = "OnlinePlayersNames")] - pub online_players_names: Vec, - #[serde(rename = "AdminOnline")] - pub admin_online: bool, - #[serde(rename = "TimeSinceStart")] - pub time_since_start: f64, - #[serde(rename = "TimeLeft")] - pub time_left: f64, - #[serde(rename = "Animals")] - pub animals: u32, - #[serde(rename = "Plants")] - pub plants: u32, - #[serde(rename = "Laws")] - pub laws: u32, - #[serde(rename = "WorldSize")] - pub world_size: String, - #[serde(rename = "Version")] - pub version: String, - #[serde(rename = "EconomyDesc")] - pub economy_desc: String, - #[serde(rename = "SkillSpecializationSetting")] - pub skill_specialization_setting: String, - #[serde(rename = "Language")] - pub language: String, - #[serde(rename = "HasPassword")] - pub has_password: bool, - #[serde(rename = "HasMeteor")] - pub has_meteor: bool, - #[serde(rename = "DistributionStationItems")] - pub distribution_station_items: String, - #[serde(rename = "Playtimes")] - pub playtimes: String, - #[serde(rename = "DiscordAddress")] - pub discord_address: String, - #[serde(rename = "IsPaused")] - pub is_paused: bool, - #[serde(rename = "ActiveAndOnlinePlayers")] - pub active_and_online_players: u32, - #[serde(rename = "PeakActivePlayers")] - pub peak_active_players: u32, - #[serde(rename = "MaxActivePlayers")] - pub max_active_players: u32, - #[serde(rename = "ShelfLifeMultiplier")] - pub shelf_life_multiplier: f64, - #[serde(rename = "ExhaustionAfterHours")] - pub exhaustion_after_hours: f64, - #[serde(rename = "IsLimitingHours")] - pub is_limiting_hours: bool, - #[serde(rename = "ServerAchievementsDict")] - pub server_achievements_dict: HashMap, - #[serde(rename = "RelayAddress")] - pub relay_address: String, - #[serde(rename = "Access")] - pub access: String, - #[serde(rename = "JoinUrl")] - pub join_url: String, -} - -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct Player { - pub name: String, -} - -impl CommonPlayer for Player { - fn as_original(&self) -> crate::protocols::types::GenericPlayer<'_> { - crate::protocols::types::GenericPlayer::Eco(self) - } - - fn name(&self) -> &str { &self.name } -} - -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq)] -pub struct Response { - pub external: bool, - pub port: u32, - pub query_port: u32, - pub is_lan: bool, - pub description: String, // this and other fields require some text filtering - pub description_detailed: String, - pub description_economy: String, - pub category: String, - pub players_online: u32, - pub players_maximum: u32, - pub players: Vec, - pub admin_online: bool, - pub time_since_start: f64, - pub time_left: f64, - pub animals: u32, - pub plants: u32, - pub laws: u32, - pub world_size: String, - pub game_version: String, - pub skill_specialization_setting: String, - pub language: String, - pub has_password: bool, - pub has_meteor: bool, - pub distribution_station_items: String, - pub playtimes: String, - pub discord_address: String, - pub is_paused: bool, - pub active_and_online_players: u32, - pub peak_active_players: u32, - pub max_active_players: u32, - pub shelf_life_multiplier: f64, - pub exhaustion_after_hours: f64, - pub is_limiting_hours: bool, - pub server_achievements_dict: HashMap, - pub relay_address: String, - pub access: String, - pub connect: String, -} - -impl From for Response { - fn from(root: Root) -> Self { - let value = root.info; - Self { - external: value.external, - port: value.game_port, - query_port: value.web_port, - is_lan: value.is_lan, - description: value.description, - description_detailed: value.detailed_description, - description_economy: value.economy_desc, - category: value.category, - players_online: value.online_players, - players_maximum: value.total_players, - players: value - .online_players_names - .iter() - .map(|player| { - Player { - name: player.clone(), - } - }) - .collect(), - admin_online: value.admin_online, - time_since_start: value.time_since_start, - time_left: value.time_left, - animals: value.animals, - plants: value.plants, - laws: value.laws, - world_size: value.world_size, - game_version: value.version, - skill_specialization_setting: value.skill_specialization_setting, - language: value.language, - has_password: value.has_password, - has_meteor: value.has_meteor, - distribution_station_items: value.distribution_station_items, - playtimes: value.playtimes, - discord_address: value.discord_address, - is_paused: value.is_paused, - active_and_online_players: value.active_and_online_players, - peak_active_players: value.peak_active_players, - max_active_players: value.max_active_players, - shelf_life_multiplier: value.shelf_life_multiplier, - exhaustion_after_hours: value.exhaustion_after_hours, - is_limiting_hours: value.is_limiting_hours, - server_achievements_dict: value.server_achievements_dict, - relay_address: value.relay_address, - access: value.access, - connect: value.join_url, - } - } -} - -impl CommonResponse for Response { - fn as_original(&self) -> crate::protocols::GenericResponse<'_> { crate::protocols::GenericResponse::Eco(self) } - - fn players_online(&self) -> u32 { self.players_online } - - fn players_maximum(&self) -> u32 { self.players_maximum } - - fn description(&self) -> Option<&str> { Some(&self.description) } - - fn game_version(&self) -> Option<&str> { Some(&self.game_version) } - - fn has_password(&self) -> Option { Some(self.has_password) } - - fn players(&self) -> Option> { Some(self.players.iter().map(|p| p as _).collect()) } -} - -/// Extra request settings for eco queries. -#[derive(Debug, Default, Clone, Eq, PartialEq)] -pub struct EcoRequestSettings { - hostname: Option, -} - -impl From for EcoRequestSettings { - fn from(value: ExtraRequestSettings) -> Self { - Self { - hostname: value.hostname, - } - } -} - -impl From for HttpSettings { - fn from(value: EcoRequestSettings) -> Self { - Self { - protocol: HttpProtocol::Http, - hostname: value.hostname, - headers: Vec::with_capacity(0), - } - } -} diff --git a/crates/lib/src/games/epic.rs b/crates/lib/src/games/epic.rs deleted file mode 100644 index 6e2771c..0000000 --- a/crates/lib/src/games/epic.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! Unreal2 game query modules - -use crate::protocols::epic::game_query_mod; - -game_query_mod!( - asa, - "Ark: Survival Ascended", - 7777, - Credentials { - deployment: "ad9a8feffb3b4b2ca315546f038c3ae2", - id: "xyza7891muomRmynIIHaJB9COBKkwj6n", - secret: "PP5UGxysEieNfSrEicaD1N2Bb3TdXuD7xHYcsdUHZ7s", - auth_by_external: false, - } -); diff --git a/crates/lib/src/games/ffow/mod.rs b/crates/lib/src/games/ffow/mod.rs deleted file mode 100644 index db37a19..0000000 --- a/crates/lib/src/games/ffow/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -/// The implementation. -/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/ffow.js) -pub mod protocol; -/// All types used by the implementation. -pub mod types; - -pub use protocol::*; -pub use types::*; diff --git a/crates/lib/src/games/ffow/protocol.rs b/crates/lib/src/games/ffow/protocol.rs deleted file mode 100644 index 95899d0..0000000 --- a/crates/lib/src/games/ffow/protocol.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::buffer::{Buffer, Utf8Decoder}; -use crate::games::ffow::types::Response; -use crate::protocols::types::TimeoutSettings; -use crate::protocols::valve::{Engine, Environment, Server, ValveProtocol}; -use crate::GDResult; -use byteorder::LittleEndian; -use std::net::{IpAddr, SocketAddr}; - -pub fn query(address: &IpAddr, port: Option) -> GDResult { query_with_timeout(address, port, None) } - -pub fn query_with_timeout( - address: &IpAddr, - port: Option, - timeout_settings: Option, -) -> GDResult { - let mut client = ValveProtocol::new( - &SocketAddr::new(*address, port.unwrap_or(5478)), - timeout_settings, - )?; - let data = client.get_request_data( - &Engine::GoldSrc(true), - 0, - 0x46, - String::from("LSQ").into_bytes(), - )?; - - let mut buffer = Buffer::::new(&data); - - let protocol_version = buffer.read::()?; - let name = buffer.read_string::(None)?; - let map = buffer.read_string::(None)?; - let active_mod = buffer.read_string::(None)?; - let game_mode = buffer.read_string::(None)?; - let description = buffer.read_string::(None)?; - let game_version = buffer.read_string::(None)?; - buffer.move_cursor(2)?; - let players_online = buffer.read::()?; - let players_maximum = buffer.read::()?; - let server_type = Server::from_gldsrc(buffer.read::()?)?; - let environment_type = Environment::from_gldsrc(buffer.read::()?)?; - let has_password = buffer.read::()? == 1; - let vac_secured = buffer.read::()? == 1; - buffer.move_cursor(1)?; // average fps - let round = buffer.read::()?; - let rounds_maximum = buffer.read::()?; - let time_left = buffer.read::()?; - - Ok(Response { - protocol_version, - name, - active_mod, - game_mode, - game_version, - description, - map, - players_online, - players_maximum, - server_type, - environment_type, - has_password, - vac_secured, - round, - rounds_maximum, - time_left, - }) -} diff --git a/crates/lib/src/games/ffow/types.rs b/crates/lib/src/games/ffow/types.rs deleted file mode 100644 index c66ff48..0000000 --- a/crates/lib/src/games/ffow/types.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::protocols::types::CommonResponse; -use crate::protocols::valve::{Environment, Server}; -use crate::protocols::GenericResponse; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -/// The query response. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct Response { - /// Protocol used by the server. - pub protocol_version: u8, - /// Name of the server. - pub name: String, - /// Map name. - pub active_mod: String, - /// Running game mode. - pub game_mode: String, - /// The version that the server is running on. - pub game_version: String, - /// Description of the server. - pub description: String, - /// Current map. - pub map: String, - /// Number of players on the server. - pub players_online: u8, - /// Maximum number of players the server reports it can hold. - pub players_maximum: u8, - /// Dedicated, NonDedicated or SourceTV - pub server_type: Server, - /// The Operating System that the server is on. - pub environment_type: Environment, - /// Indicates whether the server requires a password. - pub has_password: bool, - /// Indicates whether the server uses VAC. - pub vac_secured: bool, - /// Current round index. - pub round: u8, - /// Maximum amount of rounds. - pub rounds_maximum: u8, - /// Time left for the current round in seconds. - pub time_left: u16, -} - -impl CommonResponse for Response { - fn as_original(&self) -> GenericResponse<'_> { GenericResponse::FFOW(self) } - - fn name(&self) -> Option<&str> { Some(&self.name) } - fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) } - fn description(&self) -> Option<&str> { Some(&self.description) } - fn game_version(&self) -> Option<&str> { Some(&self.game_version) } - fn map(&self) -> Option<&str> { Some(&self.map) } - fn has_password(&self) -> Option { Some(self.has_password) } - fn players_maximum(&self) -> u32 { self.players_maximum.into() } - fn players_online(&self) -> u32 { self.players_online.into() } -} diff --git a/crates/lib/src/games/gamespy.rs b/crates/lib/src/games/gamespy.rs deleted file mode 100644 index 31f6a16..0000000 --- a/crates/lib/src/games/gamespy.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Gamespy game query modules - -use crate::protocols::gamespy::game_query_mod; - -game_query_mod!(battlefield1942, "Battlefield 1942", one, 23000); -game_query_mod!(crysiswars, "Crysis Wars", three, 64100); -game_query_mod!(hce, "Halo: Combat Evolved", two, 2302); -game_query_mod!(serioussam, "Serious Sam", one, 25601); -game_query_mod!(unrealtournament, "Unreal Tournament", one, 7778); diff --git a/crates/lib/src/games/jc2m/mod.rs b/crates/lib/src/games/jc2m/mod.rs deleted file mode 100644 index 642b155..0000000 --- a/crates/lib/src/games/jc2m/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -/// The implementation. -/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/jc2mp.js) -pub mod protocol; -/// All types used by the implementation. -pub mod types; - -pub use protocol::*; -pub use types::*; diff --git a/crates/lib/src/games/jc2m/protocol.rs b/crates/lib/src/games/jc2m/protocol.rs deleted file mode 100644 index fb92e4a..0000000 --- a/crates/lib/src/games/jc2m/protocol.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::buffer::{Buffer, Utf8Decoder}; -use crate::jc2m::{Player, Response}; -use crate::protocols::gamespy::common::has_password; -use crate::protocols::gamespy::three::{data_to_map, GameSpy3}; -use crate::protocols::types::TimeoutSettings; -use crate::GDErrorKind::{PacketBad, TypeParse}; -use crate::GDResult; -use byteorder::BigEndian; -use std::net::{IpAddr, SocketAddr}; - -fn parse_players_and_teams(packet: &[u8]) -> GDResult> { - let mut buf = Buffer::::new(packet); - - let count = buf.read::()?; - let mut players = Vec::with_capacity(count as usize); - - while buf.remaining_length() != 0 { - players.push(Player { - name: buf.read_string::(None)?, - steam_id: buf.read_string::(None)?, - ping: buf.read::()?, - }); - } - - Ok(players) -} - -pub fn query(address: &IpAddr, port: Option) -> GDResult { query_with_timeout(address, port, None) } - -pub fn query_with_timeout( - address: &IpAddr, - port: Option, - timeout_settings: Option, -) -> GDResult { - let mut client = GameSpy3::new_custom( - &SocketAddr::new(*address, port.unwrap_or(7777)), - timeout_settings, - [0xFF, 0xFF, 0xFF, 0x02], - true, - )?; - - let packets = client.get_server_packets()?; - let data = packets - .first() - .ok_or_else(|| PacketBad.context("First packet missing"))?; - - let (mut server_vars, remaining_data) = data_to_map(data)?; - let players = parse_players_and_teams(&remaining_data)?; - - let players_maximum = server_vars - .remove("maxplayers") - .ok_or_else(|| PacketBad.context("Server variables missing maxplayers"))? - .parse() - .map_err(|e| TypeParse.context(e))?; - let players_online = match server_vars.remove("numplayers") { - None => players.len(), - Some(v) => { - let reported_players = v.parse().map_err(|e| TypeParse.context(e))?; - match reported_players < players.len() { - true => players.len(), - false => reported_players, - } - } - } as u32; - - Ok(Response { - game_version: server_vars.remove("version").ok_or(PacketBad)?, - description: server_vars.remove("description").ok_or(PacketBad)?, - name: server_vars.remove("hostname").ok_or(PacketBad)?, - has_password: has_password(&mut server_vars)?, - players, - players_maximum, - players_online, - }) -} diff --git a/crates/lib/src/games/jc2m/types.rs b/crates/lib/src/games/jc2m/types.rs deleted file mode 100644 index 6bfcb86..0000000 --- a/crates/lib/src/games/jc2m/types.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer}; -use crate::protocols::GenericResponse; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct Player { - pub name: String, - pub steam_id: String, - pub ping: u16, -} - -impl CommonPlayer for Player { - fn as_original(&self) -> GenericPlayer<'_> { GenericPlayer::JCMP2(self) } - - fn name(&self) -> &str { &self.name } -} - -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct Response { - pub game_version: String, - pub description: String, - pub name: String, - pub has_password: bool, - pub players: Vec, - pub players_maximum: u32, - pub players_online: u32, -} - -impl CommonResponse for Response { - fn as_original(&self) -> GenericResponse<'_> { GenericResponse::JC2M(self) } - - fn game_version(&self) -> Option<&str> { Some(&self.game_version) } - fn description(&self) -> Option<&str> { Some(&self.description) } - fn name(&self) -> Option<&str> { Some(&self.name) } - fn has_password(&self) -> Option { Some(self.has_password) } - fn players_maximum(&self) -> u32 { self.players_maximum } - fn players_online(&self) -> u32 { self.players_online } - - fn players(&self) -> Option> { - Some( - self.players - .iter() - .map(|p| p as &dyn CommonPlayer) - .collect(), - ) - } -} diff --git a/crates/lib/src/games/mindustry/mod.rs b/crates/lib/src/games/mindustry/mod.rs deleted file mode 100644 index dc3fb7c..0000000 --- a/crates/lib/src/games/mindustry/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Mindustry game ping (v146) -//! -//! [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/ArcNetProvider.java#L225-L259) - -use std::{net::IpAddr, net::SocketAddr}; - -use crate::{GDResult, TimeoutSettings}; - -use self::types::ServerData; - -pub mod types; - -pub mod protocol; - -/// Default mindustry server port -/// -/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/Vars.java#L141-L142) -pub const DEFAULT_PORT: u16 = 6567; - -/// Query a mindustry server. -pub fn query(ip: &IpAddr, port: Option, timeout_settings: &Option) -> GDResult { - let address = SocketAddr::new(*ip, port.unwrap_or(DEFAULT_PORT)); - - protocol::query_with_retries(&address, timeout_settings) -} diff --git a/crates/lib/src/games/mindustry/protocol.rs b/crates/lib/src/games/mindustry/protocol.rs deleted file mode 100644 index 04e0e05..0000000 --- a/crates/lib/src/games/mindustry/protocol.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::net::SocketAddr; - -use crate::{ - buffer::{self, Buffer}, - socket::{Socket, UdpSocket}, - utils, - GDResult, - TimeoutSettings, -}; - -use super::types::ServerData; - -/// Mindustry max datagram packet size. -pub const MAX_BUFFER_SIZE: usize = 500; - -/// Send a ping packet. -/// -/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/ArcNetProvider.java#L248) -pub(crate) fn send_ping(socket: &mut UdpSocket) -> GDResult<()> { socket.send(&[-2i8 as u8, 1i8 as u8]) } - -/// Parse server data. -/// -/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/NetworkIO.java#L122-L135) -pub fn parse_server_data( - buffer: &mut Buffer, -) -> GDResult { - Ok(ServerData { - host: buffer.read_string::(None)?, - map: buffer.read_string::(None)?, - players: buffer.read()?, - wave: buffer.read()?, - version: buffer.read()?, - version_type: buffer.read_string::(None)?, - gamemode: buffer.read::()?.try_into()?, - player_limit: buffer.read()?, - description: buffer.read_string::(None)?, - mode_name: buffer.read_string::(None).ok(), - }) -} - -/// Query a Mindustry server (without retries). -pub fn query(address: &SocketAddr, timeout_settings: &Option) -> GDResult { - let mut socket = UdpSocket::new(address, timeout_settings)?; - - send_ping(&mut socket)?; - - let socket_data = socket.receive(Some(MAX_BUFFER_SIZE))?; - let mut buffer = Buffer::new(&socket_data); - - parse_server_data::(&mut buffer) -} - -/// Query a Mindustry server. -pub fn query_with_retries(address: &SocketAddr, timeout_settings: &Option) -> GDResult { - let retries = TimeoutSettings::get_retries_or_default(timeout_settings); - - utils::retry_on_timeout(retries, || query(address, timeout_settings)) -} diff --git a/crates/lib/src/games/mindustry/types.rs b/crates/lib/src/games/mindustry/types.rs deleted file mode 100644 index 436d5b5..0000000 --- a/crates/lib/src/games/mindustry/types.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::{ - protocols::types::{CommonResponse, GenericResponse}, - GDErrorKind, -}; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -/// Mindustry sever data -/// -/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/NetworkIO.java#L122-L135) -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct ServerData { - pub host: String, - pub map: String, - pub players: i32, - pub wave: i32, - pub version: i32, - pub version_type: String, - pub gamemode: GameMode, - pub player_limit: i32, - pub description: String, - pub mode_name: Option, -} - -/// Mindustry game mode -/// -/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/game/Gamemode.java) -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum GameMode { - Survival, - Sandbox, - Attack, - PVP, - Editor, -} - -impl TryFrom for GameMode { - type Error = GDErrorKind; - fn try_from(value: u8) -> Result { - use GameMode::*; - Ok(match value { - 0 => Survival, - 1 => Sandbox, - 2 => Attack, - 3 => PVP, - 4 => Editor, - _ => return Err(GDErrorKind::TypeParse), - }) - } -} - -impl GameMode { - const fn as_str(&self) -> &'static str { - match self { - Self::Survival => "survival", - Self::Sandbox => "sandbox", - Self::Attack => "attack", - Self::PVP => "pvp", - Self::Editor => "editor", - } - } -} - -impl CommonResponse for ServerData { - fn as_original(&self) -> GenericResponse<'_> { GenericResponse::Mindustry(self) } - - fn players_online(&self) -> u32 { self.players.try_into().unwrap_or(0) } - fn players_maximum(&self) -> u32 { self.player_limit.try_into().unwrap_or(0) } - - fn game_mode(&self) -> Option<&str> { Some(self.gamemode.as_str()) } - - fn map(&self) -> Option<&str> { Some(&self.map) } - fn description(&self) -> Option<&str> { Some(&self.description) } -} - -#[cfg(test)] -mod test { - use crate::protocols::types::CommonResponse; - - use super::ServerData; - - #[test] - fn common_impl() { - let data = ServerData { - host: String::from("host"), - map: String::from("map"), - players: 5, - wave: 2, - version: 142, - version_type: String::from("steam"), - gamemode: super::GameMode::PVP, - player_limit: 20, - description: String::from("description"), - mode_name: Some(String::from("campaign")), - }; - - let common: &dyn CommonResponse = &data; - - assert_eq!(common.players_online(), 5); - assert_eq!(common.players_maximum(), 20); - assert_eq!(common.game_mode(), Some("pvp")); - assert_eq!(common.map(), Some("map")); - assert_eq!(common.description(), Some("description")); - } -} diff --git a/crates/lib/src/games/minecraft/mod.rs b/crates/lib/src/games/minecraft/mod.rs deleted file mode 100644 index dbb8c34..0000000 --- a/crates/lib/src/games/minecraft/mod.rs +++ /dev/null @@ -1,69 +0,0 @@ -/// The implementation. -/// Reference: [Server List Ping](https://wiki.vg/Server_List_Ping) -pub mod protocol; -/// All types used by the implementation. -pub mod types; - -#[allow(unused_imports)] -pub use protocol::*; -pub use types::*; - -use crate::{GDErrorKind, GDResult}; -use std::net::{IpAddr, SocketAddr}; - -/// Query with all the protocol variants one by one (Java -> Bedrock -> Legacy -/// (1.6 -> 1.4 -> Beta 1.8)). -pub fn query(address: &IpAddr, port: Option) -> GDResult { - if let Ok(response) = query_java(address, port, None) { - return Ok(response); - } - - if let Ok(response) = query_bedrock(address, port) { - return Ok(JavaResponse::from_bedrock_response(response)); - } - - if let Ok(response) = query_legacy(address, port) { - return Ok(response); - } - - Err(GDErrorKind::AutoQuery.into()) -} - -/// Query a Java Server. -pub fn query_java( - address: &IpAddr, - port: Option, - request_settings: Option, -) -> GDResult { - protocol::query_java( - &SocketAddr::new(*address, port_or_java_default(port)), - None, - request_settings, - ) -} - -/// Query a (Java) Legacy Server (1.6 -> 1.4 -> Beta 1.8). -pub fn query_legacy(address: &IpAddr, port: Option) -> GDResult { - protocol::query_legacy(&SocketAddr::new(*address, port_or_java_default(port)), None) -} - -/// Query a specific (Java) Legacy Server. -pub fn query_legacy_specific(group: LegacyGroup, address: &IpAddr, port: Option) -> GDResult { - protocol::query_legacy_specific( - group, - &SocketAddr::new(*address, port_or_java_default(port)), - None, - ) -} - -/// Query a Bedrock Server. -pub fn query_bedrock(address: &IpAddr, port: Option) -> GDResult { - protocol::query_bedrock( - &SocketAddr::new(*address, port_or_bedrock_default(port)), - None, - ) -} - -fn port_or_java_default(port: Option) -> u16 { port.unwrap_or(25565) } - -fn port_or_bedrock_default(port: Option) -> u16 { port.unwrap_or(19132) } diff --git a/crates/lib/src/games/minecraft/protocol/bedrock.rs b/crates/lib/src/games/minecraft/protocol/bedrock.rs deleted file mode 100644 index 4d30bba..0000000 --- a/crates/lib/src/games/minecraft/protocol/bedrock.rs +++ /dev/null @@ -1,111 +0,0 @@ -// This file has code that has been documented by the NodeJS GameDig library -// (MIT) from https://github.com/gamedig/node-gamedig/blob/master/protocols/minecraftbedrock.js -use crate::{ - buffer::{Buffer, Utf8Decoder}, - games::minecraft::{BedrockResponse, GameMode, Server}, - protocols::types::TimeoutSettings, - socket::{Socket, UdpSocket}, - utils::{error_by_expected_size, retry_on_timeout}, - GDErrorKind::{PacketBad, TypeParse}, - GDResult, -}; - -use std::net::SocketAddr; - -use byteorder::LittleEndian; - -pub struct Bedrock { - socket: UdpSocket, - retry_count: usize, -} - -impl Bedrock { - fn new(address: &SocketAddr, timeout_settings: Option) -> GDResult { - let socket = UdpSocket::new(address, &timeout_settings)?; - - let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings); - Ok(Self { - socket, - retry_count, - }) - } - - fn send_status_request(&mut self) -> GDResult<()> { - self.socket.send(&[ - 0x01, // Message ID: ID_UNCONNECTED_PING - 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, // Nonce / timestamp - 0x00, 0xff, 0xff, 0x00, 0xfe, 0xfe, 0xfe, 0xfe, 0xfd, 0xfd, 0xfd, 0xfd, 0x12, 0x34, // Magic - 0x56, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Client GUID - ])?; - - Ok(()) - } - - /// Send a status request, and parse the response. - /// This function will retry fetch on timeouts. - fn get_info(&mut self) -> GDResult { - retry_on_timeout(self.retry_count, move || self.get_info_impl()) - } - - /// Send a status request, and parse the response (without retry logic). - fn get_info_impl(&mut self) -> GDResult { - self.send_status_request()?; - - let received = self.socket.receive(None)?; - let mut buffer = Buffer::::new(&received); - - if buffer.read::()? != 0x1c { - return Err(PacketBad.context("Expected 0x1c")); - } - - // Checking for our nonce directly from a u64 (as the nonce is 8 bytes). - if buffer.read::()? != 9_833_440_827_789_222_417 { - return Err(PacketBad.context("Invalid nonce")); - } - - // These 8 bytes are identical to the serverId string we receive in decimal - // below - buffer.move_cursor(8)?; - - // Verifying the magic value (as we need 16 bytes, cast to two u64 values) - if buffer.read::()? != 18_374_403_896_610_127_616 { - return Err(PacketBad.context("Invalid magic")); - } - - if buffer.read::()? != 8_671_175_388_723_805_693 { - return Err(PacketBad.context("Invalid magic")); - } - - let remaining_length = buffer.switch_endian_chunk(2)?.read::()? as usize; - - error_by_expected_size(remaining_length, buffer.remaining_length())?; - - let binding = buffer.read_string::(None)?; - let status: Vec<&str> = binding.split(';').collect(); - - // We must have at least 6 values - if status.len() < 6 { - return Err(PacketBad.context("Not enough values")); - } - - Ok(BedrockResponse { - edition: status[0].to_string(), - name: status[1].to_string(), - version_name: status[3].to_string(), - protocol_version: status[2].to_string(), - players_maximum: status[5].parse().map_err(|e| TypeParse.context(e))?, - players_online: status[4].parse().map_err(|e| TypeParse.context(e))?, - id: status.get(6).map(std::string::ToString::to_string), - map: status.get(7).map(std::string::ToString::to_string), - game_mode: match status.get(8) { - None => None, - Some(v) => Some(GameMode::from_bedrock(v)?), - }, - server_type: Server::Bedrock, - }) - } - - pub fn query(address: &SocketAddr, timeout_settings: Option) -> GDResult { - Self::new(address, timeout_settings)?.get_info() - } -} diff --git a/crates/lib/src/games/minecraft/protocol/java.rs b/crates/lib/src/games/minecraft/protocol/java.rs deleted file mode 100644 index 21aa4d9..0000000 --- a/crates/lib/src/games/minecraft/protocol/java.rs +++ /dev/null @@ -1,178 +0,0 @@ -use crate::{ - buffer::Buffer, - games::minecraft::{as_string, as_varint, get_string, get_varint, JavaResponse, Player, RequestSettings, Server}, - protocols::types::TimeoutSettings, - socket::{Socket, TcpSocket}, - utils::retry_on_timeout, - GDErrorKind::{JsonParse, PacketBad}, - GDResult, -}; - -use byteorder::LittleEndian; -use serde_json::Value; -use std::net::SocketAddr; -use std::time::{SystemTime, UNIX_EPOCH}; - -pub struct Java { - socket: TcpSocket, - request_settings: RequestSettings, - retry_count: usize, -} - -impl Java { - fn new( - address: &SocketAddr, - timeout_settings: Option, - request_settings: Option, - ) -> GDResult { - let socket = TcpSocket::new(address, &timeout_settings)?; - - let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings); - Ok(Self { - socket, - request_settings: request_settings.unwrap_or_default(), - retry_count, - }) - } - - fn send(&mut self, data: Vec) -> GDResult<()> { - self.socket - .send(&[as_varint(data.len() as i32), data].concat()) - } - - fn receive(&mut self) -> GDResult> { - let data = &self.socket.receive(None)?; - let mut buffer = Buffer::::new(data); - - let _packet_length = get_varint(&mut buffer)? as usize; - // this declared 'packet length' from within the packet might be wrong (?), not - // checking with it... - - Ok(buffer.remaining_bytes().to_vec()) - } - - fn send_handshake(&mut self) -> GDResult<()> { - let handshake_payload = [ - &[ - // Packet ID (0) - 0x00, - ], // Protocol Version (-1 to determine version) - as_varint(self.request_settings.protocol_version).as_slice(), - // Server address (can be anything) - as_string(&self.request_settings.hostname)?.as_slice(), - // Server port (can be anything) - &self.socket.port().to_le_bytes(), - &[ - // Next state (1 for status) - 0x01, - ], - ] - .concat(); - - self.send(handshake_payload)?; - - Ok(()) - } - - fn send_status_request(&mut self) -> GDResult<()> { - self.send( - [0x00] // Packet ID (0) - .to_vec(), - )?; - - Ok(()) - } - - fn send_ping_request(&mut self) -> GDResult<()> { - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64; - - let mut payload = [0x01].to_vec(); // Packet ID (1) - payload.extend_from_slice(×tamp.to_be_bytes()); // Timestamp (long, 8 bytes) - - self.send(payload)?; - - Ok(()) - } - - /// Send minecraft ping request and parse the response. - /// This function will retry fetch on timeouts. - fn get_info(&mut self) -> GDResult { - retry_on_timeout(self.retry_count, move || self.get_info_impl()) - } - - /// Send minecraft ping request and parse the response (without retry - /// logic). - fn get_info_impl(&mut self) -> GDResult { - self.send_handshake()?; - self.send_status_request()?; - self.send_ping_request()?; - - let socket_data = self.receive()?; - let mut buffer = Buffer::::new(&socket_data); - - if get_varint(&mut buffer)? != 0 { - // first var int is the packet id - return Err(PacketBad.context("Expected 0")); - } - - let json_response = get_string(&mut buffer)?; - let value_response: Value = serde_json::from_str(&json_response).map_err(|e| JsonParse.context(e))?; - - let game_version = value_response["version"]["name"] - .as_str() - .ok_or(PacketBad)? - .to_string(); - let protocol_version = value_response["version"]["protocol"] - .as_i64() - .ok_or(PacketBad)? as i32; - - let max_players = value_response["players"]["max"].as_u64().ok_or(PacketBad)? as u32; - let online_players = value_response["players"]["online"] - .as_u64() - .ok_or(PacketBad)? as u32; - let players: Option> = match value_response["players"]["sample"].is_null() { - true => None, - false => { - Some({ - let players_values = value_response["players"]["sample"] - .as_array() - .ok_or(PacketBad)?; - - let mut players = Vec::with_capacity(players_values.len()); - for player in players_values { - players.push(Player { - name: player["name"].as_str().ok_or(PacketBad)?.to_string(), - id: player["id"].as_str().ok_or(PacketBad)?.to_string(), - }); - } - - players - }) - } - }; - - Ok(JavaResponse { - game_version, - protocol_version, - players_maximum: max_players, - players_online: online_players, - players, - description: value_response["description"].to_string(), - favicon: value_response["favicon"].as_str().map(str::to_string), - previews_chat: value_response["previewsChat"].as_bool(), - enforces_secure_chat: value_response["enforcesSecureChat"].as_bool(), - server_type: Server::Java, - }) - } - - pub fn query( - address: &SocketAddr, - timeout_settings: Option, - request_settings: Option, - ) -> GDResult { - Self::new(address, timeout_settings, request_settings)?.get_info() - } -} diff --git a/crates/lib/src/games/minecraft/protocol/legacy_v1_4.rs b/crates/lib/src/games/minecraft/protocol/legacy_v1_4.rs deleted file mode 100644 index 5c71d25..0000000 --- a/crates/lib/src/games/minecraft/protocol/legacy_v1_4.rs +++ /dev/null @@ -1,83 +0,0 @@ -use byteorder::BigEndian; - -use crate::minecraft::protocol::legacy_v1_6::LegacyV1_6; -use crate::{ - buffer::{Buffer, Utf16Decoder}, - games::minecraft::{JavaResponse, LegacyGroup, Server}, - protocols::types::TimeoutSettings, - socket::{Socket, TcpSocket}, - utils::{error_by_expected_size, retry_on_timeout}, - GDErrorKind::{PacketBad, ProtocolFormat}, - GDResult, -}; -use std::net::SocketAddr; - -pub struct LegacyV1_4 { - socket: TcpSocket, - retry_count: usize, -} - -impl LegacyV1_4 { - fn new(address: &SocketAddr, timeout_settings: Option) -> GDResult { - let socket = TcpSocket::new(address, &timeout_settings)?; - - let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings); - Ok(Self { - socket, - retry_count, - }) - } - - fn send_initial_request(&mut self) -> GDResult<()> { self.socket.send(&[0xFE, 0x01]) } - - /// Send info request and parse response. - /// This function will retry fetch on timeouts. - fn get_info(&mut self) -> GDResult { - retry_on_timeout(self.retry_count, move || self.get_info_impl()) - } - - /// Send info request and parse response (without retry logic). - fn get_info_impl(&mut self) -> GDResult { - self.send_initial_request()?; - - let data = self.socket.receive(None)?; - let mut buffer = Buffer::::new(&data); - - if buffer.read::()? != 0xFF { - return Err(ProtocolFormat.context("Expected 0xFF")); - } - - let length = buffer.read::()? * 2; - error_by_expected_size((length + 3) as usize, data.len())?; - - if LegacyV1_6::is_protocol(&mut buffer)? { - return LegacyV1_6::get_response(&mut buffer); - } - - let packet_string = buffer.read_string::>(None)?; - - let split: Vec<&str> = packet_string.split('§').collect(); - error_by_expected_size(3, split.len())?; - - let description = split[0].to_string(); - let online_players = split[1].parse().map_err(|e| PacketBad.context(e))?; - let max_players = split[2].parse().map_err(|e| PacketBad.context(e))?; - - Ok(JavaResponse { - game_version: "1.4+".to_string(), - protocol_version: -1, - players_maximum: max_players, - players_online: online_players, - players: None, - description, - favicon: None, - previews_chat: None, - enforces_secure_chat: None, - server_type: Server::Legacy(LegacyGroup::V1_4), - }) - } - - pub fn query(address: &SocketAddr, timeout_settings: Option) -> GDResult { - Self::new(address, timeout_settings)?.get_info() - } -} diff --git a/crates/lib/src/games/minecraft/protocol/legacy_v1_6.rs b/crates/lib/src/games/minecraft/protocol/legacy_v1_6.rs deleted file mode 100644 index 419c288..0000000 --- a/crates/lib/src/games/minecraft/protocol/legacy_v1_6.rs +++ /dev/null @@ -1,116 +0,0 @@ -use byteorder::BigEndian; - -use crate::{ - buffer::{Buffer, Utf16Decoder}, - games::minecraft::{JavaResponse, LegacyGroup, Server}, - protocols::types::TimeoutSettings, - socket::{Socket, TcpSocket}, - utils::{error_by_expected_size, retry_on_timeout}, - GDErrorKind::{PacketBad, ProtocolFormat}, - GDResult, -}; -use std::net::SocketAddr; - -pub struct LegacyV1_6 { - socket: TcpSocket, - retry_count: usize, -} - -impl LegacyV1_6 { - fn new(address: &SocketAddr, timeout_settings: Option) -> GDResult { - let socket = TcpSocket::new(address, &timeout_settings)?; - - let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings); - Ok(Self { - socket, - retry_count, - }) - } - - fn send_initial_request(&mut self) -> GDResult<()> { - self.socket.send(&[ - 0xfe, // Packet ID (FE) - 0x01, // Ping payload (01) - 0xfa, // Packet identifier for plugin message - 0x00, 0x07, // Length of 'GameDig' string (7) as unsigned short - 0x00, 0x47, 0x00, 0x61, 0x00, 0x6D, 0x00, 0x65, 0x00, 0x44, 0x00, 0x69, 0x00, - 0x67, // 'GameDig' string as UTF-16BE - ])?; - - Ok(()) - } - - pub(crate) fn is_protocol(buffer: &mut Buffer) -> GDResult { - let state = buffer - .remaining_bytes() - .starts_with(&[0x00, 0xA7, 0x00, 0x31, 0x00, 0x00]); - - if state { - buffer.move_cursor(6)?; - } - - Ok(state) - } - - pub(crate) fn get_response(buffer: &mut Buffer) -> GDResult { - // This is a specific order! - let protocol_version = buffer - .read_string::>(None)? - .parse() - .map_err(|e| PacketBad.context(e))?; - let game_version = buffer.read_string::>(None)?; - let description = buffer.read_string::>(None)?; - let online_players = buffer - .read_string::>(None)? - .parse() - .map_err(|e| PacketBad.context(e))?; - let max_players = buffer - .read_string::>(None)? - .parse() - .map_err(|e| PacketBad.context(e))?; - - Ok(JavaResponse { - game_version, - protocol_version, - players_maximum: max_players, - players_online: online_players, - players: None, - description, - favicon: None, - previews_chat: None, - enforces_secure_chat: None, - server_type: Server::Legacy(LegacyGroup::V1_6), - }) - } - - /// Send info request and parse response. - /// This function will retry fetch on timeouts. - fn get_info(&mut self) -> GDResult { - retry_on_timeout(self.retry_count, move || self.get_info_impl()) - } - - /// Send info request and parse response (without retry logic). - fn get_info_impl(&mut self) -> GDResult { - self.send_initial_request()?; - - let data = self.socket.receive(None)?; - let mut buffer = Buffer::::new(&data); - - if buffer.read::()? != 0xFF { - return Err(ProtocolFormat.context("Expected 0xFF")); - } - - let length = buffer.read::()? * 2; - error_by_expected_size((length + 3) as usize, data.len())?; - - if !Self::is_protocol(&mut buffer)? { - return Err(ProtocolFormat.context("Not legacy 1.6 protocol")); - } - - Self::get_response(&mut buffer) - } - - pub fn query(address: &SocketAddr, timeout_settings: Option) -> GDResult { - Self::new(address, timeout_settings)?.get_info() - } -} diff --git a/crates/lib/src/games/minecraft/protocol/legacy_vb1_8.rs b/crates/lib/src/games/minecraft/protocol/legacy_vb1_8.rs deleted file mode 100644 index f96f457..0000000 --- a/crates/lib/src/games/minecraft/protocol/legacy_vb1_8.rs +++ /dev/null @@ -1,79 +0,0 @@ -use crate::{ - buffer::{Buffer, Utf16Decoder}, - games::minecraft::{JavaResponse, LegacyGroup, Server}, - protocols::types::TimeoutSettings, - socket::{Socket, TcpSocket}, - utils::{error_by_expected_size, retry_on_timeout}, - GDErrorKind::{PacketBad, ProtocolFormat}, - GDResult, -}; - -use std::net::SocketAddr; - -use byteorder::BigEndian; - -pub struct LegacyVB1_8 { - socket: TcpSocket, - retry_count: usize, -} - -impl LegacyVB1_8 { - fn new(address: &SocketAddr, timeout_settings: Option) -> GDResult { - let socket = TcpSocket::new(address, &timeout_settings)?; - - let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings); - Ok(Self { - socket, - retry_count, - }) - } - - fn send_initial_request(&mut self) -> GDResult<()> { self.socket.send(&[0xFE]) } - - /// Send request for info and parse response. - /// This function will retry fetch on timeouts. - fn get_info(&mut self) -> GDResult { - retry_on_timeout(self.retry_count, move || self.get_info_impl()) - } - - /// Send request for info and parse response (without retry logic). - fn get_info_impl(&mut self) -> GDResult { - self.send_initial_request()?; - - let data = self.socket.receive(None)?; - let mut buffer = Buffer::::new(&data); - - if buffer.read::()? != 0xFF { - return Err(ProtocolFormat.context("Expected 0xFF")); - } - - let length = buffer.read::()? * 2; - error_by_expected_size((length + 3) as usize, data.len())?; - - let packet_string = buffer.read_string::>(None)?; - - let split: Vec<&str> = packet_string.split('§').collect(); - error_by_expected_size(3, split.len())?; - - let description = split[0].to_string(); - let online_players = split[1].parse().map_err(|e| PacketBad.context(e))?; - let max_players = split[2].parse().map_err(|e| PacketBad.context(e))?; - - Ok(JavaResponse { - game_version: "Beta 1.8+".to_string(), - protocol_version: -1, - players_maximum: max_players, - players_online: online_players, - players: None, - description, - favicon: None, - previews_chat: None, - enforces_secure_chat: None, - server_type: Server::Legacy(LegacyGroup::VB1_8), - }) - } - - pub fn query(address: &SocketAddr, timeout_settings: Option) -> GDResult { - Self::new(address, timeout_settings)?.get_info() - } -} diff --git a/crates/lib/src/games/minecraft/protocol/mod.rs b/crates/lib/src/games/minecraft/protocol/mod.rs deleted file mode 100644 index 5beaeba..0000000 --- a/crates/lib/src/games/minecraft/protocol/mod.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::games::minecraft::types::RequestSettings; -use crate::{ - games::minecraft::{ - protocol::{ - bedrock::Bedrock, - java::Java, - legacy_v1_4::LegacyV1_4, - legacy_v1_6::LegacyV1_6, - legacy_vb1_8::LegacyVB1_8, - }, - BedrockResponse, - JavaResponse, - LegacyGroup, - }, - protocols::types::TimeoutSettings, - GDErrorKind::AutoQuery, - GDResult, -}; -use std::net::SocketAddr; - -mod bedrock; -mod java; -mod legacy_v1_4; -mod legacy_v1_6; -mod legacy_vb1_8; - -/// Queries a Minecraft server with all the protocol variants one by one (Java -/// -> Bedrock -> Legacy (1.6 -> 1.4 -> Beta 1.8)). -pub fn query( - address: &SocketAddr, - timeout_settings: Option, - request_settings: Option, -) -> GDResult { - if let Ok(response) = query_java(address, timeout_settings, request_settings) { - return Ok(response); - } - - if let Ok(response) = query_bedrock(address, timeout_settings) { - return Ok(JavaResponse::from_bedrock_response(response)); - } - - if let Ok(response) = query_legacy(address, timeout_settings) { - return Ok(response); - } - - Err(AutoQuery.into()) -} - -/// Query a Java Server. -pub fn query_java( - address: &SocketAddr, - timeout_settings: Option, - request_settings: Option, -) -> GDResult { - Java::query(address, timeout_settings, request_settings) -} - -/// Query a (Java) Legacy Server (1.6 -> 1.4 -> Beta 1.8). -pub fn query_legacy(address: &SocketAddr, timeout_settings: Option) -> GDResult { - if let Ok(response) = query_legacy_specific(LegacyGroup::V1_6, address, timeout_settings) { - return Ok(response); - } - - if let Ok(response) = query_legacy_specific(LegacyGroup::V1_4, address, timeout_settings) { - return Ok(response); - } - - if let Ok(response) = query_legacy_specific(LegacyGroup::VB1_8, address, timeout_settings) { - return Ok(response); - } - - Err(AutoQuery.into()) -} - -/// Query a specific (Java) Legacy Server. -pub fn query_legacy_specific( - group: LegacyGroup, - address: &SocketAddr, - timeout_settings: Option, -) -> GDResult { - match group { - LegacyGroup::V1_6 => LegacyV1_6::query(address, timeout_settings), - LegacyGroup::V1_4 => LegacyV1_4::query(address, timeout_settings), - LegacyGroup::VB1_8 => LegacyVB1_8::query(address, timeout_settings), - } -} - -/// Query a Bedrock Server. -pub fn query_bedrock(address: &SocketAddr, timeout_settings: Option) -> GDResult { - Bedrock::query(address, timeout_settings) -} diff --git a/crates/lib/src/games/minecraft/types.rs b/crates/lib/src/games/minecraft/types.rs deleted file mode 100644 index aa15c8a..0000000 --- a/crates/lib/src/games/minecraft/types.rs +++ /dev/null @@ -1,337 +0,0 @@ -// Although its a lightly modified version, this file contains code -// by Jaiden Bernard (2021-2022 - MIT) from -// https://github.com/thisjaiden/golden_apple/blob/master/src/lib.rs - -use crate::{ - buffer::Buffer, - protocols::{ - types::{CommonPlayer, CommonResponse, ExtraRequestSettings, GenericPlayer}, - GenericResponse, - }, - GDErrorKind::{InvalidInput, PacketBad, UnknownEnumCast}, - GDResult, -}; - -use byteorder::ByteOrder; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -/// The type of Minecraft Server you want to query. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum Server { - /// Java Edition. - Java, - /// Legacy Java. - Legacy(LegacyGroup), - /// Bedrock Edition. - Bedrock, -} - -/// Legacy Java (Versions) Groups. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum LegacyGroup { - /// 1.6 - V1_6, - /// 1.4 - 1.5 - V1_4, - /// Beta 1.8 - 1.3 - VB1_8, -} - -/// Information about a player. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct Player { - pub name: String, - pub id: String, -} - -impl CommonPlayer for Player { - fn as_original(&self) -> GenericPlayer<'_> { GenericPlayer::Minecraft(self) } - - fn name(&self) -> &str { &self.name } -} - -/// Versioned response type -#[cfg_attr(feature = "serde", derive(Serialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum VersionedResponse<'a> { - Bedrock(&'a BedrockResponse), - Java(&'a JavaResponse), -} - -/// A Java query response. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct JavaResponse { - /// Version name, example: "1.19.2". - pub game_version: String, - /// Protocol version, example: 760 (for 1.19.1 or 1.19.2). - /// Note that for versions below 1.6 this field is always -1. - pub protocol_version: i32, - /// Number of server capacity. - pub players_maximum: u32, - /// Number of online players. - pub players_online: u32, - /// Some online players (can be missing). - pub players: Option>, - /// Server's description or MOTD. - pub description: String, - /// The favicon (can be missing). - pub favicon: Option, - /// Tells if the chat preview is enabled (can be missing). - pub previews_chat: Option, - /// Tells if secure chat is enforced (can be missing). - pub enforces_secure_chat: Option, - /// Tell's the server type. - pub server_type: Server, -} - -/// Java-only additional request settings. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct RequestSettings { - /// Some Minecraft servers do not respond as expected if this - /// isn't a specific value, `mc.hypixel.net` is an example. - pub hostname: String, - /// Specifies the client [protocol version number](https://wiki.vg/Protocol_version_numbers), - /// `-1` means anything. - pub protocol_version: i32, -} - -impl Default for RequestSettings { - /// `hostname`: "gamedig" - /// `protocol_version`: -1 - fn default() -> Self { - Self { - hostname: "gamedig".to_string(), - protocol_version: -1, - } - } -} - -impl RequestSettings { - /// Make a new *RequestSettings* with just the hostname, the protocol - /// version defaults to -1 - pub const fn new_just_hostname(hostname: String) -> Self { - Self { - hostname, - protocol_version: -1, - } - } -} - -impl From for RequestSettings { - fn from(value: ExtraRequestSettings) -> Self { - let default = Self::default(); - Self { - hostname: value.hostname.unwrap_or(default.hostname), - protocol_version: value.protocol_version.unwrap_or(default.protocol_version), - } - } -} - -impl CommonResponse for JavaResponse { - fn as_original(&self) -> GenericResponse<'_> { GenericResponse::Minecraft(VersionedResponse::Java(self)) } - - fn description(&self) -> Option<&str> { Some(&self.description) } - fn players_maximum(&self) -> u32 { self.players_maximum } - fn players_online(&self) -> u32 { self.players_online } - fn game_version(&self) -> Option<&str> { Some(&self.game_version) } - - fn players(&self) -> Option> { - self.players - .as_ref() - .map(|players| players.iter().map(|p| p as &dyn CommonPlayer).collect()) - } -} - -/// A Bedrock Edition query response. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct BedrockResponse { - /// Server's edition. - pub edition: String, - /// Server's name. - pub name: String, - /// Version name, example: "1.19.40". - pub version_name: String, - /// Protocol version, example: 760 (for 1.19.2). - pub protocol_version: String, - /// Maximum number of players the server reports it can hold. - pub players_maximum: u32, - /// Number of players on the server. - pub players_online: u32, - /// Server id. - pub id: Option, - /// Currently running map's name. - pub map: Option, - /// Current game mode. - pub game_mode: Option, - /// Tells the server type. - pub server_type: Server, -} - -impl CommonResponse for BedrockResponse { - fn as_original(&self) -> GenericResponse<'_> { GenericResponse::Minecraft(VersionedResponse::Bedrock(self)) } - - fn name(&self) -> Option<&str> { Some(&self.name) } - fn map(&self) -> Option<&str> { self.map.as_deref() } - fn game_version(&self) -> Option<&str> { Some(&self.version_name) } - fn players_maximum(&self) -> u32 { self.players_maximum } - fn players_online(&self) -> u32 { self.players_online } -} - -impl JavaResponse { - pub fn from_bedrock_response(response: BedrockResponse) -> Self { - Self { - game_version: response.version_name, - protocol_version: 0, - players_maximum: response.players_maximum, - players_online: response.players_online, - players: None, - description: response.name, - favicon: None, - previews_chat: None, - enforces_secure_chat: None, - server_type: Server::Bedrock, - } - } -} - -/// A server's game mode (used only by Bedrock servers. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum GameMode { - Survival, - Creative, - Hardcore, - Spectator, - Adventure, -} - -impl GameMode { - pub fn from_bedrock(value: &&str) -> GDResult { - match *value { - "Survival" => Ok(Self::Survival), - "Creative" => Ok(Self::Creative), - "Hardcore" => Ok(Self::Hardcore), - "Spectator" => Ok(Self::Spectator), - "Adventure" => Ok(Self::Adventure), - _ => Err(UnknownEnumCast.context(format!("Unknown gamemode {value:?}"))), - } - } -} - -pub(crate) fn get_varint(buffer: &mut Buffer) -> GDResult { - let mut result = 0; - - let msb: u8 = 0b1000_0000; - let mask: u8 = !msb; - - for i in 0 .. 5 { - let current_byte = buffer.read::()?; - - result |= ((current_byte & mask) as i32) << (7 * i); - - // The 5th byte is only allowed to have the 4 smallest bits set - if i == 4 && (current_byte & 0xf0 != 0) { - return Err(PacketBad.context("Bad 5th byte")); - } - - if (current_byte & msb) == 0 { - break; - } - } - - Ok(result) -} - -pub(crate) fn as_varint(value: i32) -> Vec { - let mut bytes = vec![]; - let mut reading_value = value; - - let msb: u8 = 0b1000_0000; - let mask: i32 = 0b0111_1111; - - for _ in 0 .. 5 { - let tmp = (reading_value & mask) as u8; - - reading_value &= !mask; - reading_value = reading_value.rotate_right(7); - - if reading_value == 0 { - bytes.push(tmp); - break; - } - - bytes.push(tmp | msb); - } - - bytes -} - -pub(crate) fn get_string(buffer: &mut Buffer) -> GDResult { - let length = get_varint(buffer)? as usize; - let mut text = Vec::with_capacity(length); - - for _ in 0 .. length { - text.push(buffer.read::()?) - } - - String::from_utf8(text).map_err(|e| PacketBad.context(e)) -} - -pub(crate) fn as_string(value: &str) -> GDResult> { - let length = value - .len() - .try_into() - .map_err(|e| InvalidInput.context(e))?; - let mut buf = as_varint(length); - buf.extend(value.as_bytes()); - - Ok(buf) -} - -#[cfg(test)] -mod tests { - use super::{as_string, as_varint, get_varint}; - use crate::buffer::Buffer; - use crate::minecraft::get_string; - use byteorder::LittleEndian; - - #[test] - fn int_as_varint() { - assert_eq!(as_varint(1), [1]); - assert_eq!(as_varint(25565), [221, 199, 1]); - assert_eq!(as_varint(1298923567), [175, 128, 176, 235, 4]); - } - - #[test] - fn varint_as_int() { - let mut buffer = Buffer::::new(&[1, 127, 221, 199, 1, 0]); - assert_eq!(get_varint(&mut buffer), Ok(1)); - assert_eq!(get_varint(&mut buffer), Ok(127)); - assert_eq!(get_varint(&mut buffer), Ok(25565)); - assert_eq!(buffer.remaining_bytes(), [0]); - } - - #[test] - fn string_as_minecraft_string() { - assert_eq!(as_string("A"), Ok(vec![1, 65])); - assert_eq!( - as_string("VarString"), - Ok(vec![9, 86, 97, 114, 83, 116, 114, 105, 110, 103]) - ); - } - - #[test] - fn minecraft_get_string() { - let mut buffer = Buffer::::new(&[3, 65, 65, 65, 1, 66]); - assert_eq!(get_string(&mut buffer), Ok("AAA".to_string())); - assert_eq!(get_string(&mut buffer), Ok("B".to_string())); - assert_eq!(buffer.remaining_length(), 0); - } -} diff --git a/crates/lib/src/games/minetest/mod.rs b/crates/lib/src/games/minetest/mod.rs deleted file mode 100644 index bf33372..0000000 --- a/crates/lib/src/games/minetest/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -/// The implementation. -/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/minetest.js) -pub mod protocol; -/// All types used by the implementation. -pub mod types; - -pub use protocol::*; -pub use types::*; diff --git a/crates/lib/src/games/minetest/protocol.rs b/crates/lib/src/games/minetest/protocol.rs deleted file mode 100644 index a3bb138..0000000 --- a/crates/lib/src/games/minetest/protocol.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::minetest::Response; -use crate::{minetest_master_server, GDErrorKind, GDResult, TimeoutSettings}; -use std::net::IpAddr; - -pub fn query(address: &IpAddr, port: Option) -> GDResult { query_with_timeout(address, port, &None) } - -pub fn query_with_timeout( - address: &IpAddr, - port: Option, - timeout_settings: &Option, -) -> GDResult { - let address = address.to_string(); - let port = port.unwrap_or(30000); - - let servers = minetest_master_server::query(timeout_settings.unwrap_or_default())?; - for server in servers.list { - if server.ip == address && server.port == port { - return Ok(server.into()); - } - } - - Err(GDErrorKind::AutoQuery.context("Server not found in the master query list.")) -} diff --git a/crates/lib/src/games/minetest/types.rs b/crates/lib/src/games/minetest/types.rs deleted file mode 100644 index 3055c07..0000000 --- a/crates/lib/src/games/minetest/types.rs +++ /dev/null @@ -1,108 +0,0 @@ -use crate::minetest_master_server::Server; -use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer}; -use crate::protocols::GenericResponse; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct Player { - pub name: String, -} - -impl CommonPlayer for Player { - fn as_original(&self) -> GenericPlayer { GenericPlayer::Minetest(self) } - - fn name(&self) -> &str { &self.name } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct Response { - pub name: String, - pub description: String, - pub game_version: String, - pub players_maximum: u32, - pub players_online: u32, - pub has_password: Option, - pub players: Vec, - pub id: String, - pub ip: String, - pub port: u16, - pub creative: Option, - pub damage: bool, - pub game_time: u32, - pub lag: Option, - pub proto_max: u16, - pub proto_min: u16, - pub pvp: bool, - pub uptime: u32, - pub url: Option, - pub update_time: u32, - pub start: u32, - pub clients_top: u32, - pub updates: u32, - pub pop_v: f32, - pub geo_continent: Option, - pub ping: f32, -} - -impl From for Response { - fn from(server: Server) -> Self { - Self { - name: server.name, - description: server.description, - game_version: server.version, - players_maximum: server.clients_max, - players_online: server.total_clients, - has_password: server.password, - players: server - .clients_list - .unwrap_or_default() - .into_iter() - .map(|name| Player { name }) - .collect(), - ip: server.address, - creative: server.creative, - damage: server.damage, - game_time: server.game_time, - id: server.gameid, - lag: server.lag, - port: server.port, - proto_max: server.proto_max, - proto_min: server.proto_min, - pvp: server.pvp, - uptime: server.uptime, - url: server.url, - update_time: server.update_time, - start: server.start, - clients_top: server.clients_top, - updates: server.updates, - pop_v: server.pop_v, - geo_continent: server.geo_continent, - ping: server.ping, - } - } -} - -impl CommonResponse for Response { - fn as_original(&self) -> GenericResponse { GenericResponse::Minetest(self) } - - fn name(&self) -> Option<&str> { Some(&self.name) } - - fn description(&self) -> Option<&str> { Some(&self.description) } - - fn game_version(&self) -> Option<&str> { Some(&self.game_version) } - - fn players_maximum(&self) -> u32 { self.players_maximum } - - fn players_online(&self) -> u32 { self.players_online } - - fn has_password(&self) -> Option { self.has_password } - - fn players(&self) -> Option> { - Some( - self.players - .iter() - .map(|p| p as &dyn CommonPlayer) - .collect(), - ) - } -} diff --git a/crates/lib/src/games/mod.rs b/crates/lib/src/games/mod.rs deleted file mode 100644 index 1be8b5d..0000000 --- a/crates/lib/src/games/mod.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Currently supported games. - -#[cfg(feature = "tls")] -pub mod epic; -pub mod gamespy; -pub mod quake; -pub mod unreal2; -pub mod valve; - -#[cfg(all(feature = "tls", feature = "serde", feature = "services"))] -pub mod minetest; - -#[cfg(feature = "tls")] -pub use epic::*; -pub use gamespy::*; -pub use quake::*; -pub use unreal2::*; -pub use valve::*; - -#[cfg(all(feature = "tls", feature = "serde", feature = "services"))] -pub use minetest::*; - -/// Battalion 1944 -pub mod battalion1944; -/// Eco -pub mod eco; -/// Frontlines: Fuel of War -pub mod ffow; -/// Just Cause 2: Multiplayer -pub mod jc2m; -/// Mindustry -pub mod mindustry; -/// Minecraft -pub mod minecraft; -/// Savage 2 -pub mod savage2; -/// The Ship -pub mod theship; - -pub mod types; -pub use types::*; - -pub mod query; -pub use query::*; - -#[cfg(feature = "game_defs")] -mod definitions; - -#[cfg(feature = "game_defs")] -pub use definitions::GAMES; diff --git a/crates/lib/src/games/quake.rs b/crates/lib/src/games/quake.rs deleted file mode 100644 index dff9d79..0000000 --- a/crates/lib/src/games/quake.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Quake game query modules - -use crate::protocols::quake::game_query_mod; - -game_query_mod!(quake1, "Quake 1", one, 27500); -game_query_mod!(quake2, "Quake 2", two, 27910); -game_query_mod!(q3a, "Quake 3 Arena", three, 27960); -game_query_mod!(sof2, "Soldier of Fortune 2", three, 20100); -game_query_mod!(warsow, "Warsow", three, 44400); diff --git a/crates/lib/src/games/query.rs b/crates/lib/src/games/query.rs deleted file mode 100644 index 8968683..0000000 --- a/crates/lib/src/games/query.rs +++ /dev/null @@ -1,137 +0,0 @@ -//! Generic query functions - -use std::net::{IpAddr, SocketAddr}; - -#[cfg(all(feature = "services", feature = "tls", feature = "serde"))] -use crate::games::minetest; -use crate::games::types::Game; -use crate::games::{eco, ffow, jc2m, mindustry, minecraft, savage2, theship}; -use crate::protocols; -use crate::protocols::gamespy::GameSpyVersion; -use crate::protocols::quake::QuakeVersion; -use crate::protocols::types::{CommonResponse, ExtraRequestSettings, ProprietaryProtocol, Protocol, TimeoutSettings}; -use crate::GDResult; - -/// Make a query given a game definition -#[inline] -pub fn query(game: &Game, address: &IpAddr, port: Option) -> GDResult> { - query_with_timeout_and_extra_settings(game, address, port, None, None) -} - -/// Make a query given a game definition and timeout settings -#[inline] -pub fn query_with_timeout( - game: &Game, - address: &IpAddr, - port: Option, - timeout_settings: Option, -) -> GDResult> { - query_with_timeout_and_extra_settings(game, address, port, timeout_settings, None) -} - -/// Make a query given a game definition, timeout settings, and extra settings -pub fn query_with_timeout_and_extra_settings( - game: &Game, - address: &IpAddr, - port: Option, - timeout_settings: Option, - extra_settings: Option, -) -> GDResult> { - let socket_addr = SocketAddr::new(*address, port.unwrap_or(game.default_port)); - Ok(match &game.protocol { - Protocol::Valve(engine) => { - protocols::valve::query( - &socket_addr, - *engine, - extra_settings - .or_else(|| Option::from(game.request_settings.clone())) - .map(ExtraRequestSettings::into), - timeout_settings, - ) - .map(Box::new)? - } - #[cfg(feature = "tls")] - Protocol::Epic(credentials) => { - protocols::epic::query_with_timeout(credentials.clone(), &socket_addr, timeout_settings).map(Box::new)? - } - Protocol::Gamespy(version) => { - match version { - GameSpyVersion::One => protocols::gamespy::one::query(&socket_addr, timeout_settings).map(Box::new)?, - GameSpyVersion::Two => protocols::gamespy::two::query(&socket_addr, timeout_settings).map(Box::new)?, - GameSpyVersion::Three => { - protocols::gamespy::three::query(&socket_addr, timeout_settings).map(Box::new)? - } - } - } - Protocol::Quake(version) => { - match version { - QuakeVersion::One => protocols::quake::one::query(&socket_addr, timeout_settings).map(Box::new)?, - QuakeVersion::Two => protocols::quake::two::query(&socket_addr, timeout_settings).map(Box::new)?, - QuakeVersion::Three => protocols::quake::three::query(&socket_addr, timeout_settings).map(Box::new)?, - } - } - Protocol::Unreal2 => { - protocols::unreal2::query( - &socket_addr, - &extra_settings - .map(ExtraRequestSettings::into) - .unwrap_or_default(), - timeout_settings, - ) - .map(Box::new)? - } - Protocol::PROPRIETARY(protocol) => { - match protocol { - ProprietaryProtocol::Savage2 => { - savage2::query_with_timeout(address, port, timeout_settings).map(Box::new)? - } - ProprietaryProtocol::TheShip => { - theship::query_with_timeout(address, port, timeout_settings).map(Box::new)? - } - ProprietaryProtocol::FFOW => ffow::query_with_timeout(address, port, timeout_settings).map(Box::new)?, - ProprietaryProtocol::JC2M => jc2m::query_with_timeout(address, port, timeout_settings).map(Box::new)?, - ProprietaryProtocol::Mindustry => mindustry::query(address, port, &timeout_settings).map(Box::new)?, - ProprietaryProtocol::Minecraft(version) => { - match version { - Some(minecraft::Server::Java) => { - minecraft::protocol::query_java( - &socket_addr, - timeout_settings, - extra_settings.map(ExtraRequestSettings::into), - ) - .map(Box::new)? - } - Some(minecraft::Server::Bedrock) => { - minecraft::protocol::query_bedrock(&socket_addr, timeout_settings).map(Box::new)? - } - Some(minecraft::Server::Legacy(group)) => { - minecraft::protocol::query_legacy_specific(*group, &socket_addr, timeout_settings) - .map(Box::new)? - } - None => { - minecraft::protocol::query( - &socket_addr, - timeout_settings, - extra_settings.map(ExtraRequestSettings::into), - ) - .map(Box::new)? - } - } - } - ProprietaryProtocol::Eco => { - eco::query_with_timeout_and_extra_settings( - address, - port, - &timeout_settings, - extra_settings.map(ExtraRequestSettings::into), - ) - .map(Box::new)? - } - #[cfg(all(feature = "services", feature = "tls", feature = "serde"))] - ProprietaryProtocol::Minetest => { - minetest::query_with_timeout(address, port, &timeout_settings).map(Box::new)? - } - } - } - }) -} diff --git a/crates/lib/src/games/savage2/mod.rs b/crates/lib/src/games/savage2/mod.rs deleted file mode 100644 index 88d57f2..0000000 --- a/crates/lib/src/games/savage2/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -/// The implementation. -/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/savage2.js) -pub mod protocol; -/// All types used by the implementation. -pub mod types; - -pub use protocol::*; -pub use types::*; diff --git a/crates/lib/src/games/savage2/protocol.rs b/crates/lib/src/games/savage2/protocol.rs deleted file mode 100644 index dbb1eb8..0000000 --- a/crates/lib/src/games/savage2/protocol.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::buffer::{Buffer, Utf8Decoder}; -use crate::games::savage2::types::Response; -use crate::protocols::types::TimeoutSettings; -use crate::socket::{Socket, UdpSocket}; -use crate::GDResult; -use byteorder::LittleEndian; -use std::net::{IpAddr, SocketAddr}; - -pub fn query(address: &IpAddr, port: Option) -> GDResult { query_with_timeout(address, port, None) } - -pub fn query_with_timeout( - address: &IpAddr, - port: Option, - timeout_settings: Option, -) -> GDResult { - let addr = &SocketAddr::new(*address, port.unwrap_or(11235)); - let mut socket = UdpSocket::new(addr, &timeout_settings)?; - socket.send(&[0x01])?; - let data = socket.receive(None)?; - let mut buffer = Buffer::::new(&data); - - buffer.move_cursor(12)?; - - Ok(Response { - name: buffer.read_string::(None)?, - players_online: buffer.read::()?, - players_maximum: buffer.read::()?, - time: buffer.read_string::(None)?, - map: buffer.read_string::(None)?, - next_map: buffer.read_string::(None)?, - location: buffer.read_string::(None)?, - players_minimum: buffer.read::()?, - game_mode: buffer.read_string::(None)?, - protocol_version: buffer.read_string::(None)?, - level_minimum: buffer.read::()?, - }) -} diff --git a/crates/lib/src/games/savage2/types.rs b/crates/lib/src/games/savage2/types.rs deleted file mode 100644 index 0016bfd..0000000 --- a/crates/lib/src/games/savage2/types.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::protocols::types::CommonResponse; -use crate::protocols::GenericResponse; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Response { - pub name: String, - pub players_online: u8, - pub players_maximum: u8, - pub players_minimum: u8, - pub time: String, - pub map: String, - pub next_map: String, - pub location: String, - pub game_mode: String, - pub protocol_version: String, - pub level_minimum: u8, -} - -impl CommonResponse for Response { - fn as_original(&self) -> GenericResponse<'_> { GenericResponse::Savage2(self) } - - fn name(&self) -> Option<&str> { Some(&self.name) } - fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) } - fn map(&self) -> Option<&str> { Some(&self.map) } - fn players_maximum(&self) -> u32 { self.players_maximum.into() } - fn players_online(&self) -> u32 { self.players_online.into() } -} diff --git a/crates/lib/src/games/theship/mod.rs b/crates/lib/src/games/theship/mod.rs deleted file mode 100644 index b37a291..0000000 --- a/crates/lib/src/games/theship/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -/// The implementation. -/// Reference: [server queries](https://developer.valvesoftware.com/wiki/Server_queries) -pub mod protocol; -/// All types used by the implementation. -pub mod types; - -pub use protocol::*; -pub use types::*; diff --git a/crates/lib/src/games/theship/protocol.rs b/crates/lib/src/games/theship/protocol.rs deleted file mode 100644 index c041ceb..0000000 --- a/crates/lib/src/games/theship/protocol.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::games::theship::types::Response; -use crate::protocols::types::TimeoutSettings; -use crate::protocols::valve; -use crate::protocols::valve::Engine; -use crate::GDResult; -use std::net::{IpAddr, SocketAddr}; - -pub fn query(address: &IpAddr, port: Option) -> GDResult { query_with_timeout(address, port, None) } - -pub fn query_with_timeout( - address: &IpAddr, - port: Option, - timeout_settings: Option, -) -> GDResult { - let valve_response = valve::query( - &SocketAddr::new(*address, port.unwrap_or(27015)), - Engine::new(2400), - None, - timeout_settings, - )?; - - Response::new_from_valve_response(valve_response) -} diff --git a/crates/lib/src/games/theship/types.rs b/crates/lib/src/games/theship/types.rs deleted file mode 100644 index 8bb593c..0000000 --- a/crates/lib/src/games/theship/types.rs +++ /dev/null @@ -1,122 +0,0 @@ -use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer}; -use crate::protocols::valve::{get_optional_extracted_data, Server, ServerPlayer}; -use crate::protocols::{valve, GenericResponse}; -use crate::GDErrorKind::PacketBad; -use crate::GDResult; -use std::collections::HashMap; - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, PartialOrd)] -pub struct TheShipPlayer { - pub name: String, - pub score: i32, - pub duration: f32, - pub deaths: u32, - pub money: u32, -} - -impl TheShipPlayer { - pub fn new_from_valve_player(player: &ServerPlayer) -> GDResult { - Ok(Self { - name: player.name.clone(), - score: player.score, - duration: player.duration, - deaths: player.deaths.ok_or(PacketBad)?, - money: player.money.ok_or(PacketBad)?, - }) - } -} - -impl CommonPlayer for TheShipPlayer { - fn as_original(&self) -> GenericPlayer<'_> { GenericPlayer::TheShip(self) } - - fn name(&self) -> &str { &self.name } - fn score(&self) -> Option { Some(self.score) } -} - -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq)] -pub struct Response { - pub protocol_version: u8, - pub name: String, - pub map: String, - pub game_mode: String, - pub game_version: String, - pub players: Vec, - pub players_online: u8, - pub players_maximum: u8, - pub players_bots: u8, - pub server_type: Server, - pub has_password: bool, - pub vac_secured: bool, - pub port: Option, - pub steam_id: Option, - pub tv_port: Option, - pub tv_name: Option, - pub keywords: Option, - pub rules: HashMap, - pub mode: u8, - pub witnesses: u8, - pub duration: u8, -} - -impl CommonResponse for Response { - fn as_original(&self) -> GenericResponse<'_> { GenericResponse::TheShip(self) } - - fn name(&self) -> Option<&str> { Some(&self.name) } - fn map(&self) -> Option<&str> { Some(&self.map) } - fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) } - fn players_maximum(&self) -> u32 { self.players_maximum.into() } - fn players_online(&self) -> u32 { self.players_online.into() } - fn players_bots(&self) -> Option { Some(self.players_bots.into()) } - fn has_password(&self) -> Option { Some(self.has_password) } - - fn players(&self) -> Option> { - Some( - self.players - .iter() - .map(|p| p as &dyn CommonPlayer) - .collect(), - ) - } -} - -impl Response { - pub fn new_from_valve_response(response: valve::Response) -> GDResult { - let (port, steam_id, tv_port, tv_name, keywords) = get_optional_extracted_data(response.info.extra_data); - - let the_unwrapped_ship = response.info.the_ship.ok_or(PacketBad)?; - - Ok(Self { - protocol_version: response.info.protocol_version, - name: response.info.name, - map: response.info.map, - game_mode: response.info.game_mode, - game_version: response.info.game_version, - players_online: response.info.players_online, - players: response - .players - .ok_or(PacketBad)? - .iter() - .map(TheShipPlayer::new_from_valve_player) - .collect::>>()?, - players_maximum: response.info.players_maximum, - players_bots: response.info.players_bots, - server_type: response.info.server_type, - has_password: response.info.has_password, - vac_secured: response.info.vac_secured, - port, - steam_id, - tv_port, - tv_name, - keywords, - rules: response.rules.ok_or(PacketBad)?, - mode: the_unwrapped_ship.mode, - witnesses: the_unwrapped_ship.witnesses, - duration: the_unwrapped_ship.duration, - }) - } -} diff --git a/crates/lib/src/games/types.rs b/crates/lib/src/games/types.rs deleted file mode 100644 index 3868e21..0000000 --- a/crates/lib/src/games/types.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! Game related types - -use crate::protocols::types::{ExtraRequestSettings, Protocol}; - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -/// Definition of a game -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Game { - /// Full name of the game - pub name: &'static str, - /// Default port used by game - pub default_port: u16, - /// The protocol the game's query uses - pub protocol: Protocol, - /// Request settings. - pub request_settings: ExtraRequestSettings, -} diff --git a/crates/lib/src/games/unreal2.rs b/crates/lib/src/games/unreal2.rs deleted file mode 100644 index 1ce4585..0000000 --- a/crates/lib/src/games/unreal2.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Unreal2 game query modules - -use crate::protocols::unreal2::game_query_mod; - -game_query_mod!(darkesthour, "Darkest Hour: Europe '44-'45 (2008)", 7758); -game_query_mod!(devastation, "Devastation (2003)", 7778); -game_query_mod!(killingfloor, "Killing Floor", 7708); -game_query_mod!(redorchestra, "Red Orchestra", 7759); -game_query_mod!(ut2003, "Unreal Tournament 2003", 7758); -game_query_mod!(ut2004, "Unreal Tournament 2004", 7778); diff --git a/crates/lib/src/games/valve.rs b/crates/lib/src/games/valve.rs deleted file mode 100644 index 497700a..0000000 --- a/crates/lib/src/games/valve.rs +++ /dev/null @@ -1,201 +0,0 @@ -//! Valve game query modules - -use crate::protocols::valve::game_query_mod; - -game_query_mod!(abioticfactor, "Abiotic Factor", Engine::new(427_410), 27015); -game_query_mod!( - a2oa, - "ARMA 2: Operation Arrowhead", - Engine::new(33930), - 2304 -); -game_query_mod!(arma3, "ARMA 3", Engine::new(107_410), 2303); -game_query_mod!(basedefense, "Base Defense", Engine::new(632_730), 27015); -game_query_mod!(alienswarm, "Alien Swarm", Engine::new(630), 27015); -game_query_mod!(aoc, "Age of Chivalry", Engine::new(17510), 27015); -game_query_mod!( - aapg, - "America's Army: Proving Grounds", - Engine::new(203_290), - 27020, - GatheringSettings { - players: GatherToggle::Enforce, - rules: GatherToggle::Skip, - check_app_id: true, - } -); -game_query_mod!(ase, "ARK: Survival Evolved", Engine::new(346_110), 27015); -game_query_mod!( - asrd, - "Alien Swarm: Reactive Drop", - Engine::new(563_560), - 2304 -); -game_query_mod!(atlas, "ATLAS", Engine::new(834_910), 57561); -game_query_mod!(avorion, "Avorion", Engine::new(445_220), 27020); -game_query_mod!( - ballisticoverkill, - "Ballistic Overkill", - Engine::new(296_300), - 27016 -); -game_query_mod!( - armareforger, - "Arma Reforger", - Engine::new(0), - 17777, - GatheringSettings { - players: GatherToggle::Enforce, - rules: GatherToggle::Enforce, - check_app_id: false, - } -); -game_query_mod!( - avp2010, - "Aliens vs. Predator 2010", - Engine::new(10_680), - 27015 -); -game_query_mod!(barotrauma, "Barotrauma", Engine::new(602_960), 27016); -game_query_mod!(blackmesa, "Black Mesa", Engine::new(362_890), 27015); -game_query_mod!(brainbread2, "BrainBread 2", Engine::new(346_330), 27015); -game_query_mod!( - codbo3, - "Call Of Duty: Black Ops 3", - Engine::new(311_210), - 27017 -); -game_query_mod!(codenamecure, "Codename CURE", Engine::new(355_180), 27015); -game_query_mod!( - colonysurvival, - "Colony Survival", - Engine::new(366_090), - 27004 -); -game_query_mod!( - conanexiles, - "Conan Exiles", - Engine::new(440_900), - 27015, - GatheringSettings { - players: GatherToggle::Skip, - rules: GatherToggle::Enforce, - check_app_id: true, - } -); -game_query_mod!( - counterstrike, - "Counter-Strike", - Engine::new_gold_src(false), - 27015 -); -game_query_mod!(counterstrike2, "Counter-Strike 2", Engine::new(730), 27015); -game_query_mod!(creativerse, "Creativerse", Engine::new(280_790), 26901); -game_query_mod!( - cscz, - "Counter Strike: Condition Zero", - Engine::new_gold_src(false), - 27015 -); -game_query_mod!( - csgo, - "Counter-Strike: Global Offensive", - Engine::new(730), - 27015 -); -game_query_mod!(css, "Counter-Strike: Source", Engine::new(240), 27015); -game_query_mod!(dab, "Double Action: Boogaloo", Engine::new(317_360), 27015); -game_query_mod!(dod, "Day of Defeat", Engine::new_gold_src(false), 27015); -game_query_mod!(dods, "Day of Defeat: Source", Engine::new(300), 27015); -game_query_mod!(doi, "Day of Infamy", Engine::new(447_820), 27015); -game_query_mod!(dst, "Don't Starve Together", Engine::new(322_320), 27016); -game_query_mod!(enshrouded, "Enshrouded", Engine::new(1_203_620), 15637); -game_query_mod!(garrysmod, "Garry's Mod", Engine::new(4000), 27016); -game_query_mod!(hl2d, "Half-Life 2 Deathmatch", Engine::new(320), 27015); -game_query_mod!( - hlds, - "Half-Life Deathmatch: Source", - Engine::new(360), - 27015 -); -game_query_mod!(hll, "Hell Let Loose", Engine::new(686_810), 26420); -game_query_mod!( - imic, - "Insurgency: Modern Infantry Combat", - Engine::new(17700), - 27015 -); -game_query_mod!(insurgency, "Insurgency", Engine::new(222_880), 27015); -game_query_mod!( - insurgencysandstorm, - "Insurgency: Sandstorm", - Engine::new(581_320), - 27131 -); -game_query_mod!(l4d, "Left 4 Dead", Engine::new(500), 27015); -game_query_mod!(l4d2, "Left 4 Dead 2", Engine::new(550), 27015); -game_query_mod!( - ohd, - "Operation: Harsh Doorstop", - Engine::new_with_dedicated(736_590, 950_900), - 27005 -); -game_query_mod!(onset, "Onset", Engine::new(1_105_810), 7776); -game_query_mod!(postscriptum, "Post Scriptum", Engine::new(736_220), 10037); -game_query_mod!( - projectzomboid, - "Project Zomboid", - Engine::new(108_600), - 16261 -); -game_query_mod!(risingworld, "Rising World", Engine::new(324_080), 4254); -game_query_mod!(ror2, "Risk of Rain 2", Engine::new(632_360), 27016); -game_query_mod!(rust, "Rust", Engine::new(252_490), 27015); -game_query_mod!(sco, "Sven Co-op", Engine::new_gold_src(false), 27015); -game_query_mod!(sdtd, "7 Days to Die", Engine::new(251_570), 26900); -game_query_mod!(soulmask, "Soulmask", Engine::new(2_646_460), 27015); -game_query_mod!(squad, "Squad", Engine::new(393_380), 27165); -game_query_mod!( - starbound, - "Starbound", - Engine::new(211_820), - 21025, - GatheringSettings { - players: GatherToggle::Enforce, - rules: GatherToggle::Enforce, - check_app_id: false, - } -); -game_query_mod!(teamfortress2, "Team Fortress 2", Engine::new(440), 27015); -game_query_mod!( - tfc, - "Team Fortress Classic", - Engine::new_gold_src(false), - 27015 -); -game_query_mod!(theforest, "The Forest", Engine::new(556_450), 27016); -game_query_mod!(thefront, "The Front", Engine::new(2_285_150), 27015); -game_query_mod!(unturned, "Unturned", Engine::new(304_930), 27015); -game_query_mod!( - valheim, - "Valheim", - Engine::new(892_970), - 2457, - GatheringSettings { - players: GatherToggle::Enforce, - rules: GatherToggle::Skip, - check_app_id: true, - } -); -game_query_mod!(vrising, "V Rising", Engine::new(1_604_030), 27016); -game_query_mod!(zps, "Zombie Panic: Source", Engine::new(17_500), 27015); -game_query_mod!(moe, "Myth of Empires", Engine::new(1_371_580), 12888); -game_query_mod!(mordhau, "Mordhau", Engine::new(629_760), 27015); -game_query_mod!( - pvak2, - "Pirates, Vikings, and Knights II", - Engine::new(17_570), - 27015 -); -game_query_mod!(nla, "Nova-Life: Amboise", Engine::new(885_570), 27015); -game_query_mod!(pixark, "PixARK", Engine::new(593_600), 27015); diff --git a/crates/lib/src/http.rs b/crates/lib/src/http.rs deleted file mode 100644 index fd219fb..0000000 --- a/crates/lib/src/http.rs +++ /dev/null @@ -1,491 +0,0 @@ -//! Client for making HTTP requests. -//! -//! This is the first draft implementation: feel free to change things to suit -//! your needs. - -// Because this is first draft some functionality is not used yet. -// TODO: When this is used in more places remove this and refine the interface. -#![allow(dead_code)] - -use crate::GDErrorKind::{HostLookup, InvalidInput, PacketReceive, PacketSend, ProtocolFormat}; -use crate::{GDResult, TimeoutSettings}; - -use std::io::Read; -use std::net::{SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs}; - -use ureq::{Agent, AgentBuilder, Request}; -use url::{Host, Url}; - -use serde::{de::DeserializeOwned, Serialize}; - -/// Max length of HTTP responses in bytes: 1GB -const MAX_RESPONSE_LENGTH: usize = 1024 * 1024 * 1024; - -/// HTTP request client. Define parameters host parameters on new, then re-use -/// for each request. -/// -/// When making requests directly to the server use [HttpClient::new] as this -/// allows directly specifying the IP to connect to. -/// -/// When requests must go through an intermediatary (that we don't know the IP -/// of) use [HttpClient::from_url] which will perform a DNS lookup internally. -/// -/// For example usage see [tests]. -pub struct HttpClient { - client: Agent, - address: Url, - headers: Vec<(String, String)>, -} - -/// HttpHeaders for use with a single request. -pub type HttpHeaders<'a> = Option<&'a [(&'a str, &'a str)]>; - -/// HTTP Protocols. -/// -/// Note: if the `tls` feature is disabled this will only contain Http. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] -pub enum HttpProtocol { - #[default] - Http, - #[cfg(feature = "tls")] - Https, -} - -impl HttpProtocol { - /// Convert [Protocol] to a static str for use in a [Url]. - /// e.g. "http:" - pub const fn as_str(&self) -> &'static str { - use HttpProtocol::*; - match self { - Http => "http:", - #[cfg(feature = "tls")] - Https => "https:", - } - } -} - -/// Additional settings for HTTPClients. -/// -/// # Can be created using builder functions: -/// ```ignore, We cannot test private functionality -/// use gamedig::http::{HttpSettings, HttpProtocol}; -/// -/// let _ = HttpSettings::default() -/// .protocol(HttpProtocol::Http) -/// .hostname(String::from("test.com")) -/// .header(String::from("Authorization"), String::from("Bearer Token")); -/// ``` -#[derive(Debug, Default, Clone, Eq, PartialEq)] -pub struct HttpSettings> { - /// Choose whether to use HTTP or HTTPS. - pub protocol: HttpProtocol, - /// Choose a hostname override (used to set the [Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) header) and for TLS. - pub hostname: Option, - /// Choose HTTP headers to send with requests. - pub headers: Vec<(S, S)>, -} - -impl> HttpSettings { - /// Set the HTTP protocol (defaults to HTTP). - pub const fn protocol(mut self, protocol: HttpProtocol) -> HttpSettings { - self.protocol = protocol; - self - } - - /// Set the desired HTTP host name: used for the HTTP Host header and for - /// TLS negotiation. - pub fn hostname(mut self, hostname: S) -> HttpSettings { - self.hostname = Some(hostname); - self - } - - /// Overwrite all the current HTTP headers with new headers. - pub fn headers(mut self, headers: Vec<(S, S)>) -> HttpSettings { - self.headers = headers; - self - } - - /// Set one HTTP header value. - pub fn header(mut self, name: S, value: S) -> HttpSettings { - self.headers.push((name, value)); - self - } -} - -impl HttpClient { - /// Creates a new HTTPClient that can be used to send requests. - /// - /// # Parameters - /// - [address](SocketAddr): The IP and port the HTTP request will connect - /// to. - /// - [timeout_settings](TimeoutSettings): Used to set the connect and - /// socket timeouts for the requests. - /// - [http_settings](HttpSettings): Additional settings for the HTTPClient. - pub fn new>( - address: &SocketAddr, - timeout_settings: &Option, - http_settings: HttpSettings, - ) -> GDResult - where - Self: Sized, - { - let mut client_builder = AgentBuilder::new(); - - // Set timeout settings - let (read_timeout, write_timeout) = TimeoutSettings::get_read_and_write_or_defaults(timeout_settings); - - if let Some(read_timeout) = read_timeout { - client_builder = client_builder.timeout_read(read_timeout); - } - - if let Some(write_timeout) = write_timeout { - client_builder = client_builder.timeout_write(write_timeout); - } - - if let Some(connect_timeout) = TimeoutSettings::get_connect_or_default(timeout_settings) { - client_builder = client_builder.timeout_connect(connect_timeout); - } - - // Every request sent from this client will connect to the address set - { - let address = *address; - client_builder = client_builder.resolver(move |_: &str| Ok(vec![address])); - } - - // Set a friendly user-agent string - client_builder = client_builder.user_agent(concat!( - env!("CARGO_PKG_NAME"), - "/", - env!("CARGO_PKG_VERSION") - )); - - let client = client_builder.build(); - - let host = http_settings - .hostname - .map(S::into) - .unwrap_or_else(|| address.ip().to_string()); - - Ok(Self { - client, - // TODO: Use Url from_parts if it gets added - address: Url::parse(&format!( - "{}//{}:{}", - http_settings.protocol.as_str(), - host, - address.port() - )) - .map_err(|e| InvalidInput.context(e))?, - headers: http_settings - .headers - .into_iter() - .map(|(k, v)| (k.into(), v.into())) - .collect(), - }) - } - - /// Create a new HTTP client from a pre-existing URL, performing a DNS - /// lookup on the host when necessary. - /// - /// This is aimed to be used when we know the domain of the server but not - /// the IP i.e. when the server is not the service being directly queried - /// for server info. - pub fn from_url>( - url: U, - timeout_settings: &Option, - headers: Option>, - ) -> GDResult - where - U::Error: std::error::Error + Send + Sync + 'static, - { - let url: Url = url.try_into().map_err(|e| InvalidInput.context(e))?; - - let host = url - .host() - .ok_or_else(|| InvalidInput.context("URL used to create a HTTPClient must have a host"))?; - let port = url - .port_or_known_default() - .ok_or_else(|| InvalidInput.context("URL used to create HttpClient must have a port"))?; - - let address = match host { - Host::Ipv4(ip) => SocketAddr::V4(SocketAddrV4::new(ip, port)), - Host::Ipv6(ip) => SocketAddr::V6(SocketAddrV6::new(ip, port, 0, 0)), - Host::Domain(domain) => { - format!("{domain}:{port}") - .to_socket_addrs() - .map_err(|e| HostLookup.context(e))? - .next() - .ok_or_else(|| HostLookup.context("No socket addresses found for host"))? - } - }; - - let http_settings = HttpSettings { - hostname: url.host_str(), - protocol: match url.scheme() { - #[cfg(feature = "tls")] - "https" => HttpProtocol::Https, - _ => HttpProtocol::Http, - }, - headers: headers.unwrap_or_default(), - }; - - Self::new(&address, timeout_settings, http_settings) - } - - /// Send a HTTP GET request and return the response data as a buffer. - pub fn get(&mut self, path: &str, headers: HttpHeaders) -> GDResult> { self.request("GET", path, headers) } - - /// Send a HTTP GET request and parse the JSON resonse. - pub fn get_json(&mut self, path: &str, headers: HttpHeaders) -> GDResult { - self.request_json("GET", path, headers) - } - - /// Send a HTTP Post request with JSON data and parse a JSON response. - pub fn post_json( - &mut self, - path: &str, - headers: HttpHeaders, - data: S, - ) -> GDResult { - self.request_with_json_data("POST", path, headers, data) - } - - /// Send a HTTP Post request with FORM data and parse a JSON response. - pub fn post_json_with_form( - &mut self, - path: &str, - headers: HttpHeaders, - data: &[(&str, &str)], - ) -> GDResult { - self.request_with_form_data("POST", path, headers, data) - } - - // NOTE: More methods can be added here as required using the request_json or - // request_with_json methods - - fn make_request(&self, method: &str, headers: HttpHeaders) -> Request { - let mut request = self.client.request_url(method, &self.address); - - // Set the request headers. - for (key, value) in self.headers.iter() { - request = request.set(key, value); - } - - if let Some(headers) = headers { - for (key, value) in headers { - request = request.set(key, value); - } - } - - request - } - - /// Internal request method, makes a request with an arbitrary HTTP method. - #[inline] - fn request(&mut self, method: &str, path: &str, headers: HttpHeaders) -> GDResult> { - // Append the path to the pre-parsed URL and create a request object. - self.address.set_path(path); - let request = self.make_request(method, headers); - - // Send the request. - let http_response = request.call().map_err(|e| PacketSend.context(e))?; - - let length = if let Some(length) = http_response.header("Content-Length") { - length - .parse::() - .map_err(|e| ProtocolFormat.context(e))? - .min(MAX_RESPONSE_LENGTH) - } else { - 5012 // Sensible default allocation - }; - - let mut buffer: Vec = Vec::with_capacity(length); - - let _ = http_response - .into_reader() - .take(MAX_RESPONSE_LENGTH as u64) - .read_to_end(&mut buffer) - .map_err(|e| PacketReceive.context(e))?; - - Ok(buffer) - } - - /// Send a HTTP request without any data and parse the JSON response. - #[inline] - fn request_json(&mut self, method: &str, path: &str, headers: HttpHeaders) -> GDResult { - // Append the path to the pre-parsed URL and create a request object. - self.address.set_path(path); - let request = self.make_request(method, headers); - - // Send the request and parse the response as JSON. - request - .call() - .map_err(|e| PacketSend.context(e))? - .into_json::() - .map_err(|e| ProtocolFormat.context(e)) - } - - /// Send a HTTP request with JSON data and parse the JSON response. - #[inline] - fn request_with_json_data( - &mut self, - method: &str, - path: &str, - headers: HttpHeaders, - data: S, - ) -> GDResult { - self.address.set_path(path); - let request = self.make_request(method, headers); - - request - .send_json(data) - .map_err(|e| PacketSend.context(e))? - .into_json::() - .map_err(|e| ProtocolFormat.context(e)) - } - - /// Send a HTTP request with FORM data and parse the JSON response. - #[inline] - fn request_with_form_data( - &mut self, - method: &str, - path: &str, - headers: HttpHeaders, - data: &[(&str, &str)], - ) -> GDResult { - self.address.set_path(path); - let request = self.make_request(method, headers); - - request - .send_form(data) - .map_err(|e| PacketSend.context(e))? - .into_json::() - .map_err(|e| ProtocolFormat.context(e)) - } -} - -#[cfg(test)] -mod tests { - use std::net::{Ipv4Addr, SocketAddrV4, ToSocketAddrs}; - - use super::*; - - #[test] - fn http_settings_builder() { - const HOSTNAME: &str = "example.org"; - - #[cfg(feature = "tls")] - const PROTOCOL: HttpProtocol = HttpProtocol::Https; - #[cfg(not(feature = "tls"))] - const PROTOCOL: HttpProtocol = HttpProtocol::Http; - - let settings = HttpSettings::default() - .hostname(HOSTNAME) - .protocol(PROTOCOL) - .header("Gamedig", "Is Awesome") - .headers(vec![("Foo", "bar")]) - .header("Baz", "Buzz"); - - assert_eq!(settings.hostname, Some(HOSTNAME)); - assert_eq!(settings.protocol, PROTOCOL); - assert_eq!(settings.headers, vec![("Foo", "bar"), ("Baz", "Buzz"),]); - } - - #[test] - fn http_client_new() { - const PROTOCOL: HttpProtocol = HttpProtocol::Http; - - const ADDRESS: SocketAddr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8000)); - - let settings = HttpSettings { - protocol: PROTOCOL, - hostname: Some("github.com"), - headers: vec![("Authorization", "UUDDLRLRBA")], - }; - - let client = HttpClient::new(&ADDRESS, &None, settings).unwrap(); - - assert_eq!(client.address.as_str(), "http://github.com:8000/"); - assert_eq!( - client.headers, - vec![(String::from("Authorization"), String::from("UUDDLRLRBA")),] - ); - } - - #[cfg(feature = "tls")] - #[test] - #[ignore = "HTTP requests won't work without internet"] - fn https_json_get_request() { - let address = "api.github.com:443" - .to_socket_addrs() - .unwrap() - .next() - .unwrap(); - - let settings = HttpSettings::default() - .protocol(HttpProtocol::Https) - .hostname("api.github.com"); - - let mut client = HttpClient::new(&address, &None, settings).unwrap(); - - let response: serde_json::Value = client.get_json("/events", None).unwrap(); - - println!("{:?}", response); - } - - #[test] - #[ignore = "HTTP requests won't work without internet"] - fn http_json_get_request() { - let address = "postman-echo.com:80" - .to_socket_addrs() - .unwrap() - .next() - .unwrap(); - - let settings = HttpSettings::default().hostname("postman-echo.com"); - - let mut client = HttpClient::new(&address, &None, settings).unwrap(); - - let response: serde_json::Value = client.get_json("/get", None).unwrap(); - - println!("{:?}", response); - } - - #[test] - #[ignore = "HTTP requests won't work without internet"] - fn http_get_request() { - let address = "ifconfig.me:80".to_socket_addrs().unwrap().next().unwrap(); - - let settings = HttpSettings::default() - .hostname("ifconfig.me") - .header("User-Agent", "Curl/8.6.0"); - - let mut client = HttpClient::new(&address, &None, settings).unwrap(); - - let response = client.get("/", None).unwrap(); - - println!("{:?}", std::str::from_utf8(&response)); - } - - #[test] - #[ignore = "HTTP requests won't work without internet"] - fn http_get_from_url() { - let mut client = HttpClient::from_url("http://postman-echo.com/path-is-ignored", &None, None).unwrap(); - - let response: serde_json::Value = client.get_json("/get", None).unwrap(); - - println!("{:?}", response); - } - - #[test] - #[ignore = "HTTP requests won't work without internet"] - fn http_get_from_url_parsed() { - let url = Url::parse("http://postman-echo.com/path-is-ignored").unwrap(); - - let mut client = HttpClient::from_url(url, &None, None).unwrap(); - - let response: serde_json::Value = client.get_json("/get", None).unwrap(); - - println!("{:?}", response); - } -} diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs deleted file mode 100644 index be23fec..0000000 --- a/crates/lib/src/lib.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Game Server Query Library. -//! -//! # Usage example: -//! -//! ## For a specific game -//! ``` -//! use gamedig::games::teamfortress2; -//! -//! let response = teamfortress2::query(&"127.0.0.1".parse().unwrap(), None); // None is the default port (which is 27015), could also be Some(27015) -//! match response { // Result type, must check what it is... -//! Err(error) => println!("Couldn't query, error: {}", error), -//! Ok(r) => println!("{:#?}", r) -//! } -//! ``` -//! -//! ## Using a game definition -//! ``` -//! use gamedig::{GAMES, query}; -//! -//! let game = GAMES.get("teamfortress2").unwrap(); // Get a game definition, the full list can be found in src/games/mod.rs -//! let response = query(game, &"127.0.0.1".parse().unwrap(), None); // None will use the default port -//! match response { -//! Err(error) => println!("Couldn't query, error: {}", error), -//! Ok(r) => println!("{:#?}", r.as_json()), -//! } -//! ``` -//! -//! # Crate features: -//! Enabled by default: `games`, `game_defs`, `services` -//! -//! `serde` - enables serde serialization/deserialization for many gamedig types -//! using serde derive.
-//! `games` - include games support.
-//! `services` - include services support.
-//! `game_defs` - include game definitions for programmatic access (enabled by -//! default).
-//! `clap` - enable clap derivations for gamedig settings types.
-//! `tls` - enable TLS support for the HTTP client. - -pub mod errors; -#[cfg(feature = "games")] -pub mod games; -pub mod protocols; -#[cfg(feature = "services")] -pub mod services; - -mod buffer; -mod http; -mod socket; -mod utils; - -#[cfg(feature = "packet_capture")] -pub mod capture; - -pub use errors::*; -#[cfg(feature = "games")] -pub use games::*; -#[allow(unused_imports)] -#[cfg(feature = "games")] -pub use query::*; -#[cfg(feature = "services")] -pub use services::*; - -// Re-export types needed to call games::query::query in the root -pub use protocols::types::{ExtraRequestSettings, TimeoutSettings}; diff --git a/crates/lib/src/protocols/epic/mod.rs b/crates/lib/src/protocols/epic/mod.rs deleted file mode 100644 index a9c5c24..0000000 --- a/crates/lib/src/protocols/epic/mod.rs +++ /dev/null @@ -1,58 +0,0 @@ -/// The implementation. -pub mod protocol; -/// All types used by the implementation. -pub mod types; - -pub use protocol::*; -pub use types::*; - -/// Generate a module containing a query function for an epic (EOS) game. -/// -/// * `mod_name` - The name to be given to the game module (see ID naming -/// conventions in CONTRIBUTING.md). -/// * `pretty_name` - The full name of the game, will be used as the -/// documentation for the created module. -/// * `steam_app`, `default_port` - Passed through to [game_query_fn]. -#[cfg(feature = "games")] -macro_rules! game_query_mod { - ($mod_name: ident, $pretty_name: expr, $default_port: literal, $credentials: expr) => { - #[doc = $pretty_name] - pub mod $mod_name { - use crate::protocols::epic::Credentials; - - crate::protocols::epic::game_query_fn!($pretty_name, $default_port, $credentials); - } - }; -} - -#[cfg(feature = "games")] -pub(crate) use game_query_mod; - -/// Generate a query function for an epic (EOS) game. -/// -/// * `default_port` - The default port the game uses. -/// * `credentials` - Credentials to access EOS. -#[cfg(feature = "games")] -macro_rules! game_query_fn { - ($pretty_name: expr, $default_port: literal, $credentials: expr) => { - crate::protocols::epic::game_query_fn! {@gen $default_port, concat!( - "Make a Epic query for ", $pretty_name, ".\n\n", - "If port is `None`, then the default port (", stringify!($default_port), ") will be used."), $credentials} - }; - - (@gen $default_port: literal, $doc: expr, $credentials: expr) => { - #[doc = $doc] - pub fn query( - address: &std::net::IpAddr, - port: Option, - ) -> crate::GDResult { - crate::protocols::epic::query( - $credentials, - &std::net::SocketAddr::new(*address, port.unwrap_or($default_port)), - ) - } - }; -} - -#[cfg(feature = "games")] -pub(crate) use game_query_fn; diff --git a/crates/lib/src/protocols/epic/protocol.rs b/crates/lib/src/protocols/epic/protocol.rs deleted file mode 100644 index 6ace21d..0000000 --- a/crates/lib/src/protocols/epic/protocol.rs +++ /dev/null @@ -1,183 +0,0 @@ -use crate::http::HttpClient; -use crate::protocols::epic::Response; -use crate::GDErrorKind::{JsonParse, PacketBad}; -use crate::{GDResult, TimeoutSettings}; -use base64::prelude::BASE64_STANDARD; -use base64::Engine; -use serde::Deserialize; -#[cfg(feature = "serde")] -use serde::Serialize; -use serde_json::Value; -use std::net::SocketAddr; - -const EPIC_API_ENDPOINT: &str = "https://api.epicgames.dev"; - -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Credentials { - #[cfg_attr(feature = "serde", serde(skip_deserializing, skip_serializing))] - pub deployment: &'static str, - #[cfg_attr(feature = "serde", serde(skip_deserializing, skip_serializing))] - pub id: &'static str, - #[cfg_attr(feature = "serde", serde(skip_deserializing, skip_serializing))] - pub secret: &'static str, - pub auth_by_external: bool, -} - -pub struct EpicProtocol { - client: HttpClient, - credentials: Credentials, -} - -#[derive(Deserialize)] -struct ClientTokenResponse { - access_token: String, -} - -#[derive(Deserialize)] -struct QueryResponse { - sessions: Value, -} - -macro_rules! extract_optional_field { - ($value:expr, $fields:expr, $map_func:expr) => { - $fields - .iter() - .fold(Some(&$value), |acc, &key| acc.and_then(|val| val.get(key))) - .map($map_func) - .flatten() - }; -} - -macro_rules! extract_field { - ($value:expr, $fields:expr, $map_func:expr) => { - extract_optional_field!($value, $fields, $map_func) - .ok_or(PacketBad.context("Field is missing or is not parsable."))? - }; -} - -impl EpicProtocol { - pub fn new(credentials: Credentials, timeout_settings: TimeoutSettings) -> GDResult { - Ok(Self { - client: HttpClient::from_url(EPIC_API_ENDPOINT, &Some(timeout_settings), None)?, - credentials, - }) - } - - pub fn auth_by_external(&self) -> GDResult { Ok(String::new()) } - - pub fn auth_by_client(&mut self) -> GDResult { - let body = [ - ("grant_type", "client_credentials"), - ("deployment_id", self.credentials.deployment), - ]; - - let auth_format = format!("{}:{}", self.credentials.id, self.credentials.secret); - let auth_base = BASE64_STANDARD.encode(auth_format); - let auth = format!("Basic {}", auth_base.as_str()); - let authorization = auth.as_str(); - - let headers = [ - ("Authorization", authorization), - ("Content-Type", "application/x-www-form-urlencoded"), - ]; - - let response = - self.client - .post_json_with_form::("/auth/v1/oauth/token", Some(&headers), &body)?; - Ok(response.access_token) - } - - pub fn query_raw(&mut self, address: &SocketAddr) -> GDResult { - let port = address.port(); - let address = address.ip().to_string(); - - let body = format!( - "{{\"criteria\":[{{\"key\":\"attributes.ADDRESS_s\",\"op\":\"EQUAL\",\"value\":\"{}\"}}]}}", - address - ); - let body = serde_json::from_str::(body.as_str()).map_err(|e| JsonParse.context(e))?; - - let token = if self.credentials.auth_by_external { - self.auth_by_external()? - } else { - self.auth_by_client()? - }; - let authorization = format!("Bearer {}", token); - let headers = [ - ("Content-Type", "application/json"), - ("Accept", "application/json"), - ("Authorization", authorization.as_str()), - ]; - - let url = format!("/matchmaking/v1/{}/filter", self.credentials.deployment); - let response: QueryResponse = self.client.post_json(url.as_str(), Some(&headers), body)?; - - if let Value::Array(sessions) = response.sessions { - if sessions.is_empty() { - return Err(PacketBad.context("No servers provided.")); - } - - for session in sessions.into_iter() { - let attributes = session - .get("attributes") - .ok_or(PacketBad.context("Expected attributes field missing in sessions."))?; - - let address_match = attributes - .get("ADDRESSBOUND_s") - .and_then(Value::as_str) - .map_or(false, |v| v == address || v == format!("0.0.0.0:{}", port)) - || attributes - .get("GAMESERVER_PORT_1") - .and_then(Value::as_u64) - .map_or(false, |v| v == port as u64); - - if address_match { - return Ok(session); - } - } - - return Err( - PacketBad.context("Servers were provided but the specified one couldn't be found amongst them.") - ); - } - - Err(PacketBad.context("Expected session field to be an array.")) - } - - pub fn query(&mut self, address: &SocketAddr) -> GDResult { - let value = self.query_raw(address)?; - - let build_version = extract_optional_field!(value, ["attributes", "BUILDID_s"], Value::as_str); - let minor_version = extract_optional_field!(value, ["attributes", "MINORBUILDID_s"], Value::as_str); - - let game_version = match (build_version, minor_version) { - (Some(b), Some(m)) => Some(format!("{b}.{m}")), - _ => None, - }; - - Ok(Response { - name: extract_field!(value, ["attributes", "CUSTOMSERVERNAME_s"], Value::as_str).to_string(), - map: extract_field!(value, ["attributes", "MAPNAME_s"], Value::as_str).to_string(), - has_password: extract_field!(value, ["attributes", "SERVERPASSWORD_b"], Value::as_bool), - players_online: extract_field!(value, ["totalPlayers"], Value::as_u64) as u32, - players_maxmimum: extract_field!(value, ["settings", "maxPublicPlayers"], Value::as_u64) as u32, - players: vec![], - game_version, - raw: value, - }) - } -} - -pub fn query(credentials: Credentials, address: &SocketAddr) -> GDResult { - query_with_timeout(credentials, address, None) -} - -pub fn query_with_timeout( - credentials: Credentials, - address: &SocketAddr, - timeout_settings: Option, -) -> GDResult { - let mut client = EpicProtocol::new(credentials, timeout_settings.unwrap_or_default())?; - client.query(address) -} diff --git a/crates/lib/src/protocols/epic/types.rs b/crates/lib/src/protocols/epic/types.rs deleted file mode 100644 index 1734dca..0000000 --- a/crates/lib/src/protocols/epic/types.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer}; -use crate::protocols::GenericResponse; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq)] -pub struct Response { - pub name: String, - pub map: String, - pub has_password: bool, - pub players_online: u32, - pub players_maxmimum: u32, - pub players: Vec, - pub game_version: Option, - pub raw: Value, -} - -impl CommonResponse for Response { - fn as_original(&self) -> GenericResponse { GenericResponse::Epic(self) } - fn name(&self) -> Option<&str> { Some(&self.name) } - fn map(&self) -> Option<&str> { Some(&self.map) } - fn players_maximum(&self) -> u32 { self.players_maxmimum } - - fn players_online(&self) -> u32 { self.players_online } - - fn has_password(&self) -> Option { Some(self.has_password) } - - fn players(&self) -> Option> { - Some( - self.players - .iter() - .map(|p| p as &dyn CommonPlayer) - .collect(), - ) - } - - fn game_version(&self) -> Option<&str> { self.game_version.as_deref() } -} - -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq)] -pub struct Player { - pub name: String, -} - -impl CommonPlayer for Player { - fn as_original(&self) -> GenericPlayer { GenericPlayer::Epic(self) } - - fn name(&self) -> &str { &self.name } -} diff --git a/crates/lib/src/protocols/gamespy/common.rs b/crates/lib/src/protocols/gamespy/common.rs deleted file mode 100644 index 18b672d..0000000 --- a/crates/lib/src/protocols/gamespy/common.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::{GDErrorKind, GDResult}; -use std::collections::HashMap; - -pub fn has_password(server_vars: &mut HashMap) -> GDResult { - let password_value = server_vars - .remove("password") - .ok_or_else(|| GDErrorKind::PacketBad.context("Missing password (exists) field"))? - .to_lowercase(); - - if let Ok(has) = password_value.parse::() { - return Ok(has); - } - - let as_numeral: u8 = password_value - .parse() - .map_err(|e| GDErrorKind::TypeParse.context(e))?; - - Ok(as_numeral != 0) -} diff --git a/crates/lib/src/protocols/gamespy/mod.rs b/crates/lib/src/protocols/gamespy/mod.rs deleted file mode 100644 index 125b87d..0000000 --- a/crates/lib/src/protocols/gamespy/mod.rs +++ /dev/null @@ -1,92 +0,0 @@ -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -pub(crate) mod common; -/// The implementations. -pub mod protocols; - -pub use protocols::*; - -/// Versions of the gamespy protocol -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum GameSpyVersion { - One, - Two, - Three, -} - -/// Versioned response type -#[cfg_attr(feature = "serde", derive(Serialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum VersionedResponse<'a> { - One(&'a one::Response), - Two(&'a two::Response), - Three(&'a three::Response), -} - -/// Versioned player type -#[cfg_attr(feature = "serde", derive(Serialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum VersionedPlayer<'a> { - One(&'a one::Player), - Two(&'a two::Player), - Three(&'a three::Player), -} - -/// Generate a module containing a query function for a gamespy game. -/// -/// * `mod_name` - The name to be given to the game module (see ID naming -/// conventions in CONTRIBUTING.md). -/// * `pretty_name` - The full name of the game, will be used as the -/// documentation for the created module. -/// * `gamespy_ver`, `default_port` - Passed through to [game_query_fn]. -#[cfg(feature = "games")] -macro_rules! game_query_mod { - ($mod_name: ident, $pretty_name: expr, $gamespy_ver: ident, $default_port: literal) => { - #[doc = $pretty_name] - pub mod $mod_name { - crate::protocols::gamespy::game_query_fn!($gamespy_ver, $default_port); - } - }; -} - -#[cfg(feature = "games")] -pub(crate) use game_query_mod; - -// Allow generating doc comments: -// https://users.rust-lang.org/t/macros-filling-text-in-comments/20473 -/// Generate a query function for a gamespy game. -/// -/// * `gamespy_ver` - The name of the [module](crate::protocols::gamespy) for -/// the gamespy version the game uses. -/// * `default_port` - The default port the game uses. -/// -/// ```rust,ignore -/// use crate::protocols::gamespy::game_query_fn; -/// game_query_fn!(one, 7778); -/// ``` -#[cfg(feature = "games")] -macro_rules! game_query_fn { - ($gamespy_ver: ident, $default_port: literal) => { - crate::protocols::gamespy::game_query_fn! {@gen $gamespy_ver, $default_port, concat!( - "Make a gamespy ", stringify!($gamespy_ver), " query with default timeout settings.\n\n", - "If port is `None`, then the default port (", stringify!($default_port), ") will be used.")} - }; - - (@gen $gamespy_ver: ident, $default_port: literal, $doc: expr) => { - #[doc = $doc] - pub fn query( - address: &std::net::IpAddr, - port: Option, - ) -> crate::GDResult { - crate::protocols::gamespy::$gamespy_ver::query( - &std::net::SocketAddr::new(*address, port.unwrap_or($default_port)), - None, - ) - } - }; -} - -#[cfg(feature = "games")] -pub(crate) use game_query_fn; diff --git a/crates/lib/src/protocols/gamespy/protocols/mod.rs b/crates/lib/src/protocols/gamespy/protocols/mod.rs deleted file mode 100644 index bdd73c4..0000000 --- a/crates/lib/src/protocols/gamespy/protocols/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod one; -pub mod three; -pub mod two; diff --git a/crates/lib/src/protocols/gamespy/protocols/one/mod.rs b/crates/lib/src/protocols/gamespy/protocols/one/mod.rs deleted file mode 100644 index 14c6b48..0000000 --- a/crates/lib/src/protocols/gamespy/protocols/one/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod protocol; -pub mod types; - -pub use protocol::*; -pub use types::*; diff --git a/crates/lib/src/protocols/gamespy/protocols/one/protocol.rs b/crates/lib/src/protocols/gamespy/protocols/one/protocol.rs deleted file mode 100644 index 32350ce..0000000 --- a/crates/lib/src/protocols/gamespy/protocols/one/protocol.rs +++ /dev/null @@ -1,243 +0,0 @@ -use byteorder::LittleEndian; - -use crate::buffer::Utf8Decoder; -use crate::protocols::gamespy::common::has_password; -use crate::GDErrorKind::TypeParse; - -use crate::utils::retry_on_timeout; -use crate::{ - buffer::Buffer, - protocols::{ - gamespy::one::{Player, Response}, - types::TimeoutSettings, - }, - socket::{Socket, UdpSocket}, - GDErrorKind, - GDResult, -}; -use std::collections::HashMap; -use std::net::SocketAddr; - -/// Send status request, and parse response into HashMap. -/// This function will retry fetch on timeouts. -fn get_server_values( - address: &SocketAddr, - timeout_settings: &Option, -) -> GDResult> { - let mut socket = UdpSocket::new(address, timeout_settings)?; - retry_on_timeout( - TimeoutSettings::get_retries_or_default(timeout_settings), - move || get_server_values_impl(&mut socket), - ) -} - -/// Send status request, and parse response into HashMap (without retry logic). -fn get_server_values_impl(socket: &mut UdpSocket) -> GDResult> { - socket.send(b"\\status\\xserverquery")?; - - let mut received_query_id: Option = None; - let mut parts: Vec = Vec::new(); - let mut is_finished = false; - - let mut server_values = HashMap::new(); - - while !is_finished { - let data = socket.receive(None)?; - let mut bufferer = Buffer::::new(&data); - - let mut as_string = bufferer.read_string::(None)?; - as_string.remove(0); - - let splited: Vec = as_string.split('\\').map(str::to_string).collect(); - - for i in 0 .. splited.len() / 2 { - let position = i * 2; - let key = splited[position].clone(); - let value = splited - .get(position + 1) - .map_or_else(String::new, Clone::clone); - - server_values.insert(key, value); - } - - is_finished = server_values.remove("final").is_some(); - - let query_data = server_values.get("queryid"); - - let mut part = parts.len(); // if the part number isn't provided, it's value is the parts length - let mut query_id = None; - if let Some(qid) = query_data { - let split: Vec<&str> = qid.split('.').collect(); - - query_id = Some(split[0].parse().map_err(|e| TypeParse.context(e))?); - match split.len() { - 1 => (), - 2 => part = split[1].parse().map_err(|e| TypeParse.context(e))?, - _ => Err(GDErrorKind::PacketBad)?, /* the queryid can't be splitted in more than 2 - * elements */ - }; - } - - server_values.remove("queryid"); - - if received_query_id.is_some() && received_query_id != query_id { - return Err(GDErrorKind::PacketBad.into()); // wrong query id! - } - - received_query_id = query_id; - - match parts.contains(&part) { - true => Err(GDErrorKind::PacketBad)?, - false => parts.push(part), - } - } - - Ok(server_values) -} - -fn extract_players(server_vars: &mut HashMap, players_maximum: u32) -> GDResult> { - let mut players_data: Vec> = Vec::with_capacity(players_maximum as usize); - - server_vars.retain(|key, value| { - let split: Vec<&str> = key.split('_').collect(); - - if split.len() != 2 { - return true; - } - - let kind = split[0]; - let id: usize = match split[1].parse() { - Ok(v) => v, - Err(_) => return true, - }; - - let early_return = match kind { - "team" | "player" | "playername" | "ping" | "face" | "skin" | "mesh" | "frags" | "ngsecret" | "deaths" - | "health" => false, - _x => true, // println!("UNKNOWN {id} {x} {value}"); - }; - - if early_return { - return true; - } - - if id >= players_data.len() { - let others = vec![HashMap::new(); id - players_data.len() + 1]; - players_data.extend_from_slice(&others); - } - players_data[id].insert(kind.to_string(), value.to_string()); - - false - }); - - let mut players: Vec = Vec::with_capacity(players_data.len()); - - for player_data in players_data { - let new_player = Player { - name: match player_data.get("player") { - Some(v) => v.clone(), - None => { - player_data - .get("playername") - .ok_or(GDErrorKind::PacketBad)? - .clone() - } - }, - team: match player_data.get("team") { - Some(t) => Some(t.trim().parse().map_err(|e| TypeParse.context(e))?), - None => None, - }, - ping: player_data - .get("ping") - .ok_or(GDErrorKind::PacketBad)? - .trim() - .parse() - .map_err(|e| TypeParse.context(e))?, - face: player_data.get("face").cloned(), - skin: player_data.get("skin").cloned(), - mesh: player_data.get("mesh").cloned(), - score: player_data - .get("frags") - .ok_or(GDErrorKind::PacketBad)? - .trim() - .parse() - .map_err(|e| TypeParse.context(e))?, - deaths: match player_data.get("deaths") { - Some(v) => Some(v.trim().parse().map_err(|e| TypeParse.context(e))?), - None => None, - }, - health: match player_data.get("health") { - Some(v) => Some(v.trim().parse().map_err(|e| TypeParse.context(e))?), - None => None, - }, - secret: match player_data.get("ngsecret") { - Some(s) => Some(s.to_lowercase().parse().map_err(|e| TypeParse.context(e))?), - None => None, - }, - }; - - players.push(new_player); - } - - Ok(players) -} - -/// If there are parsing problems using the `query` function, you can directly -/// get the server's values using this function. -pub fn query_vars( - address: &SocketAddr, - timeout_settings: Option, -) -> GDResult> { - get_server_values(address, &timeout_settings) -} - -/// Query a server by providing the address, the port and timeout settings. -/// Providing None to the timeout settings results in using the default values. -/// (TimeoutSettings::[default](TimeoutSettings::default)). -pub fn query(address: &SocketAddr, timeout_settings: Option) -> GDResult { - let mut server_vars = query_vars(address, timeout_settings)?; - - let players_maximum: u32 = server_vars - .remove("maxplayers") - .ok_or(GDErrorKind::PacketBad)? - .parse() - .map_err(|e| TypeParse.context(e))?; - let players_minimum = match server_vars.remove("minplayers") { - None => None, - Some(v) => Some(v.parse::().map_err(|e| TypeParse.context(e))?), - }; - - let players = extract_players(&mut server_vars, players_maximum)?; - - Ok(Response { - name: server_vars - .remove("hostname") - .ok_or(GDErrorKind::PacketBad)?, - map: server_vars - .remove("mapname") - .ok_or(GDErrorKind::PacketBad)?, - map_title: server_vars.remove("maptitle"), - admin_contact: server_vars.remove("AdminEMail"), - admin_name: server_vars - .remove("AdminName") - .or_else(|| server_vars.remove("admin")), - has_password: has_password(&mut server_vars)?, - game_mode: server_vars - .remove("gametype") - .ok_or(GDErrorKind::PacketBad)?, - game_version: server_vars - .remove("gamever") - .ok_or(GDErrorKind::PacketBad)?, - players_maximum, - players_online: players.len() as u32, - players_minimum, - players, - tournament: server_vars - .remove("tournament") - .unwrap_or_else(|| "true".to_string()) - .to_lowercase() - .parse() - .map_err(|e| TypeParse.context(e))?, - unused_entries: server_vars, - }) -} diff --git a/crates/lib/src/protocols/gamespy/protocols/one/types.rs b/crates/lib/src/protocols/gamespy/protocols/one/types.rs deleted file mode 100644 index 6344d39..0000000 --- a/crates/lib/src/protocols/gamespy/protocols/one/types.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::collections::HashMap; - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -use crate::protocols::gamespy::{VersionedPlayer, VersionedResponse}; -use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer}; -use crate::protocols::GenericResponse; - -/// A player’s details. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct Player { - pub name: String, - pub team: Option, - /// The ping from the server's perspective. - pub ping: u16, - pub face: Option, - pub skin: Option, - pub mesh: Option, - pub score: i32, - pub deaths: Option, - pub health: Option, - pub secret: Option, -} - -impl CommonPlayer for Player { - fn as_original(&self) -> GenericPlayer<'_> { GenericPlayer::Gamespy(VersionedPlayer::One(self)) } - - fn name(&self) -> &str { &self.name } - fn score(&self) -> Option { Some(self.score) } -} - -/// A query response. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Response { - pub name: String, - pub map: String, - pub map_title: Option, - pub admin_contact: Option, - pub admin_name: Option, - pub has_password: bool, - pub game_mode: String, - pub game_version: String, - pub players_maximum: u32, - pub players_online: u32, - pub players_minimum: Option, - pub players: Vec, - pub tournament: bool, - pub unused_entries: HashMap, -} - -impl CommonResponse for Response { - fn as_original(&self) -> GenericResponse<'_> { GenericResponse::GameSpy(VersionedResponse::One(self)) } - - fn name(&self) -> Option<&str> { Some(&self.name) } - fn map(&self) -> Option<&str> { Some(&self.map) } - fn has_password(&self) -> Option { Some(self.has_password) } - fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) } - fn game_version(&self) -> Option<&str> { Some(&self.game_version) } - fn players_maximum(&self) -> u32 { self.players_maximum } - fn players_online(&self) -> u32 { self.players_online } - - fn players(&self) -> Option> { - Some( - self.players - .iter() - .map(|p| p as &dyn CommonPlayer) - .collect(), - ) - } -} diff --git a/crates/lib/src/protocols/gamespy/protocols/three/mod.rs b/crates/lib/src/protocols/gamespy/protocols/three/mod.rs deleted file mode 100644 index 14c6b48..0000000 --- a/crates/lib/src/protocols/gamespy/protocols/three/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod protocol; -pub mod types; - -pub use protocol::*; -pub use types::*; diff --git a/crates/lib/src/protocols/gamespy/protocols/three/protocol.rs b/crates/lib/src/protocols/gamespy/protocols/three/protocol.rs deleted file mode 100644 index 6e40320..0000000 --- a/crates/lib/src/protocols/gamespy/protocols/three/protocol.rs +++ /dev/null @@ -1,410 +0,0 @@ -use byteorder::{BigEndian, LittleEndian}; - -use crate::buffer::{Buffer, Utf8Decoder}; -use crate::protocols::gamespy::common::has_password; -use crate::protocols::gamespy::three::{Player, Response, Team}; -use crate::protocols::types::TimeoutSettings; -use crate::socket::{Socket, UdpSocket}; -use crate::utils::retry_on_timeout; -use crate::GDErrorKind::{PacketBad, TypeParse}; -use crate::{GDErrorKind, GDResult}; -use std::collections::HashMap; -use std::net::SocketAddr; - -const THIS_SESSION_ID: u32 = 1; - -struct RequestPacket { - header: u16, - kind: u8, - session_id: u32, - challenge: Option, - payload: Option<[u8; 4]>, -} - -impl RequestPacket { - fn to_bytes(&self) -> Vec { - let mut packet: Vec = Vec::with_capacity(7); - packet.extend_from_slice(&self.header.to_be_bytes()); - packet.push(self.kind); - packet.extend_from_slice(&self.session_id.to_be_bytes()); - - if let Some(challenge) = self.challenge { - packet.extend_from_slice(&challenge.to_be_bytes()); - } - - if let Some(payload) = self.payload { - packet.extend_from_slice(&payload); - } - - packet - } -} - -pub(crate) struct GameSpy3 { - socket: UdpSocket, - payload: [u8; 4], - single_packets: bool, - retry_count: usize, -} - -const PACKET_SIZE: usize = 2048; -const DEFAULT_PAYLOAD: [u8; 4] = [0xFF, 0xFF, 0xFF, 0x01]; - -impl GameSpy3 { - fn new(address: &SocketAddr, timeout_settings: Option) -> GDResult { - let socket = UdpSocket::new(address, &timeout_settings)?; - let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings); - - Ok(Self { - socket, - payload: DEFAULT_PAYLOAD, - single_packets: false, - retry_count, - }) - } - - pub(crate) fn new_custom( - address: &SocketAddr, - timeout_settings: Option, - payload: [u8; 4], - single_packets: bool, - ) -> GDResult { - let socket = UdpSocket::new(address, &timeout_settings)?; - let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings); - - Ok(Self { - socket, - payload, - single_packets, - retry_count, - }) - } - - fn receive(&mut self, size: Option, kind: u8) -> GDResult> { - let received = self.socket.receive(size.or(Some(PACKET_SIZE)))?; - let mut buf = Buffer::::new(&received); - - if buf.read::()? != kind { - return Err(PacketBad.context("Kind of packet did not match")); - } - - if buf.read::()? != THIS_SESSION_ID { - return Err(PacketBad.context("Session ID did not match")); - } - - Ok(buf.remaining_bytes().to_vec()) - } - - fn make_initial_handshake(&mut self) -> GDResult> { - self.socket.send( - &RequestPacket { - header: 65277, - kind: 9, - session_id: THIS_SESSION_ID, - challenge: None, - payload: None, - } - .to_bytes(), - )?; - - let data = self.receive(Some(16), 9)?; - let mut buf = Buffer::::new(&data); - - let challenge_as_string = buf.read_string::(None)?; - let challenge = challenge_as_string - .parse() - .map_err(|e| TypeParse.context(e))?; - - Ok(match challenge == 0 { - true => None, - false => Some(challenge), - }) - } - - fn send_data_request(&mut self, challenge: Option) -> GDResult<()> { - self.socket.send( - &RequestPacket { - header: 65277, - kind: 0, - session_id: THIS_SESSION_ID, - challenge, - payload: Some(self.payload), - } - .to_bytes(), - ) - } - - /// Fetch packets from server and store in buffer. - /// This function will retry fetch on timeouts. - pub(crate) fn get_server_packets(&mut self) -> GDResult>> { - retry_on_timeout(self.retry_count, move || self.get_server_packets_impl()) - } - - /// Fetch packets from server and store in buffer (without retry logic). - fn get_server_packets_impl(&mut self) -> GDResult>> { - let challenge = self.make_initial_handshake()?; - self.send_data_request(challenge)?; - - let mut values: Vec> = Vec::new(); - - let mut reached_expected_packets_size = false; - - while !reached_expected_packets_size { - let received_data = self.receive(None, 0)?; - let mut buf = Buffer::::new(&received_data); - - if self.single_packets { - buf.move_cursor(11)?; - return Ok(vec![buf.remaining_bytes().to_vec()]); - } - - if buf.read_string::(None)? != "splitnum" { - return Err(PacketBad.context("Expected string \"splitnum\"")); - } - - let id = buf.read::()?; - let is_last = (id & 0x80) > 0; - let packet_id = (id & 0x7f) as usize; - buf.move_cursor(1)?; // unknown byte regarding packet no. - - if is_last && packet_id + 1 != values.len() { - reached_expected_packets_size = true; - } - - while values.len() <= packet_id { - values.push(Vec::new()); - } - - values[packet_id] = buf.remaining_bytes().to_vec(); - } - - if values.iter().any(Vec::is_empty) { - return Err(PacketBad.context("One (or more) packets is empty")); - } - - Ok(values) - } -} - -pub(crate) fn data_to_map(packet: &[u8]) -> GDResult<(HashMap, Vec)> { - let mut vars = HashMap::new(); - - let mut buf = Buffer::::new(packet); - while buf.remaining_length() != 0 { - let key = buf.read_string::(None)?; - if key.is_empty() { - break; - } - - let value = buf.read_string::(None)?; - - vars.insert(key, value); - } - - Ok((vars, buf.remaining_bytes().to_vec())) -} - -/// If there are parsing problems using the `query` function, you can directly -/// get the server's values using this function. -pub fn query_vars( - address: &SocketAddr, - timeout_settings: Option, -) -> GDResult> { - let mut client = GameSpy3::new(address, timeout_settings)?; - let packets = client.get_server_packets()?; - - let mut vars = HashMap::new(); - - for packet in &packets { - let (key_values, _remaining_data) = data_to_map(packet)?; - vars.extend(key_values); - } - - Ok(vars) -} - -fn parse_players_and_teams(packets: Vec>) -> GDResult<(Vec, Vec)> { - let mut players_data: Vec> = vec![HashMap::new()]; - let mut teams_data: Vec> = vec![HashMap::new()]; - - for packet in packets { - let mut buf = Buffer::::new(&packet); - - while buf.remaining_length() != 0 { - if buf.read::()? < 3 { - continue; - } - - buf.move_cursor(1)?; - - let field = buf.read_string::(None)?; - if field.is_empty() { - continue; - } - - let field_split: Vec<&str> = field.split('_').collect(); - let field_name = field_split.first().ok_or(GDErrorKind::PacketBad)?; - if !["player", "score", "ping", "team", "deaths", "pid", "skill"].contains(field_name) { - continue; - } - - let field_type = match field_split.get(1) { - None => None, - Some(v) => { - match v.is_empty() { - true => None, - false => { - if v != &"t" { - Err(GDErrorKind::PacketBad)?; - } - - Some(v) - } - } - } - }; - - let mut offset = buf.read::()? as usize; - - let data = match field_type.is_none() { - true => &mut players_data, - false => &mut teams_data, - }; - - while buf.remaining_length() != 0 { - let item = buf.read_string::(None)?; - if item.is_empty() { - break; - } - - while data.len() <= offset { - data.push(HashMap::new()); - } - - let entry_data = data.get_mut(offset).ok_or(PacketBad)?; - entry_data.insert((*field_name).to_string(), item); - - offset += 1; - } - } - } - - let mut players: Vec = Vec::new(); - for player_data in players_data { - if player_data.is_empty() { - continue; - } - - players.push(Player { - name: player_data.get("player").ok_or(PacketBad)?.to_string(), - score: player_data - .get("score") - .ok_or(GDErrorKind::PacketBad)? - .parse() - .map_err(|e| TypeParse.context(e))?, - ping: player_data - .get("ping") - .ok_or(GDErrorKind::PacketBad)? - .parse() - .map_err(|e| TypeParse.context(e))?, - team: player_data - .get("team") - .ok_or(GDErrorKind::PacketBad)? - .parse() - .map_err(|e| TypeParse.context(e))?, - deaths: player_data - .get("deaths") - .ok_or(GDErrorKind::PacketBad)? - .parse() - .map_err(|e| TypeParse.context(e))?, - skill: player_data - .get("skill") - .ok_or(GDErrorKind::PacketBad)? - .parse() - .map_err(|e| TypeParse.context(e))?, - }); - } - - let mut teams: Vec = Vec::new(); - for team_data in teams_data { - if team_data.is_empty() { - continue; - } - - teams.push(Team { - name: team_data - .get("team") - .ok_or(GDErrorKind::PacketBad)? - .to_string(), - score: team_data - .get("score") - .ok_or(GDErrorKind::PacketBad)? - .parse() - .map_err(|e| TypeParse.context(e))?, - }); - } - - Ok((players, teams)) -} - -/// Query a server by providing the address, the port and timeout settings. -/// Providing None to the timeout settings results in using the default values. -/// (TimeoutSettings::[default](TimeoutSettings::default)). -pub fn query(address: &SocketAddr, timeout_settings: Option) -> GDResult { - let mut client = GameSpy3::new(address, timeout_settings)?; - let packets = client.get_server_packets()?; - - let (mut server_vars, remaining_data) = data_to_map(packets.first().ok_or(GDErrorKind::PacketBad)?)?; - - let mut remaining_data_packets = vec![remaining_data]; - remaining_data_packets.extend_from_slice(&packets[1 ..]); - let (players, teams) = parse_players_and_teams(remaining_data_packets)?; - - let players_maximum = server_vars - .remove("maxplayers") - .ok_or(GDErrorKind::PacketBad)? - .parse() - .map_err(|e| TypeParse.context(e))?; - let players_minimum = match server_vars.remove("minplayers") { - None => None, - Some(v) => Some(v.parse::().map_err(|e| TypeParse.context(e))?), - }; - let players_online: u32 = match server_vars.remove("numplayers") { - None => players.len(), - Some(v) => { - let reported_players = v.parse().map_err(|e| TypeParse.context(e))?; - match reported_players < players.len() { - true => players.len(), - false => reported_players, - } - } - } as u32; - - Ok(Response { - name: server_vars - .remove("hostname") - .ok_or(GDErrorKind::PacketBad)?, - map: server_vars - .remove("mapname") - .ok_or(GDErrorKind::PacketBad)?, - has_password: has_password(&mut server_vars)?, - game_mode: server_vars - .remove("gametype") - .ok_or(GDErrorKind::PacketBad)?, - game_version: server_vars - .remove("gamever") - .ok_or(GDErrorKind::PacketBad)?, - players_maximum, - players_online, - players_minimum, - players, - teams, - tournament: server_vars - .remove("tournament") - .unwrap_or_else(|| "true".to_string()) - .to_lowercase() - .parse() - .map_err(|e| TypeParse.context(e))?, - unused_entries: server_vars, - }) -} diff --git a/crates/lib/src/protocols/gamespy/protocols/three/types.rs b/crates/lib/src/protocols/gamespy/protocols/three/types.rs deleted file mode 100644 index db53ada..0000000 --- a/crates/lib/src/protocols/gamespy/protocols/three/types.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::protocols::gamespy::{VersionedPlayer, VersionedResponse}; -use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer}; -use crate::protocols::GenericResponse; -use std::collections::HashMap; - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -/// A player’s details. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct Player { - pub name: String, - pub score: i32, - pub ping: u16, - pub team: u8, - pub deaths: u32, - pub skill: u32, -} - -impl CommonPlayer for Player { - fn as_original(&self) -> crate::protocols::types::GenericPlayer<'_> { - GenericPlayer::Gamespy(VersionedPlayer::Three(self)) - } - - fn name(&self) -> &str { &self.name } - fn score(&self) -> Option { Some(self.score) } -} - -/// A team's details -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct Team { - pub name: String, - pub score: i32, -} - -/// A query response. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Response { - pub name: String, - pub map: String, - pub has_password: bool, - pub game_mode: String, - pub game_version: String, - pub players_maximum: u32, - pub players_online: u32, - pub players_minimum: Option, - pub players: Vec, - pub teams: Vec, - pub tournament: bool, - pub unused_entries: HashMap, -} - -impl CommonResponse for Response { - fn as_original(&self) -> GenericResponse<'_> { GenericResponse::GameSpy(VersionedResponse::Three(self)) } - - fn name(&self) -> Option<&str> { Some(&self.name) } - fn map(&self) -> Option<&str> { Some(&self.map) } - fn has_password(&self) -> Option { Some(self.has_password) } - fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) } - fn game_version(&self) -> Option<&str> { Some(&self.game_version) } - fn players_maximum(&self) -> u32 { self.players_maximum } - fn players_online(&self) -> u32 { self.players_online } - - fn players(&self) -> Option> { - Some( - self.players - .iter() - .map(|p| p as &dyn CommonPlayer) - .collect(), - ) - } -} diff --git a/crates/lib/src/protocols/gamespy/protocols/two/mod.rs b/crates/lib/src/protocols/gamespy/protocols/two/mod.rs deleted file mode 100644 index 14c6b48..0000000 --- a/crates/lib/src/protocols/gamespy/protocols/two/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod protocol; -pub mod types; - -pub use protocol::*; -pub use types::*; diff --git a/crates/lib/src/protocols/gamespy/protocols/two/protocol.rs b/crates/lib/src/protocols/gamespy/protocols/two/protocol.rs deleted file mode 100644 index cba6e2f..0000000 --- a/crates/lib/src/protocols/gamespy/protocols/two/protocol.rs +++ /dev/null @@ -1,210 +0,0 @@ -use crate::buffer::{Buffer, Utf8Decoder}; -use crate::protocols::gamespy::two::{Player, Response, Team}; -use crate::protocols::types::TimeoutSettings; -use crate::socket::{Socket, UdpSocket}; -use crate::utils::retry_on_timeout; -use crate::GDErrorKind::{PacketBad, TypeParse}; -use crate::{GDErrorKind, GDResult}; -use byteorder::BigEndian; -use std::collections::HashMap; -use std::net::SocketAddr; - -struct GameSpy2 { - socket: UdpSocket, - retry_count: usize, -} - -macro_rules! table_extract { - ($table:expr, $name:literal, $index:expr) => { - $table - .get($name) - .ok_or(GDErrorKind::PacketBad)? - .get($index) - .ok_or(GDErrorKind::PacketBad)? - }; -} - -macro_rules! table_extract_parse { - ($table:expr, $name:literal, $index:expr) => { - table_extract!($table, $name, $index) - .parse() - .map_err(|e| PacketBad.context(e))? - }; -} - -fn data_as_table(data: &mut Buffer) -> GDResult<(HashMap>, usize)> { - if data.read::()? != 0 { - Err(GDErrorKind::PacketBad)?; - } - - let rows = data.read::()? as usize; - - if rows == 0 { - return Ok((HashMap::new(), 0)); - } - - let mut column_heads = Vec::new(); - - let mut current_column = data.read_string::(None)?; - while !current_column.is_empty() { - column_heads.push(current_column); - current_column = data.read_string::(None)?; - } - - let columns = column_heads.len(); - let mut table = HashMap::with_capacity(columns); - for head in &column_heads { - // TODO: This doesn't look good nor it is performant, fix later - // By using &column_heads in the for loop instead of cloning column_heads, you - // avoid creating an unnecessary copy. However, column_heads is a - // Vec and head is a &String (a reference to a string). Hence, to use - // head as a key to the HashMap, we still need to call clone(). This is because - // HashMap takes ownership of its keys and we cannot give it a reference to a - // local variable (head) that will be dropped at the end of the function. - table.insert(head.clone(), Vec::new()); - } - - for _ in 0 .. rows { - for column in &column_heads { - let value = data.read_string::(None)?; - table - .get_mut(column) - .ok_or(GDErrorKind::PacketBad)? - .push(value); - } - } - - Ok((table, rows)) -} - -impl GameSpy2 { - fn new(address: &SocketAddr, timeout_settings: Option) -> GDResult { - let socket = UdpSocket::new(address, &timeout_settings)?; - let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings); - - Ok(Self { - socket, - retry_count, - }) - } - - /// Send fetch request to server and store result in buffer. - /// This function will retry fetch on timeouts. - fn request_data(&mut self) -> GDResult<(Vec, usize)> { - retry_on_timeout(self.retry_count, move || self.request_data_impl()) - } - - /// Send fetch request to server and store result in buffer (without retry - /// logic). - fn request_data_impl(&mut self) -> GDResult<(Vec, usize)> { - self.socket - .send(&[0xFE, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF])?; - - let received = self.socket.receive(None)?; - - let mut buf = Buffer::::new(&received); - if buf.read::()? != 0 || buf.read::()? != 1 { - return Err(PacketBad.into()); - } - - let buf_index = buf.current_position(); - Ok((received, buf_index)) - } -} - -fn get_server_vars(bufferer: &mut Buffer) -> GDResult> { - let mut values = HashMap::new(); - - let mut done_processing_vars = false; - while !done_processing_vars && bufferer.remaining_length() != 0 { - let key = bufferer.read_string::(None)?; - let value = bufferer.read_string::(None)?; - - if key.is_empty() { - if value.is_empty() { - bufferer.move_cursor(-1)?; - done_processing_vars = true; - } - - continue; - } - - values.insert(key, value); - } - - Ok(values) -} - -fn get_teams(bufferer: &mut Buffer) -> GDResult> { - let mut teams = Vec::new(); - - let (table, entries) = data_as_table(bufferer)?; - - for index in 0 .. entries { - teams.push(Team { - name: table_extract!(table, "team_t", index).clone(), - score: table_extract_parse!(table, "score_t", index), - }); - } - - Ok(teams) -} - -fn get_players(bufferer: &mut Buffer) -> GDResult> { - let mut players = Vec::new(); - - let (table, entries) = data_as_table(bufferer)?; - - for index in 0 .. entries { - players.push(Player { - name: table_extract!(table, "player_", index).clone(), - score: table_extract_parse!(table, "score_", index), - ping: table_extract_parse!(table, "ping_", index), - team_index: table_extract_parse!(table, "team_", index), - }); - } - - Ok(players) -} - -pub fn query(address: &SocketAddr, timeout_settings: Option) -> GDResult { - let mut client = GameSpy2::new(address, timeout_settings)?; - let (data, buf_index) = client.request_data()?; - - let mut buffer = Buffer::::new(&data); - buffer.move_cursor(buf_index as isize)?; - - let mut server_vars = get_server_vars(&mut buffer)?; - let players = get_players(&mut buffer)?; - - let players_online = match server_vars.remove("numplayers") { - None => players.len(), - Some(v) => { - let reported_players = v.parse().map_err(|e| TypeParse.context(e))?; - match reported_players < players.len() { - true => players.len(), - false => reported_players, - } - } - } as u32; - let players_minimum = match server_vars.remove("minplayers") { - None => None, - Some(v) => Some(v.parse::().map_err(|e| TypeParse.context(e))?), - }; - - Ok(Response { - name: server_vars.remove("hostname").ok_or(PacketBad)?, - map: server_vars.remove("mapname").ok_or(PacketBad)?, - has_password: server_vars.remove("password").ok_or(PacketBad)? == "1", - teams: get_teams(&mut buffer)?, - players_maximum: server_vars - .remove("maxplayers") - .ok_or(PacketBad)? - .parse() - .map_err(|e| TypeParse.context(e))?, - players_online, - players_minimum, - players, - unused_entries: server_vars, - }) -} diff --git a/crates/lib/src/protocols/gamespy/protocols/two/types.rs b/crates/lib/src/protocols/gamespy/protocols/two/types.rs deleted file mode 100644 index e99c33c..0000000 --- a/crates/lib/src/protocols/gamespy/protocols/two/types.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::collections::HashMap; - -use crate::protocols::gamespy::{VersionedPlayer, VersionedResponse}; -use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer}; -use crate::protocols::GenericResponse; - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct Team { - pub name: String, - pub score: u16, -} - -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct Player { - pub name: String, - pub score: u16, - pub ping: u16, - pub team_index: u16, -} - -impl CommonPlayer for Player { - fn as_original(&self) -> GenericPlayer<'_> { GenericPlayer::Gamespy(VersionedPlayer::Two(self)) } - - fn name(&self) -> &str { &self.name } - fn score(&self) -> Option { Some(self.score.into()) } -} - -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Response { - pub name: String, - pub map: String, - pub has_password: bool, - pub teams: Vec, - pub players_maximum: u32, - pub players_online: u32, - pub players_minimum: Option, - pub players: Vec, - pub unused_entries: HashMap, -} - -impl CommonResponse for Response { - fn as_original(&self) -> GenericResponse<'_> { GenericResponse::GameSpy(VersionedResponse::Two(self)) } - - fn name(&self) -> Option<&str> { Some(&self.name) } - fn map(&self) -> Option<&str> { Some(&self.map) } - fn has_password(&self) -> Option { Some(self.has_password) } - fn players_maximum(&self) -> u32 { self.players_maximum } - fn players_online(&self) -> u32 { self.players_online } - - fn players(&self) -> Option> { - Some( - self.players - .iter() - .map(|p| p as &dyn CommonPlayer) - .collect(), - ) - } -} diff --git a/crates/lib/src/protocols/mod.rs b/crates/lib/src/protocols/mod.rs deleted file mode 100644 index e98843e..0000000 --- a/crates/lib/src/protocols/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Protocols that are currently implemented. -//! -//! A protocol will be here if it supports multiple entries, if not, its -//! implementation will be in that specific needed place, a protocol can be -//! independently queried. - -#[cfg(feature = "tls")] -/// Reference: [node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/epic.js) -pub mod epic; -/// Reference: [node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy1.js) -pub mod gamespy; -/// Reference: [node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake1.js) -pub mod quake; -/// General types that are used by all protocols. -pub mod types; -/// Reference: [node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/unreal2.js) -pub mod unreal2; -/// Reference: [Server Query](https://developer.valvesoftware.com/wiki/Server_queries) -pub mod valve; - -pub use types::{ExtraRequestSettings, GenericResponse, Protocol}; diff --git a/crates/lib/src/protocols/quake/client.rs b/crates/lib/src/protocols/quake/client.rs deleted file mode 100644 index e905097..0000000 --- a/crates/lib/src/protocols/quake/client.rs +++ /dev/null @@ -1,145 +0,0 @@ -use byteorder::LittleEndian; - -use crate::buffer::{Buffer, Utf8Decoder}; -use crate::protocols::quake::types::Response; -use crate::protocols::types::TimeoutSettings; -use crate::socket::{Socket, UdpSocket}; -use crate::utils::retry_on_timeout; -use crate::GDErrorKind::{PacketBad, TypeParse}; -use crate::{GDErrorKind, GDResult}; -use std::collections::HashMap; -use std::net::SocketAddr; -use std::slice::Iter; - -pub trait QuakeClient { - type Player; - - fn get_send_header<'a>() -> &'a str; - fn get_response_header<'a>() -> &'a str; - fn parse_player_string(data: Iter<&str>) -> GDResult; -} - -/// Send request and return result buffer. -/// This function will retry fetch on timeouts. -fn get_data( - address: &SocketAddr, - timeout_settings: &Option, -) -> GDResult> { - let mut socket = UdpSocket::new(address, timeout_settings)?; - retry_on_timeout( - TimeoutSettings::get_retries_or_default(timeout_settings), - move || get_data_impl::(&mut socket), - ) -} - -/// Send request and return result buffer (without retry logic). -fn get_data_impl(socket: &mut UdpSocket) -> GDResult> { - socket.send( - &[ - &[0xFF, 0xFF, 0xFF, 0xFF], - Client::get_send_header().as_bytes(), - &[0x00], - ] - .concat(), - )?; - - let data = socket.receive(None)?; - let mut bufferer = Buffer::::new(&data); - - if bufferer.read::()? != u32::MAX { - return Err(PacketBad.context("Expected 4294967295")); - } - - let response_header = Client::get_response_header().as_bytes(); - if !bufferer.remaining_bytes().starts_with(response_header) { - Err(GDErrorKind::PacketBad)?; - } - - bufferer.move_cursor(response_header.len() as isize)?; - - Ok(bufferer.remaining_bytes().to_vec()) -} - -fn get_server_values(bufferer: &mut Buffer) -> GDResult> { - let data = bufferer.read_string::(Some([0x0A]))?; - let mut data_split = data.split('\\').collect::>(); - if let Some(first) = data_split.first() { - if first == &"" { - data_split.remove(0); - } - } - - let values = data_split.chunks(2); - - let mut vars: HashMap = HashMap::new(); - for data in values { - let key = data.first(); - let value = data.get(1); - - if let Some(k) = key { - if let Some(v) = value { - vars.insert((*k).to_string(), (*v).to_string()); - } - } - } - - Ok(vars) -} - -fn get_players(bufferer: &mut Buffer) -> GDResult> { - let mut players: Vec = Vec::new(); - - // this needs to be looked at again as theres no way to check if the buffer has - // a remaining null byte the original code was: - // while !bufferer.is_remaining_empty() && bufferer.remaining_data() != [0x00] - while !bufferer.remaining_length() == 0 { - let data = bufferer.read_string::(Some([0x0A]))?; - let data_split = data.split(' ').collect::>(); - let data_iter = data_split.iter(); - - players.push(Client::parse_player_string(data_iter)?); - } - - Ok(players) -} - -pub fn client_query( - address: &SocketAddr, - timeout_settings: Option, -) -> GDResult> { - let data = get_data::(address, &timeout_settings)?; - let mut bufferer = Buffer::::new(&data); - - let mut server_vars = get_server_values(&mut bufferer)?; - let players = get_players::(&mut bufferer)?; - - Ok(Response { - name: server_vars - .remove("hostname") - .or_else(|| server_vars.remove("sv_hostname")) - .ok_or(GDErrorKind::PacketBad)?, - map: server_vars - .remove("mapname") - .or_else(|| server_vars.remove("map")) - .ok_or(GDErrorKind::PacketBad)?, - players_online: players.len() as u8, - players_maximum: server_vars - .remove("maxclients") - .or_else(|| server_vars.remove("sv_maxclients")) - .ok_or(GDErrorKind::PacketBad)? - .parse() - .map_err(|e| TypeParse.context(e))?, - players, - game_version: server_vars - .remove("version") - .or_else(|| server_vars.remove("*version")), - unused_entries: server_vars, - }) -} - -pub fn remove_wrapping_quotes<'a>(string: &&'a str) -> &'a str { - match string.starts_with('\"') && string.ends_with('\"') { - false => string, - true => &string[1 .. string.len() - 1], - } -} diff --git a/crates/lib/src/protocols/quake/mod.rs b/crates/lib/src/protocols/quake/mod.rs deleted file mode 100644 index 2368738..0000000 --- a/crates/lib/src/protocols/quake/mod.rs +++ /dev/null @@ -1,78 +0,0 @@ -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -pub mod one; -pub mod three; -pub mod two; - -/// All types used by the implementation. -pub mod types; -pub use types::*; - -mod client; - -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum QuakeVersion { - One, - Two, - Three, -} - -/// Generate a module containing a query function for a quake game. -/// -/// * `mod_name` - The name to be given to the game module (see ID naming -/// conventions in CONTRIBUTING.md). -/// * `pretty_name` - The full name of the game, will be used as the -/// documentation for the created module. -/// * `quake_ver`, `default_port` - Passed through to [game_query_fn]. -#[cfg(feature = "games")] -macro_rules! game_query_mod { - ($mod_name: ident, $pretty_name: expr, $quake_ver: ident, $default_port: literal) => { - #[doc = $pretty_name] - pub mod $mod_name { - crate::protocols::quake::game_query_fn!($quake_ver, $default_port); - } - }; -} - -#[cfg(feature = "games")] -pub(crate) use game_query_mod; - -// Allow generating doc comments: -// https://users.rust-lang.org/t/macros-filling-text-in-comments/20473 -/// Generate a query function for a quake game. -/// -/// * `quake_ver` - The name of the [module](crate::protocols::quake) for the -/// quake version the game uses. -/// * `default_port` - The default port the game uses. -/// -/// ```rust,ignore -/// use crate::protocols::quake::game_query_fn; -/// game_query_fn!(one, 27500); -/// ``` -#[cfg(feature = "games")] -macro_rules! game_query_fn { - ($quake_ver: ident, $default_port: literal) => { - use crate::protocols::quake::$quake_ver::Player; - crate::protocols::quake::game_query_fn! {@gen $quake_ver, Player, $default_port, concat!( - "Make a quake ", stringify!($quake_ver), " query with default timeout settings.\n\n", - "If port is `None`, then the default port (", stringify!($default_port), ") will be used.")} - }; - - (@gen $quake_ver: ident, $player_type: ty, $default_port: literal, $doc: expr) => { - #[doc = $doc] - pub fn query( - address: &std::net::IpAddr, - port: Option, - ) -> crate::GDResult> { - crate::protocols::quake::$quake_ver::query( - &std::net::SocketAddr::new(*address, port.unwrap_or($default_port)), - None, - ) - } - }; -} - -#[cfg(feature = "games")] -pub(crate) use game_query_fn; diff --git a/crates/lib/src/protocols/quake/one.rs b/crates/lib/src/protocols/quake/one.rs deleted file mode 100644 index b25a018..0000000 --- a/crates/lib/src/protocols/quake/one.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::protocols::quake::client::{client_query, remove_wrapping_quotes, QuakeClient}; -use crate::protocols::quake::Response; -use crate::protocols::types::{CommonPlayer, GenericPlayer, TimeoutSettings}; -use crate::GDErrorKind::TypeParse; -use crate::{GDErrorKind, GDResult}; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; -use std::net::SocketAddr; -use std::slice::Iter; - -use super::QuakePlayerType; - -/// Quake 1 player data. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct Player { - /// Player's server id. - pub id: u8, - pub score: u16, - pub time: u16, - pub ping: u16, - pub name: String, - pub skin: String, - pub color_primary: u8, - pub color_secondary: u8, -} - -impl QuakePlayerType for Player { - fn version(response: &Response) -> super::VersionedResponse<'_> { super::VersionedResponse::One(response) } -} - -impl CommonPlayer for Player { - fn as_original(&self) -> GenericPlayer<'_> { GenericPlayer::QuakeOne(self) } - - fn name(&self) -> &str { &self.name } - fn score(&self) -> Option { Some(self.score.into()) } -} - -pub(crate) struct QuakeOne; -impl QuakeClient for QuakeOne { - type Player = Player; - - fn get_send_header<'a>() -> &'a str { "status" } - - fn get_response_header<'a>() -> &'a str { "n" } - - fn parse_player_string(mut data: Iter<&str>) -> GDResult { - Ok(Player { - id: match data.next() { - None => Err(GDErrorKind::PacketBad)?, - Some(v) => v.parse().map_err(|e| TypeParse.context(e))?, - }, - score: match data.next() { - None => Err(GDErrorKind::PacketBad)?, - Some(v) => v.parse().map_err(|e| TypeParse.context(e))?, - }, - time: match data.next() { - None => Err(GDErrorKind::PacketBad)?, - Some(v) => v.parse().map_err(|e| TypeParse.context(e))?, - }, - ping: match data.next() { - None => Err(GDErrorKind::PacketBad)?, - Some(v) => v.parse().map_err(|e| TypeParse.context(e))?, - }, - name: match data.next() { - None => Err(GDErrorKind::PacketBad)?, - Some(v) => remove_wrapping_quotes(v).to_string(), - }, - skin: match data.next() { - None => Err(GDErrorKind::PacketBad)?, - Some(v) => remove_wrapping_quotes(v).to_string(), - }, - color_primary: match data.next() { - None => Err(GDErrorKind::PacketBad)?, - Some(v) => v.parse().map_err(|e| TypeParse.context(e))?, - }, - color_secondary: match data.next() { - None => Err(GDErrorKind::PacketBad)?, - Some(v) => v.parse().map_err(|e| TypeParse.context(e))?, - }, - }) - } -} - -pub fn query(address: &SocketAddr, timeout_settings: Option) -> GDResult> { - client_query::(address, timeout_settings) -} diff --git a/crates/lib/src/protocols/quake/three.rs b/crates/lib/src/protocols/quake/three.rs deleted file mode 100644 index 68a25ef..0000000 --- a/crates/lib/src/protocols/quake/three.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::protocols::quake::client::{client_query, QuakeClient}; -use crate::protocols::quake::two::QuakeTwo; -use crate::protocols::quake::Response; -use crate::protocols::types::TimeoutSettings; -use crate::GDResult; -use std::net::SocketAddr; -use std::slice::Iter; - -pub use crate::protocols::quake::two::Player; - -struct QuakeThree; -impl QuakeClient for QuakeThree { - type Player = Player; - - fn get_send_header<'a>() -> &'a str { "getstatus" } - - fn get_response_header<'a>() -> &'a str { "statusResponse\n" } - - fn parse_player_string(data: Iter<&str>) -> GDResult { QuakeTwo::parse_player_string(data) } -} - -pub fn query(address: &SocketAddr, timeout_settings: Option) -> GDResult> { - client_query::(address, timeout_settings) -} diff --git a/crates/lib/src/protocols/quake/two.rs b/crates/lib/src/protocols/quake/two.rs deleted file mode 100644 index c6491f2..0000000 --- a/crates/lib/src/protocols/quake/two.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::protocols::quake::client::{client_query, remove_wrapping_quotes, QuakeClient}; -use crate::protocols::quake::one::QuakeOne; -use crate::protocols::quake::Response; -use crate::protocols::types::{CommonPlayer, GenericPlayer, TimeoutSettings}; -use crate::GDErrorKind::TypeParse; -use crate::{GDErrorKind, GDResult}; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; -use std::net::SocketAddr; -use std::slice::Iter; - -use super::QuakePlayerType; - -/// Quake 2 player data. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct Player { - pub score: i32, - pub ping: u16, - pub name: String, - pub address: Option, -} - -impl QuakePlayerType for Player { - fn version(response: &Response) -> super::VersionedResponse<'_> { - super::VersionedResponse::TwoAndThree(response) - } -} - -impl CommonPlayer for Player { - fn as_original(&self) -> GenericPlayer<'_> { GenericPlayer::QuakeTwo(self) } - - fn name(&self) -> &str { &self.name } - - fn score(&self) -> Option { Some(self.score) } -} - -pub(crate) struct QuakeTwo; -impl QuakeClient for QuakeTwo { - type Player = Player; - - fn get_send_header<'a>() -> &'a str { QuakeOne::get_send_header() } - - fn get_response_header<'a>() -> &'a str { "print\n" } - - fn parse_player_string(mut data: Iter<&str>) -> GDResult { - Ok(Player { - score: match data.next() { - None => Err(GDErrorKind::PacketBad)?, - Some(v) => v.parse().map_err(|e| TypeParse.context(e))?, - }, - ping: match data.next() { - None => Err(GDErrorKind::PacketBad)?, - Some(v) => v.parse().map_err(|e| TypeParse.context(e))?, - }, - name: match data.next() { - None => Err(GDErrorKind::PacketBad)?, - Some(v) => remove_wrapping_quotes(v).to_string(), - }, - address: data.next().map(|v| remove_wrapping_quotes(v).to_string()), - }) - } -} - -pub fn query(address: &SocketAddr, timeout_settings: Option) -> GDResult> { - client_query::(address, timeout_settings) -} diff --git a/crates/lib/src/protocols/quake/types.rs b/crates/lib/src/protocols/quake/types.rs deleted file mode 100644 index 6e72ed3..0000000 --- a/crates/lib/src/protocols/quake/types.rs +++ /dev/null @@ -1,59 +0,0 @@ -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -use crate::protocols::{ - types::{CommonPlayer, CommonResponse}, - GenericResponse, -}; - -/// General server information's. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Response

{ - /// Name of the server. - pub name: String, - /// Map name. - pub map: String, - /// Current online players. - pub players: Vec

, - /// Number of players on the server. - pub players_online: u8, - /// Maximum number of players the server reports it can hold. - pub players_maximum: u8, - /// The server version. - pub game_version: Option, - /// Other server entries that weren't used. - pub unused_entries: HashMap, -} - -pub trait QuakePlayerType: Sized + CommonPlayer { - fn version(response: &Response) -> VersionedResponse<'_>; -} - -impl CommonResponse for Response

{ - fn as_original(&self) -> GenericResponse<'_> { GenericResponse::Quake(P::version(self)) } - - fn name(&self) -> Option<&str> { Some(&self.name) } - fn game_version(&self) -> Option<&str> { self.game_version.as_deref() } - fn map(&self) -> Option<&str> { Some(&self.map) } - fn players_maximum(&self) -> u32 { self.players_maximum.into() } - fn players_online(&self) -> u32 { self.players_online.into() } - - fn players(&self) -> Option> { - Some( - self.players - .iter() - .map(|p| p as &dyn CommonPlayer) - .collect(), - ) - } -} - -/// Versioned response type -#[cfg_attr(feature = "serde", derive(Serialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum VersionedResponse<'a> { - One(&'a Response), - TwoAndThree(&'a Response), -} diff --git a/crates/lib/src/protocols/types.rs b/crates/lib/src/protocols/types.rs deleted file mode 100644 index 501321b..0000000 --- a/crates/lib/src/protocols/types.rs +++ /dev/null @@ -1,493 +0,0 @@ -#[cfg(feature = "games")] -use crate::games::minecraft; -#[cfg(feature = "tls")] -use crate::protocols::epic; -use crate::protocols::{gamespy, quake, unreal2, valve}; -use crate::GDErrorKind::InvalidInput; -use crate::GDResult; - -use std::time::Duration; - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -/// Enumeration of all custom protocols -#[cfg(feature = "games")] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum ProprietaryProtocol { - TheShip, - Minecraft(Option), - FFOW, - JC2M, - Savage2, - Eco, - Mindustry, - #[cfg(all(feature = "services", feature = "tls", feature = "serde"))] - Minetest, -} - -/// Enumeration of all valid protocol types -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Protocol { - Gamespy(gamespy::GameSpyVersion), - Quake(quake::QuakeVersion), - Valve(valve::Engine), - Unreal2, - #[cfg(feature = "tls")] - Epic(epic::Credentials), - #[cfg(feature = "games")] - PROPRIETARY(ProprietaryProtocol), -} - -/// All response types -#[cfg_attr(feature = "serde", derive(Serialize))] -#[derive(Debug, Clone, PartialEq)] -pub enum GenericResponse<'a> { - GameSpy(gamespy::VersionedResponse<'a>), - Quake(quake::VersionedResponse<'a>), - Valve(&'a valve::Response), - Unreal2(&'a unreal2::Response), - #[cfg(feature = "tls")] - Epic(&'a epic::Response), - #[cfg(feature = "games")] - Mindustry(&'a crate::games::mindustry::types::ServerData), - #[cfg(feature = "games")] - Minecraft(minecraft::VersionedResponse<'a>), - #[cfg(feature = "games")] - TheShip(&'a crate::games::theship::Response), - #[cfg(feature = "games")] - FFOW(&'a crate::games::ffow::Response), - #[cfg(feature = "games")] - JC2M(&'a crate::games::jc2m::Response), - #[cfg(feature = "games")] - Savage2(&'a crate::games::savage2::Response), - #[cfg(feature = "games")] - Eco(&'a crate::games::eco::Response), - #[cfg(all( - feature = "services", - feature = "tls", - feature = "serde", - feature = "games" - ))] - Minetest(&'a crate::games::minetest::Response), -} - -/// All player types -#[cfg_attr(feature = "serde", derive(Serialize))] -#[derive(Debug, Clone, PartialEq)] -pub enum GenericPlayer<'a> { - Valve(&'a valve::ServerPlayer), - QuakeOne(&'a quake::one::Player), - QuakeTwo(&'a quake::two::Player), - Gamespy(gamespy::VersionedPlayer<'a>), - Unreal2(&'a unreal2::Player), - #[cfg(feature = "tls")] - Epic(&'a epic::Player), - #[cfg(feature = "games")] - Minecraft(&'a minecraft::Player), - #[cfg(feature = "games")] - TheShip(&'a crate::games::theship::TheShipPlayer), - #[cfg(feature = "games")] - JCMP2(&'a crate::games::jc2m::Player), - #[cfg(feature = "games")] - Eco(&'a crate::games::eco::Player), - #[cfg(all( - feature = "services", - feature = "tls", - feature = "serde", - feature = "games" - ))] - Minetest(&'a crate::games::minetest::Player), -} - -pub trait CommonResponse { - /// Get the original response type - fn as_original(&self) -> GenericResponse<'_>; - /// Get a struct that can be stored as JSON (you don't need to override - /// this) - fn as_json(&self) -> CommonResponseJson<'_> { - CommonResponseJson { - name: self.name(), - description: self.description(), - game_mode: self.game_mode(), - game_version: self.game_version(), - has_password: self.has_password(), - map: self.map(), - players_maximum: self.players_maximum(), - players_online: self.players_online(), - players_bots: self.players_bots(), - players: self - .players() - .map(|players| players.iter().map(|p| p.as_json()).collect()), - } - } - - /// The name of the server - fn name(&self) -> Option<&str> { None } - /// Description of the server - fn description(&self) -> Option<&str> { None } - /// Name of the current game or game mode - fn game_mode(&self) -> Option<&str> { None } - /// Version of the game being run on the server - fn game_version(&self) -> Option<&str> { None } - /// The current map name - fn map(&self) -> Option<&str> { None } - /// Maximum number of players allowed to connect - fn players_maximum(&self) -> u32; - /// Number of players currently connected - fn players_online(&self) -> u32; - /// Number of bots currently connected - fn players_bots(&self) -> Option { None } - /// Whether the server requires a password to join - fn has_password(&self) -> Option { None } - /// Currently connected players - fn players(&self) -> Option> { None } -} - -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct CommonResponseJson<'a> { - pub name: Option<&'a str>, - pub description: Option<&'a str>, - pub game_mode: Option<&'a str>, - pub game_version: Option<&'a str>, - pub map: Option<&'a str>, - pub players_maximum: u32, - pub players_online: u32, - pub players_bots: Option, - pub has_password: Option, - pub players: Option>>, -} - -pub trait CommonPlayer { - /// Get the original player type - fn as_original(&self) -> GenericPlayer<'_>; - /// Get a struct that can be stored as JSON (you don't need to override - /// this) - fn as_json(&self) -> CommonPlayerJson<'_> { - CommonPlayerJson { - name: self.name(), - score: self.score(), - } - } - - /// Player name - fn name(&self) -> &str; - /// Player score - fn score(&self) -> Option { None } -} - -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct CommonPlayerJson<'a> { - pub name: &'a str, - pub score: Option, -} - -#[cfg(feature = "clap")] -fn parse_duration_secs(value: &str) -> Result { - let secs = value.parse()?; - Ok(Duration::from_secs(secs)) -} - -/// Timeout settings for socket operations -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "clap", derive(clap::Args))] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct TimeoutSettings { - #[cfg_attr(feature = "clap", arg(long = "connect-timeout", value_parser = parse_duration_secs, help = "Socket connect timeout (in seconds)", default_value = "4"))] - connect: Option, - #[cfg_attr(feature = "clap", arg(long = "read-timeout", value_parser = parse_duration_secs, help = "Socket read timeout (in seconds)", default_value = "4"))] - read: Option, - #[cfg_attr(feature = "clap", arg(long = "write-timeout", value_parser = parse_duration_secs, help = "Socket write timeout (in seconds)", default_value = "4"))] - write: Option, - /// Number of retries per request - #[cfg_attr(feature = "clap", arg(long, default_value = "0"))] - retries: usize, -} - -impl TimeoutSettings { - /// Construct new settings, passing None will block indefinitely. - /// Passing zero Duration throws GDErrorKind::[InvalidInput]. - /// - /// The retry count is the number of extra tries once the original request - /// fails, so a value of "0" will only make a single request, whereas - /// "1" will try the request again once if it fails. - /// The retry count is per-request so for multi-request queries (valve) if a - /// single part fails that part can be retried up to `retries` times. - pub fn new( - read: Option, - write: Option, - connect: Option, - retries: usize, - ) -> GDResult { - if let Some(read_duration) = read { - if read_duration.is_zero() { - return Err(InvalidInput.context("Read duration must not be 0")); - } - } - - if let Some(write_duration) = write { - if write_duration.is_zero() { - return Err(InvalidInput.context("Write duration must not be 0")); - } - } - - if let Some(connect_duration) = connect { - if connect_duration.is_zero() { - return Err(InvalidInput.context("Connect duration must not be 0")); - } - } - - Ok(Self { - read, - write, - connect, - retries, - }) - } - - /// Get the read timeout. - pub const fn get_read(&self) -> Option { self.read } - - /// Get the write timeout. - pub const fn get_write(&self) -> Option { self.write } - - /// Get the connect timeout. - pub const fn get_connect(&self) -> Option { self.connect } - - /// Get number of retries - pub const fn get_retries(&self) -> usize { self.retries } - - /// Get the number of retries if there are timeout settings else fall back - /// to the default - pub const fn get_retries_or_default(timeout_settings: &Option) -> usize { - if let Some(timeout_settings) = timeout_settings { - timeout_settings.get_retries() - } else { - Self::const_default().get_retries() - } - } - - /// Get the read and write durations if there are timeout settings else fall - /// back to the defaults - pub const fn get_read_and_write_or_defaults( - timeout_settings: &Option, - ) -> (Option, Option) { - if let Some(timeout_settings) = timeout_settings { - (timeout_settings.get_read(), timeout_settings.get_write()) - } else { - let default = Self::const_default(); - (default.get_read(), default.get_write()) - } - } - - /// Get the connect duration given timeout settings or get the default. - pub const fn get_connect_or_default(timeout_settings: &Option) -> Option { - if let Some(timeout_settings) = timeout_settings { - timeout_settings.get_connect() - } else { - Self::const_default().get_connect() - } - } - - /// Default values are 4 seconds for both read and write, no retries. - pub const fn const_default() -> Self { - Self { - read: Some(Duration::from_secs(4)), - write: Some(Duration::from_secs(4)), - connect: Some(Duration::from_secs(4)), - retries: 0, - } - } -} - -impl Default for TimeoutSettings { - /// Default values are 4 seconds for both read and write, no retries. - fn default() -> Self { Self::const_default() } -} - -/// Generic extra request settings -/// -/// Fields of this struct may not be used depending on which protocol -/// is selected, the individual fields link to the specific places -/// they will be used with additional documentation. -/// -/// ## Examples -/// Create minecraft settings with builder: -/// ``` -/// use gamedig::games::minecraft; -/// use gamedig::protocols::ExtraRequestSettings; -/// let mc_settings: minecraft::RequestSettings = ExtraRequestSettings::default().set_hostname("mc.hypixel.net".to_string()).into(); -/// ``` -/// -/// Create valve settings with builder: -/// ``` -/// use gamedig::protocols::{valve, ExtraRequestSettings}; -/// let valve_settings: valve::GatheringSettings = ExtraRequestSettings::default().set_check_app_id(false).into(); -/// ``` -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "clap", derive(clap::Args))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] -pub struct ExtraRequestSettings { - /// The server's hostname. - /// - /// Used by: - /// - [minecraft::RequestSettings#structfield.hostname] - #[cfg_attr(feature = "clap", arg(long))] - pub hostname: Option, - /// The protocol version to use. - /// - /// Used by: - /// - [minecraft::RequestSettings#structfield.protocol_version] - #[cfg_attr(feature = "clap", arg(long))] - pub protocol_version: Option, - /// Whether to gather player information - /// - /// Used by: - /// - [valve::GatheringSettings#structfield.players] - /// - [unreal2::GatheringSettings#structfield.players] - #[cfg_attr(feature = "clap", arg(long))] - pub gather_players: Option, - /// Whether to gather rule information. - /// - /// Used by: - /// - [valve::GatheringSettings#structfield.rules] - /// - [unreal2::GatheringSettings#structfield.mutators_and_rules] - #[cfg_attr(feature = "clap", arg(long))] - pub gather_rules: Option, - /// Whether to check if the App ID is valid. - /// - /// Used by: - /// - [valve::GatheringSettings#structfield.check_app_id] - #[cfg_attr(feature = "clap", arg(long))] - pub check_app_id: Option, -} - -/// Select how to go about gathering extra information via additional requests. -/// -/// Used by: -/// - [ExtraRequestSettings] -/// - [valve::GatheringSettings] -/// - [unreal2::GatheringSettings] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] -pub enum GatherToggle { - /// No request is sent for the relevant data. This option bypasses data - /// gathering. - #[default] - Skip, - - /// A request will be sent, but errors are not treated as criticial. - /// In the case of an error, the operation will return a default value or - /// `None`. - Try, - - /// A request will be sent, and any resulting errors will be propagated. - /// This option treats successful data gathering as mandatory. - Enforce, -} - -impl ExtraRequestSettings { - /// [Sets hostname](ExtraRequestSettings#structfield.hostname) - pub fn set_hostname(mut self, hostname: String) -> Self { - self.hostname = Some(hostname); - self - } - /// [Sets protocol - /// version](ExtraRequestSettings#structfield.protocol_version) - pub const fn set_protocol_version(mut self, protocol_version: i32) -> Self { - self.protocol_version = Some(protocol_version); - self - } - /// [Sets gather players](ExtraRequestSettings#structfield.gather_players) - pub const fn set_gather_players(mut self, gather_players: GatherToggle) -> Self { - self.gather_players = Some(gather_players); - self - } - /// [Sets gather rules](ExtraRequestSettings#structfield.gather_rules) - pub const fn set_gather_rules(mut self, gather_rules: GatherToggle) -> Self { - self.gather_rules = Some(gather_rules); - self - } - /// [Sets check app ID](ExtraRequestSettings#structfield.check_app_id) - pub const fn set_check_app_id(mut self, check_app_id: bool) -> Self { - self.check_app_id = Some(check_app_id); - self - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::time::Duration; - - // Test creating new TimeoutSettings with valid durations - #[test] - fn test_new_with_valid_durations() -> GDResult<()> { - // Define valid read and write durations - let read_duration = Duration::from_secs(1); - let write_duration = Duration::from_secs(2); - let connect_duration = Duration::from_secs(3); - - // Create new TimeoutSettings with the valid durations - let timeout_settings = TimeoutSettings::new( - Some(read_duration), - Some(write_duration), - Some(connect_duration), - 0, - )?; - - // Verify that the get_read and get_write methods return the expected values - assert_eq!(timeout_settings.get_read(), Some(read_duration)); - assert_eq!(timeout_settings.get_write(), Some(write_duration)); - - Ok(()) - } - - // Test creating new TimeoutSettings with a zero duration - #[test] - fn test_new_with_zero_duration() { - // Define a zero read duration and a valid write duration - let read_duration = Duration::new(0, 0); - let write_duration = Duration::from_secs(2); - let connect_duration = Duration::from_secs(3); - - // Try to create new TimeoutSettings with the zero read duration (this should - // fail) - let result = TimeoutSettings::new( - Some(read_duration), - Some(write_duration), - Some(connect_duration), - 0, - ); - - // Verify that the function returned an error and that the error type is - // InvalidInput - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), crate::GDErrorKind::InvalidInput.into()); - } - - // Test that the default TimeoutSettings values are correct - #[test] - fn test_default_values() { - // Get the default TimeoutSettings values - let default_settings = TimeoutSettings::default(); - - // Verify that the get_read and get_write methods return the expected default - // values - assert_eq!(default_settings.get_read(), Some(Duration::from_secs(4))); - assert_eq!(default_settings.get_write(), Some(Duration::from_secs(4))); - } - - // Test that extra request settings can be converted - #[test] - fn test_extra_request_settings() { - let settings = ExtraRequestSettings::default(); - - let _: valve::GatheringSettings = settings.into(); - } -} diff --git a/crates/lib/src/protocols/unreal2/mod.rs b/crates/lib/src/protocols/unreal2/mod.rs deleted file mode 100644 index 7cf5e5c..0000000 --- a/crates/lib/src/protocols/unreal2/mod.rs +++ /dev/null @@ -1,58 +0,0 @@ -/// The implementation. -pub mod protocol; -/// All types used by the implementation. -pub mod types; - -pub use protocol::*; -pub use types::*; - -/// Generate a module containing a query function for a valve game. -/// -/// * `mod_name` - The name to be given to the game module (see ID naming -/// conventions in CONTRIBUTING.md). -/// * `pretty_name` - The full name of the game, will be used as the -/// documentation for the created module. -/// * `default_port` - Passed through to [game_query_fn]. -#[cfg(feature = "games")] -macro_rules! game_query_mod { - ($mod_name: ident, $pretty_name: expr, $default_port: literal) => { - #[doc = $pretty_name] - pub mod $mod_name { - crate::protocols::unreal2::game_query_fn!($default_port); - } - }; -} - -#[cfg(feature = "games")] -pub(crate) use game_query_mod; - -// Allow generating doc comments: -// https://users.rust-lang.org/t/macros-filling-text-in-comments/20473 -/// Generate a query function for a valve game. -/// -/// * `default_port` - The default port the game uses. -#[cfg(feature = "games")] -macro_rules! game_query_fn { - ($default_port: literal) => { - crate::protocols::unreal2::game_query_fn! {@gen $default_port, concat!( - "Make a Unreal2 query for with default timeout settings and default extra request settings.\n\n", - "If port is `None`, then the default port (", stringify!($default_port), ") will be used.")} - }; - - (@gen $default_port: literal, $doc: expr) => { - #[doc = $doc] - pub fn query( - address: &std::net::IpAddr, - port: Option, - ) -> crate::GDResult { - crate::protocols::unreal2::query( - &std::net::SocketAddr::new(*address, port.unwrap_or($default_port)), - &crate::protocols::unreal2::GatheringSettings::default(), - None, - ) - } - }; -} - -#[cfg(feature = "games")] -pub(crate) use game_query_fn; diff --git a/crates/lib/src/protocols/unreal2/protocol.rs b/crates/lib/src/protocols/unreal2/protocol.rs deleted file mode 100644 index 5e09d96..0000000 --- a/crates/lib/src/protocols/unreal2/protocol.rs +++ /dev/null @@ -1,311 +0,0 @@ -use crate::buffer::{Buffer, StringDecoder}; -use crate::errors::GDErrorKind::PacketBad; -use crate::protocols::types::TimeoutSettings; -use crate::socket::{Socket, UdpSocket}; -use crate::utils::{maybe_gather, retry_on_timeout}; -use crate::GDResult; - -use super::{GatheringSettings, MutatorsAndRules, PacketKind, Players, Response, ServerInfo}; - -use std::net::SocketAddr; - -use byteorder::{ByteOrder, LittleEndian}; -use encoding_rs::{UTF_16LE, WINDOWS_1252}; - -/// Response packets don't seem to exceed 500 bytes, set to 1024 just to be -/// safe. -const PACKET_SIZE: usize = 1024; - -/// Default amount of players to pre-allocate if numplayers was not included in -/// server info response. -const DEFAULT_PLAYER_PREALLOCATION: usize = 10; - -/// Maximum amount of players to pre-allocate: if the server specifies a number -/// larger than this in serverinfo we don't allocate that many. -const MAXIMUM_PLAYER_PREALLOCATION: usize = 50; - -/// The Unreal2 protocol implementation. -pub(crate) struct Unreal2Protocol { - socket: UdpSocket, - retry_count: usize, -} - -impl Unreal2Protocol { - pub fn new(address: &SocketAddr, timeout_settings: Option) -> GDResult { - let socket = UdpSocket::new(address, &timeout_settings)?; - let retry_count = timeout_settings.as_ref().map_or_else( - || TimeoutSettings::default().get_retries(), - TimeoutSettings::get_retries, - ); - - Ok(Self { - socket, - retry_count, - }) - } - - /// Send a request packet and recieve the first response (with retries). - fn get_request_data(&mut self, packet_type: PacketKind) -> GDResult> { - retry_on_timeout(self.retry_count, move || { - self.get_request_data_impl(packet_type) - }) - } - - /// Send a request packet - fn get_request_data_impl(&mut self, packet_type: PacketKind) -> GDResult> { - let request = [0x79, 0, 0, 0, packet_type as u8]; - self.socket.send(&request)?; - - let data = self.socket.receive(Some(PACKET_SIZE))?; - - Ok(data) - } - - /// Consume the header part of a response packet, validate that the packet - /// type matches what is expected. - fn consume_response_headers( - buffer: &mut Buffer, - expected_packet_type: PacketKind, - ) -> GDResult<()> { - // Skip header - buffer.move_cursor(4)?; - - let packet_type: u8 = buffer.read()?; - - let packet_type: PacketKind = packet_type.try_into()?; - - if packet_type != expected_packet_type { - Err(PacketBad.context(format!( - "Packet response ({packet_type:?}) didn't match request ({expected_packet_type:?}) packet type", - ))) - } else { - Ok(()) - } - } - - /// Send server info query. - pub fn query_server_info(&mut self) -> GDResult { - let data = self.get_request_data(PacketKind::ServerInfo)?; - let mut buffer = Buffer::::new(&data); - // TODO: Maybe put consume headers in individual packet parse methods - Self::consume_response_headers(&mut buffer, PacketKind::ServerInfo)?; - ServerInfo::parse(&mut buffer) - } - - /// Send mutators and rules query. - pub fn query_mutators_and_rules(&mut self) -> GDResult { - // This is a required packet so we validate that we get at least one response. - // However there can be many packets in response to a single request so - // we greedily handle packets until we get a timeout (or any receive - // error). - - let mut mutators_and_rules = MutatorsAndRules::default(); - { - let data = self.get_request_data(PacketKind::MutatorsAndRules)?; - let mut buffer = Buffer::::new(&data); - // TODO: Maybe put consume headers in individual packet parse methods - Self::consume_response_headers(&mut buffer, PacketKind::MutatorsAndRules)?; - mutators_and_rules.parse(&mut buffer)? - }; - - // We could receive multiple packets in response - while let Ok(data) = self.socket.receive(Some(PACKET_SIZE)) { - let mut buffer = Buffer::::new(&data); - - let r = Self::consume_response_headers(&mut buffer, PacketKind::MutatorsAndRules); - if r.is_err() { - println!("{r:?}"); - break; - } - - mutators_and_rules.parse(&mut buffer)?; - } - - Ok(mutators_and_rules) - } - - /// Send players query. - pub fn query_players(&mut self, server_info: Option<&ServerInfo>) -> GDResult { - // Pre-allocate the player arrays, but don't over allocate memory if the server - // specifies an insane number of players. - let num_players: Option = server_info.and_then(|i| i.num_players.try_into().ok()); - - let mut players = Players::with_capacity( - num_players - .unwrap_or(DEFAULT_PLAYER_PREALLOCATION) - .min(MAXIMUM_PLAYER_PREALLOCATION), - ); - - // Fetch first players packet (with retries) - let mut players_data = self.get_request_data(PacketKind::Players); - // Players are non required so if we don't get any responses we continue to - // return - while let Ok(data) = players_data { - let mut buffer = Buffer::::new(&data); - - Self::consume_response_headers(&mut buffer, PacketKind::Players)?; - - players.parse(&mut buffer)?; - - if let Some(num_players) = num_players { - if players.total_len() >= num_players { - // If we have already received the amount of players specified in server info - // then we don't need to wait for more player packets to time out. - break; - } - } - - // Receive next packet - players_data = self.socket.receive(Some(PACKET_SIZE)); - } - - Ok(players) - } - - /// Make a full server query. - pub fn query(&mut self, gather_settings: &GatheringSettings) -> GDResult { - // Fetch the server info, this can only handle one response packet - let mut server_info = self.query_server_info()?; - - let mutators_and_rules = maybe_gather!( - gather_settings.mutators_and_rules, - self.query_mutators_and_rules() - ) - .unwrap_or_default(); - - if let Some(password) = mutators_and_rules.rules.get("GamePassword") { - let string = password.concat().to_lowercase(); - server_info.password = string == "true"; - } - - let players = maybe_gather!( - gather_settings.players, - self.query_players(Some(&server_info)) - ) - .unwrap_or_else(|| Players::with_capacity(0)); - - // TODO: Handle extra info parsing when we detect certain game types (or maybe - // include that in gather settings). - - Ok(Response { - server_info, - mutators_and_rules, - players, - }) - } -} - -/// Unreal 2 string decoder -pub struct Unreal2StringDecoder; -impl StringDecoder for Unreal2StringDecoder { - type Delimiter = [u8; 1]; - - const DELIMITER: Self::Delimiter = [0x00]; - - fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult { - let mut ucs2 = false; - let mut length: usize = (*data - .first() - .ok_or_else(|| PacketBad.context("Tried to decode string without length"))?) - .into(); - - let mut start = 0; - - // Check if it is a UCS-2 string - if length >= 0x80 { - ucs2 = true; - - length = (length & 0x7f) * 2; - - start += 1; - - // For UCS-2 strings, some unreal 2 games randomly insert an extra 0x01 here, - // not included in the length. Skip it if present (hopefully this never happens - // legitimately) - if data[start ..].first() == Some(&1) { - start += 1; - } - } - - // If UCS2 the first byte is the masked length of the string - let result = if ucs2 { - let string_data = &data[start .. start + length]; - if string_data.len() != length { - return Err(PacketBad.context("Not enough data in buffer to read string")); - } - - // When node decodes UCS2 it uses the UFT16LE encoding. - // https://github.com/nodejs/node/blob/2aaa21f9f684484edb54be30589c4af0b923cdef/lib/buffer.js#L637-L645 - let (result, _, invalid_sequences) = UTF_16LE.decode(string_data); - - if invalid_sequences { - return Err(PacketBad.context("UTF-8 string contained invalid character(s)")); - } - - result - } else { - // Else the string is null-delimited latin1 - - // TODO: Replace this with delimiter finder helper - let position = data - // Create an iterator over the data. - .iter() - // Find the position of the delimiter - .position(|&b| b == delimiter.as_ref()[0]) - // If the delimiter is not found, use the whole data slice. - .unwrap_or(data.len()); - - length = position + 1; - - // Decode as latin1 - let (result, _, invalid_sequences) = WINDOWS_1252.decode(&data[0 .. position]); - - if invalid_sequences { - return Err(PacketBad.context("latin1 string contained invalid character(s)")); - } - - result - }; - - // Strip color encodings - // TODO: Improve efficiency - // TODO: There might be a nicer way to do this once string patterns are stable - // https://github.com/rust-lang/rust/issues/27721 - - // After '0x1b' skip 3 characters (including the '0x1b') - let mut char_skip = 0usize; - let result: String = result - .chars() - .filter(|c: &char| { - if '\x1b'.eq(c) { - char_skip = 4; - return false; - } - char_skip = char_skip.saturating_sub(1); - - char_skip == 0 - }) - .collect(); - - // Remove all characters between 0x00 and 0x1a - let result = result.replace(|c: char| c > '\x00' && c <= '\x1a', ""); - - *cursor += start + length; - - // Strip delimiter that wasn't included in length - Ok(result.trim_matches('\0').to_string()) - } -} - -/// Make an unreal2 query. -pub fn query( - address: &SocketAddr, - gather_settings: &GatheringSettings, - timeout_settings: Option, -) -> GDResult { - let mut client = Unreal2Protocol::new(address, timeout_settings)?; - - client.query(gather_settings) -} - -// TODO: Add tests diff --git a/crates/lib/src/protocols/unreal2/types.rs b/crates/lib/src/protocols/unreal2/types.rs deleted file mode 100644 index 96316be..0000000 --- a/crates/lib/src/protocols/unreal2/types.rs +++ /dev/null @@ -1,250 +0,0 @@ -use crate::buffer::Buffer; -use crate::errors::GDErrorKind::PacketBad; -use crate::protocols::types::{CommonPlayer, CommonResponse, ExtraRequestSettings, GatherToggle, GenericPlayer}; -use crate::protocols::GenericResponse; -use crate::{GDError, GDResult}; - -use super::Unreal2StringDecoder; - -use std::collections::{HashMap, HashSet}; - -use byteorder::ByteOrder; - -/// Unreal 2 packet types. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[repr(u8)] -pub enum PacketKind { - ServerInfo = 0, - MutatorsAndRules = 1, - Players = 2, -} - -impl TryFrom for PacketKind { - type Error = GDError; - fn try_from(value: u8) -> GDResult { - match value { - 0 => Ok(Self::ServerInfo), - 1 => Ok(Self::MutatorsAndRules), - 2 => Ok(Self::Players), - _ => Err(PacketBad.context("Unknown packet type")), - } - } -} - -/// Unreal 2 server info. -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct ServerInfo { - pub server_id: u32, - pub ip: String, - pub game_port: u32, - pub query_port: u32, - pub name: String, - pub map: String, - pub game_type: String, - pub num_players: u32, - pub max_players: u32, - pub password: bool, -} - -impl ServerInfo { - pub fn parse(buffer: &mut Buffer) -> GDResult { - Ok(Self { - server_id: buffer.read()?, - ip: buffer.read_string::(None)?, - game_port: buffer.read()?, - query_port: buffer.read()?, - name: buffer.read_string::(None)?, - map: buffer.read_string::(None)?, - game_type: buffer.read_string::(None)?, - num_players: buffer.read()?, - max_players: buffer.read()?, - password: false, - }) - } -} - -/// Unreal 2 mutators and rules. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct MutatorsAndRules { - pub mutators: HashSet, - pub rules: HashMap>, -} - -impl MutatorsAndRules { - pub fn parse(&mut self, buffer: &mut Buffer) -> GDResult<()> { - while buffer.remaining_length() > 0 { - let key = buffer.read_string::(None)?; - let value = buffer.read_string::(None).ok(); - - if key.eq_ignore_ascii_case("mutator") { - if let Some(value) = value { - self.mutators.insert(value); - } - } else { - let rule_vec = self.rules.get_mut(&key); - - let rule_vec = if let Some(rule_vec) = rule_vec { - rule_vec - } else { - self.rules.insert(key.clone(), Vec::default()); - self.rules - .get_mut(&key) - .expect("Value should be in HashMap after we inserted") - }; - - if let Some(value) = value { - rule_vec.push(value); - } - } - } - Ok(()) - } -} - -/// Unreal 2 players and bots. -#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Players { - /// List of players returned by server (without 0 ping). - pub players: Vec, - /// List of bots returned by server (players with 0 ping). - pub bots: Vec, -} - -impl Players { - /// Pre-allocate the vectors inside the players struct based on the provided - /// capacity. - pub fn with_capacity(capacity: usize) -> Self { - Self { - players: Vec::with_capacity(capacity), - // Allocate half as many bots as we don't expect there to be as many - bots: Vec::with_capacity(capacity / 2), - } - } - - /// Parse a raw buffer of players into the current struct. - pub fn parse(&mut self, buffer: &mut Buffer) -> GDResult<()> { - while buffer.remaining_length() > 0 { - let player = Player { - id: buffer.read()?, - name: buffer.read_string::(None)?, - ping: buffer.read()?, - score: buffer.read()?, - stats_id: buffer.read()?, - }; - - // If ping is 0 the player is a bot - if player.ping == 0 { - self.bots.push(player); - } else { - self.players.push(player); - } - } - - Ok(()) - } - - /// Length of both players and bots. - pub fn total_len(&self) -> usize { self.players.len() + self.bots.len() } -} - -/// Unreal 2 player info. -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Player { - pub id: u32, - pub name: String, - pub ping: u32, - pub score: i32, - pub stats_id: u32, -} - -impl CommonPlayer for Player { - fn name(&self) -> &str { &self.name } - - fn score(&self) -> Option { Some(self.score) } - - fn as_original(&self) -> GenericPlayer<'_> { GenericPlayer::Unreal2(self) } -} - -/// Unreal 2 response. -#[derive(Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Response { - pub server_info: ServerInfo, - pub mutators_and_rules: MutatorsAndRules, - pub players: Players, -} - -impl CommonResponse for Response { - fn as_original(&self) -> GenericResponse<'_> { GenericResponse::Unreal2(self) } - - fn name(&self) -> Option<&str> { Some(&self.server_info.name) } - - fn game_mode(&self) -> Option<&str> { Some(&self.server_info.game_type) } - - fn map(&self) -> Option<&str> { Some(&self.server_info.map) } - - fn players_maximum(&self) -> u32 { self.server_info.max_players } - - fn players_online(&self) -> u32 { self.server_info.num_players } - - fn has_password(&self) -> Option { Some(self.server_info.password) } - - fn players(&self) -> Option> { - Some( - self.players - .players - .iter() - .map(|player| player as _) - .collect(), - ) - } -} - -/// What data to gather, purely used only with the query function. -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct GatheringSettings { - pub players: GatherToggle, - pub mutators_and_rules: GatherToggle, -} - -impl GatheringSettings { - /// Default values is attempt both players and rules. - pub const fn default() -> Self { - Self { - players: GatherToggle::Try, - mutators_and_rules: GatherToggle::Enforce, - } - } - - pub const fn into_extra(self) -> ExtraRequestSettings { - ExtraRequestSettings { - hostname: None, - protocol_version: None, - gather_players: Some(self.players), - gather_rules: Some(self.mutators_and_rules), - check_app_id: None, - } - } -} - -impl Default for GatheringSettings { - fn default() -> Self { Self::default() } -} - -impl From for GatheringSettings { - fn from(value: ExtraRequestSettings) -> Self { - let default = Self::default(); - Self { - players: value.gather_players.unwrap_or(default.players), - mutators_and_rules: value.gather_rules.unwrap_or(default.mutators_and_rules), - } - } -} - -// TODO: Add tests diff --git a/crates/lib/src/protocols/valve/mod.rs b/crates/lib/src/protocols/valve/mod.rs deleted file mode 100644 index cb5b634..0000000 --- a/crates/lib/src/protocols/valve/mod.rs +++ /dev/null @@ -1,81 +0,0 @@ -/// The implementation. -pub mod protocol; -/// All types used by the implementation. -pub mod types; - -pub use protocol::*; -pub use types::*; - -/// Generate a module containing a query function for a valve game. -/// -/// * `mod_name` - The name to be given to the game module (see ID naming -/// conventions in CONTRIBUTING.md). -/// * `pretty_name` - The full name of the game, will be used as the -/// documentation for the created module. -/// * `steam_app`, `default_port` - Passed through to [game_query_fn]. -#[cfg(feature = "games")] -macro_rules! game_query_mod { - ($mod_name: ident, $pretty_name: expr, $engine: expr, $default_port: literal) => { - crate::protocols::valve::game_query_mod!( - $mod_name, - $pretty_name, - $engine, - $default_port, - GatheringSettings::default() - ); - }; - - ($mod_name: ident, $pretty_name: expr, $engine: expr, $default_port: literal, $gathering_settings: expr) => { - #[doc = $pretty_name] - pub mod $mod_name { - #[allow(unused_imports)] - use crate::protocols::{ - types::GatherToggle, - valve::{Engine, GatheringSettings}, - }; - - crate::protocols::valve::game_query_fn!($pretty_name, $engine, $default_port, $gathering_settings); - } - }; -} - -#[cfg(feature = "games")] -pub(crate) use game_query_mod; - -// Allow generating doc comments: -// https://users.rust-lang.org/t/macros-filling-text-in-comments/20473 -/// Generate a query function for a valve game. -/// -/// * `engine` - The [Engine] that the game uses. -/// * `default_port` - The default port the game uses. -/// -/// ```rust,ignore -/// use crate::protocols::valve::game_query_fn; -/// game_query_fn!(TEAMFORTRESS2, 27015); -/// ``` -#[cfg(feature = "games")] -macro_rules! game_query_fn { - ($pretty_name: expr, $engine: expr, $default_port: literal, $gathering_settings: expr) => { - // TODO: By using $gathering_settings, also add to doc if a game doesnt respond to certain gathering settings - crate::protocols::valve::game_query_fn!{@gen $engine, $default_port, concat!( - "Make a valve query for ", $pretty_name, " with default timeout settings and default extra request settings.\n\n", - "If port is `None`, then the default port (", stringify!($default_port), ") will be used."), $gathering_settings} - }; - - (@gen $engine: expr, $default_port: literal, $doc: expr, $gathering_settings: expr) => { - #[doc = $doc] - pub fn query(address: &std::net::IpAddr, port: Option) -> crate::GDResult { - let valve_response = crate::protocols::valve::query( - &std::net::SocketAddr::new(*address, port.unwrap_or($default_port)), - $engine, - Some($gathering_settings), - None, - )?; - - Ok(crate::protocols::valve::game::Response::new_from_valve_response(valve_response)) - } - }; -} - -#[cfg(feature = "games")] -pub(crate) use game_query_fn; diff --git a/crates/lib/src/protocols/valve/protocol.rs b/crates/lib/src/protocols/valve/protocol.rs deleted file mode 100644 index 04a83ea..0000000 --- a/crates/lib/src/protocols/valve/protocol.rs +++ /dev/null @@ -1,482 +0,0 @@ -use crate::{ - buffer::Buffer, - protocols::{ - types::TimeoutSettings, - valve::{ - types::{ - Environment, - ExtraData, - GatheringSettings, - Request, - Response, - Server, - ServerInfo, - ServerPlayer, - TheShip, - }, - Engine, - ModData, - }, - }, - socket::{Socket, UdpSocket}, - utils::{maybe_gather, retry_on_timeout, u8_lower_upper}, - GDErrorKind::{BadGame, Decompress, UnknownEnumCast}, - GDResult, -}; - -use bzip2_rs::decoder::Decoder; - -use crate::buffer::Utf8Decoder; -use crate::protocols::valve::Packet; -use byteorder::LittleEndian; -use std::collections::HashMap; -use std::net::SocketAddr; - -#[derive(Debug)] -#[allow(dead_code)] // remove this later on -struct SplitPacket { - pub header: u32, - pub id: u32, - pub total: u8, - pub number: u8, - pub size: u16, - /// None means its not compressed, Some means it is - /// and it contains (size and crc32) - pub decompressed: Option<(u32, u32)>, - payload: Vec, -} - -impl SplitPacket { - fn new(engine: &Engine, protocol: u8, buffer: &mut Buffer) -> GDResult { - let header = buffer.read()?; // buffer.get_u32()?; - let id = buffer.read()?; - let (total, number, size, decompressed) = match engine { - Engine::GoldSrc(_) => { - let (lower, upper) = u8_lower_upper(buffer.read()?); - (lower, upper, 0, None) - } - Engine::Source(_) => { - let total = buffer.read()?; - let number = buffer.read()?; - let size = match protocol == 7 && (*engine == Engine::new(240)) { - // certain apps with protocol = 7 dont have this field, such as CSS - false => buffer.read()?, - true => 1248, - }; - - let is_compressed = ((id >> 31) & 1u32) == 1u32; - let decompressed = match is_compressed { - false => None, - true => Some((buffer.read()?, buffer.read()?)), - }; - - (total, number, size, decompressed) - } - }; - - Ok(Self { - header, - id, - total, - number, - size, - decompressed, - payload: buffer.remaining_bytes().to_vec(), - }) - } - - fn get_payload(&self) -> GDResult> { - if let Some(decompressed) = self.decompressed { - let mut decoder = Decoder::new(); - decoder - .write(&self.payload) - .map_err(|e| Decompress.context(e))?; - - let decompressed_size = decompressed.0 as usize; - - let mut decompressed_payload = vec![0; decompressed_size]; - - decoder - .read(&mut decompressed_payload) - .map_err(|e| Decompress.context(e))?; - - if decompressed_payload.len() != decompressed_size - || crc32fast::hash(&decompressed_payload) != decompressed.1 - { - Err(Decompress.context(format!( - "Decompressed size {} was not expected {}", - decompressed_payload.len(), - decompressed_size - ))) - } else { - Ok(decompressed_payload) - } - } else { - Ok(self.payload.clone()) - } - } -} - -pub(crate) struct ValveProtocol { - socket: UdpSocket, - retry_count: usize, -} - -static PACKET_SIZE: usize = 6144; - -impl ValveProtocol { - pub fn new(address: &SocketAddr, timeout_settings: Option) -> GDResult { - let socket = UdpSocket::new(address, &timeout_settings)?; - let retry_count = timeout_settings.as_ref().map_or_else( - || TimeoutSettings::default().get_retries(), - TimeoutSettings::get_retries, - ); - - Ok(Self { - socket, - retry_count, - }) - } - - fn receive(&mut self, engine: &Engine, protocol: u8, buffer_size: usize) -> GDResult { - let data = self.socket.receive(Some(buffer_size))?; - let mut buffer = Buffer::::new(&data); - - let header: u8 = buffer.read()?; - buffer.move_cursor(-1)?; - if header == 0xFE { - // the packet is split - let mut main_packet = SplitPacket::new(engine, protocol, &mut buffer)?; - let mut chunk_packets = Vec::with_capacity((main_packet.total - 1) as usize); - - for _ in 1 .. main_packet.total { - let new_data = self.socket.receive(Some(buffer_size))?; - buffer = Buffer::::new(&new_data); - let chunk_packet = SplitPacket::new(engine, protocol, &mut buffer)?; - chunk_packets.push(chunk_packet); - } - - chunk_packets.sort_by(|a, b| a.number.cmp(&b.number)); - - for chunk_packet in chunk_packets { - main_packet.payload.extend(chunk_packet.payload); - } - - let payload = main_packet.get_payload()?; // Creating a non-temporary value here - let mut new_packet_buffer = Buffer::::new(&payload); // Using the non-temporary value here - Ok(Packet::new_from_bufferer(&mut new_packet_buffer)?) - } else { - Packet::new_from_bufferer(&mut buffer) - } - } - - pub fn get_kind_request_data(&mut self, engine: &Engine, protocol: u8, kind: Request) -> GDResult> { - let data = self.get_request_data(engine, protocol, kind as u8, kind.get_default_payload())?; - Ok(data) - } - - /// Ask for a specific request only. - /// This function will retry fetch on timeouts. - pub fn get_request_data(&mut self, engine: &Engine, protocol: u8, kind: u8, payload: Vec) -> GDResult> { - retry_on_timeout(self.retry_count, || { - self.get_request_data_impl(engine, protocol, kind, payload.clone()) - }) - } - - /// Ask for a specific request only (without retry logic). - fn get_request_data_impl( - &mut self, - engine: &Engine, - protocol: u8, - kind: u8, - payload: Vec, - ) -> GDResult> { - let request_initial_packet = Packet::new(kind, payload).to_bytes(); - self.socket.send(&request_initial_packet)?; - - let mut packet = self.receive(engine, protocol, PACKET_SIZE)?; - while packet.kind == 0x41 { - // 'A' - let challenge = packet.payload; - - const INFO: u8 = Request::Info as u8; - let challenge_packet = Packet::new( - kind, - match kind { - INFO => [Request::Info.get_default_payload(), challenge].concat(), - _ => challenge, - }, - ) - .to_bytes(); - - self.socket.send(&challenge_packet)?; - - packet = self.receive(engine, protocol, PACKET_SIZE)?; - } - - Ok(packet.payload) - } - - fn get_goldsrc_server_info(buffer: &mut Buffer) -> GDResult { - let _header: u8 = buffer.read()?; // get the header (useless info) - let _address: String = buffer.read_string::(None)?; // get the server address (useless info) - let name = buffer.read_string::(None)?; - let map = buffer.read_string::(None)?; - let folder = buffer.read_string::(None)?; - let game_mode = buffer.read_string::(None)?; - let players = buffer.read()?; - let max_players = buffer.read()?; - let protocol = buffer.read()?; - let server_type = match buffer.read::()? { - 68 => Server::Dedicated, //'D' - 76 => Server::NonDedicated, //'L' - 80 => Server::TV, //'P' - _ => Err(UnknownEnumCast)?, - }; - let environment_type = match buffer.read::()? { - 76 => Environment::Linux, //'L' - 87 => Environment::Windows, //'W' - _ => Err(UnknownEnumCast)?, - }; - let has_password = buffer.read::()? == 1; - let is_mod = buffer.read::()? == 1; - let mod_data = match is_mod { - false => None, - true => { - Some(ModData { - link: buffer.read_string::(None)?, - download_link: buffer.read_string::(None)?, - version: buffer.read()?, - size: buffer.read()?, - multiplayer_only: buffer.read::()? == 1, - has_own_dll: buffer.read::()? == 1, - }) - } - }; - let vac_secured = buffer.read::()? == 1; - let bots = buffer.read::()?; - - Ok(ServerInfo { - protocol_version: protocol, - name, - map, - folder, - game_mode, - appid: 0, // not present in the obsolete response - players_online: players, - players_maximum: max_players, - players_bots: bots, - server_type, - environment_type, - has_password, - vac_secured, - the_ship: None, - game_version: String::new(), // a version field only for the mod - extra_data: None, - is_mod, - mod_data, - }) - } - - /// Get the server information's. - fn get_server_info(&mut self, engine: &Engine) -> GDResult { - let data = self.get_kind_request_data(engine, 0, Request::Info)?; - let mut buffer = Buffer::::new(&data); - - if let Engine::GoldSrc(force) = engine { - if *force { - return Self::get_goldsrc_server_info(&mut buffer); - } - } - - let protocol = buffer.read()?; - let name = buffer.read_string::(None)?; - let map = buffer.read_string::(None)?; - let folder = buffer.read_string::(None)?; - let game_mode = buffer.read_string::(None)?; - let mut appid = buffer.read::()? as u32; - let players = buffer.read()?; - let max_players = buffer.read()?; - let bots = buffer.read()?; - let server_type = Server::from_gldsrc(buffer.read()?)?; - let environment_type = Environment::from_gldsrc(buffer.read()?)?; - let has_password = buffer.read::()? == 1; - let vac_secured = buffer.read::()? == 1; - let the_ship = match *engine == Engine::new(2400) { - false => None, - true => { - Some(TheShip { - mode: buffer.read()?, - witnesses: buffer.read()?, - duration: buffer.read()?, - }) - } - }; - let game_version = buffer.read_string::(None)?; - let extra_data = match buffer.read::() { - Err(_) => None, - Ok(value) => { - Some(ExtraData { - port: match (value & 0x80) > 0 { - false => None, - true => Some(buffer.read()?), - }, - steam_id: match (value & 0x10) > 0 { - false => None, - true => Some(buffer.read()?), - }, - tv_port: match (value & 0x40) > 0 { - false => None, - true => Some(buffer.read()?), - }, - tv_name: match (value & 0x40) > 0 { - false => None, - true => Some(buffer.read_string::(None)?), - }, - keywords: match (value & 0x20) > 0 { - false => None, - true => Some(buffer.read_string::(None)?), - }, - game_id: match (value & 0x01) > 0 { - false => None, - true => { - let gid = buffer.read()?; - appid = (gid & ((1 << 24) - 1)) as u32; - - Some(gid) - } - }, - }) - } - }; - - Ok(ServerInfo { - protocol_version: protocol, - name, - map, - folder, - game_mode, - appid, - players_online: players, - players_maximum: max_players, - players_bots: bots, - server_type, - environment_type, - has_password, - vac_secured, - the_ship, - game_version, - extra_data, - is_mod: false, - mod_data: None, - }) - } - - /// Get the server player's. - fn get_server_players(&mut self, engine: &Engine, protocol: u8) -> GDResult> { - let data = self.get_kind_request_data(engine, protocol, Request::Players)?; - let mut buffer = Buffer::::new(&data); - - let count = buffer.read::()? as usize; - let mut players: Vec = Vec::with_capacity(count); - - for _ in 0 .. count { - buffer.move_cursor(1)?; // skip the index byte - - players.push(ServerPlayer { - name: buffer.read_string::(None)?, - score: buffer.read()?, - duration: buffer.read()?, - deaths: match *engine == Engine::new(2400) { - false => None, - true => Some(buffer.read()?), - }, - money: match *engine == Engine::new(2400) { - false => None, - true => Some(buffer.read()?), - }, - }); - } - - Ok(players) - } - - /// Get the server's rules. - fn get_server_rules(&mut self, engine: &Engine, protocol: u8) -> GDResult> { - let data = self.get_kind_request_data(engine, protocol, Request::Rules)?; - let mut buffer = Buffer::::new(&data); - - let count = buffer.read::()? as usize; - let mut rules: HashMap = HashMap::with_capacity(count); - - for _ in 0 .. count { - let name = buffer.read_string::(None)?; - let value = buffer.read_string::(None)?; - - rules.insert(name, value); - } - - if *engine == Engine::new(632_360) { - // ROR2 - rules.remove("Test"); - } - - Ok(rules) - } -} - -/// Query a server by providing the address, the port, the app, gather and -/// timeout settings. Providing None to the settings results in using the -/// default values for them -/// (GatherSettings::[default](GatheringSettings::default), -/// TimeoutSettings::[default](TimeoutSettings::default)). -pub fn query( - address: &SocketAddr, - engine: Engine, - gather_settings: Option, - timeout_settings: Option, -) -> GDResult { - let response_gather_settings = gather_settings.unwrap_or_default(); - get_response(address, engine, response_gather_settings, timeout_settings) -} - -fn get_response( - address: &SocketAddr, - engine: Engine, - gather_settings: GatheringSettings, - timeout_settings: Option, -) -> GDResult { - let mut client = ValveProtocol::new(address, timeout_settings)?; - - let info = client.get_server_info(&engine)?; - - if let Engine::Source(Some(appids)) = &engine { - let mut is_specified_id = false; - - if appids.0 == info.appid { - is_specified_id = true; - } else if let Some(dedicated_appid) = appids.1 { - if dedicated_appid == info.appid { - is_specified_id = true; - } - } - - if !is_specified_id && gather_settings.check_app_id { - return Err(BadGame.context(format!("AppId: {}", info.appid))); - } - } - - let protocol = info.protocol_version; - - Ok(Response { - info, - players: maybe_gather!( - gather_settings.players, - client.get_server_players(&engine, protocol) - ), - rules: maybe_gather!( - gather_settings.rules, - client.get_server_rules(&engine, protocol) - ), - }) -} diff --git a/crates/lib/src/protocols/valve/types.rs b/crates/lib/src/protocols/valve/types.rs deleted file mode 100644 index 46629ab..0000000 --- a/crates/lib/src/protocols/valve/types.rs +++ /dev/null @@ -1,437 +0,0 @@ -use std::collections::HashMap; - -use crate::protocols::types::{CommonPlayer, CommonResponse, ExtraRequestSettings, GatherToggle, GenericPlayer}; -use crate::GDErrorKind::UnknownEnumCast; -use crate::GDResult; -use crate::{buffer::Buffer, protocols::GenericResponse}; -use byteorder::LittleEndian; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -/// The type of the server. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum Server { - Dedicated, - NonDedicated, - TV, -} - -impl Server { - pub(crate) fn from_gldsrc(value: u8) -> GDResult { - Ok(match value.to_ascii_lowercase() { - 100 => Self::Dedicated, //'d' - 108 => Self::NonDedicated, //'l' - 112 => Self::TV, //'p' - _ => Err(UnknownEnumCast)?, - }) - } -} - -/// The Operating System that the server is on. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum Environment { - Linux, - Windows, - Mac, -} - -impl Environment { - pub(crate) fn from_gldsrc(value: u8) -> GDResult { - Ok(match value.to_ascii_lowercase() { - 108 => Self::Linux, //'l' - 119 => Self::Windows, //'w' - 109 | 111 => Self::Mac, //'m' or 'o' - _ => Err(UnknownEnumCast)?, - }) - } -} - -/// A query response. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq)] -pub struct Response { - pub info: ServerInfo, - pub players: Option>, - pub rules: Option>, -} - -impl CommonResponse for Response { - fn as_original(&self) -> GenericResponse<'_> { GenericResponse::Valve(self) } - - fn name(&self) -> Option<&str> { Some(&self.info.name) } - fn game_mode(&self) -> Option<&str> { Some(&self.info.game_mode) } - fn game_version(&self) -> Option<&str> { Some(&self.info.game_version) } - fn map(&self) -> Option<&str> { Some(&self.info.map) } - fn players_maximum(&self) -> u32 { self.info.players_maximum.into() } - fn players_online(&self) -> u32 { self.info.players_online.into() } - fn players_bots(&self) -> Option { Some(self.info.players_bots.into()) } - fn has_password(&self) -> Option { Some(self.info.has_password) } - - fn players(&self) -> Option> { - self.players - .as_ref() - .map(|p| p.iter().map(|p| p as &dyn CommonPlayer).collect()) - } -} - -/// General server information's. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct ServerInfo { - /// Protocol used by the server. - pub protocol_version: u8, - /// Name of the server. - pub name: String, - /// Map name. - pub map: String, - /// Name of the folder containing the game files. - pub folder: String, - /// The server-declared name of the game/game mode. - pub game_mode: String, - /// [Steam Application ID](https://developer.valvesoftware.com/wiki/Steam_Application_ID) of game. - pub appid: u32, - /// Number of players on the server. - pub players_online: u8, - /// Maximum number of players the server reports it can hold. - pub players_maximum: u8, - /// Number of bots on the server. - pub players_bots: u8, - /// Dedicated, NonDedicated or SourceTV - pub server_type: Server, - /// The Operating System that the server is on. - pub environment_type: Environment, - /// Indicates whether the server requires a password. - pub has_password: bool, - /// Indicates whether the server uses VAC. - pub vac_secured: bool, - /// [The ship](https://developer.valvesoftware.com/wiki/The_Ship) extra data - pub the_ship: Option, - /// Version of the game installed on the server. - pub game_version: String, - /// Some extra data that the server might provide or not. - pub extra_data: Option, - /// GoldSrc only: Indicates whether the hosted game is a mod. - pub is_mod: bool, - /// GoldSrc only: If the game is a mod, provide additional data. - pub mod_data: Option, -} - -/// A server player. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, PartialOrd)] -pub struct ServerPlayer { - /// Player's name. - pub name: String, - /// General score. - pub score: i32, - /// How long a player has been in the server (seconds). - pub duration: f32, - /// Only for [the ship](https://developer.valvesoftware.com/wiki/The_Ship): deaths count - pub deaths: Option, // the_ship - /// Only for [the ship](https://developer.valvesoftware.com/wiki/The_Ship): money amount - pub money: Option, // the_ship -} - -impl CommonPlayer for ServerPlayer { - fn as_original(&self) -> GenericPlayer<'_> { GenericPlayer::Valve(self) } - fn name(&self) -> &str { &self.name } - fn score(&self) -> Option { Some(self.score) } -} - -/// Only present for [the ship](https://developer.valvesoftware.com/wiki/The_Ship). -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct TheShip { - pub mode: u8, - pub witnesses: u8, - pub duration: u8, -} - -/// Some extra data that the server might provide or not. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct ExtraData { - /// The server's game port number. - pub port: Option, - /// Server's SteamID. - pub steam_id: Option, - /// SourceTV's port. - pub tv_port: Option, - /// SourceTV's name. - pub tv_name: Option, - /// Keywords that describe the server according to it. - pub keywords: Option, - /// The server's 64-bit GameID. - pub game_id: Option, -} - -/// Data related to GoldSrc Mod response. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct ModData { - pub link: String, - pub download_link: String, - pub version: u32, - pub size: u32, - pub multiplayer_only: bool, - pub has_own_dll: bool, -} - -pub(crate) type ExtractedData = ( - Option, - Option, - Option, - Option, - Option, -); - -pub(crate) fn get_optional_extracted_data(data: Option) -> ExtractedData { - match data { - None => (None, None, None, None, None), - Some(ed) => (ed.port, ed.steam_id, ed.tv_port, ed.tv_name, ed.keywords), - } -} - -#[derive(Debug, Clone)] -pub(crate) struct Packet { - pub header: u32, - pub kind: u8, - pub payload: Vec, -} - -impl Packet { - pub fn new(kind: u8, payload: Vec) -> Self { - Self { - header: u32::MAX, // FF FF FF FF - kind, - payload, - } - } - - pub fn new_from_bufferer(buffer: &mut Buffer) -> GDResult { - Ok(Self { - header: buffer.read::()?, - kind: buffer.read::()?, - payload: buffer.remaining_bytes().to_vec(), - }) - } - - pub fn to_bytes(&self) -> Vec { - let mut buf = Vec::from(self.header.to_be_bytes()); - - buf.push(self.kind); - buf.extend(&self.payload); - - buf - } -} - -/// The type of the request, see the [protocol](https://developer.valvesoftware.com/wiki/Server_queries). -#[derive(Eq, PartialEq, Copy, Clone)] -#[repr(u8)] -pub(crate) enum Request { - /// Known as `A2S_INFO` - Info = 0x54, - /// Known as `A2S_PLAYERS` - Players = 0x55, - /// Known as `A2S_RULES` - Rules = 0x56, -} - -impl Request { - pub fn get_default_payload(self) -> Vec { - match self { - Self::Info => String::from("Source Engine Query\0").into_bytes(), - _ => vec![0xFF, 0xFF, 0xFF, 0xFF], - } - } -} - -/// Every supported Valve game references this enum, represents the behaviour -/// of server requests and responses. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum Engine { - /// A Source game, the argument represents the possible steam app ids. - /// If its **None**, let the query find it (could come with some drawbacks, - /// some games do not respond on certain protocol versions (CSS on 7), - /// some have additional data (The Ship). - /// If its **Some**, the first value is the main steam app id, the second - /// could be a secondly used id, as some games use a different one for - /// dedicated servers. Beware if **check_app_id** is set to true in - /// [GatheringSettings], as the query will fail if the server doesnt respond - /// with the expected ids. - Source(Option<(u32, Option)>), - /// A GoldSrc game, the argument indicates whether to enforce - /// requesting the obsolete A2S_INFO response or not. - GoldSrc(bool), -} - -impl Engine { - pub const fn new(appid: u32) -> Self { Self::Source(Some((appid, None))) } - - pub const fn new_gold_src(force: bool) -> Self { Self::GoldSrc(force) } - - pub const fn new_with_dedicated(appid: u32, dedicated_appid: u32) -> Self { - Self::Source(Some((appid, Some(dedicated_appid)))) - } -} - -/// What data to gather, purely used only with the query function. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct GatheringSettings { - pub players: GatherToggle, - pub rules: GatherToggle, - pub check_app_id: bool, -} - -impl GatheringSettings { - /// Default values are try to gather but don't fail on timeout for both - /// players and rules. - pub const fn default() -> Self { - Self { - players: GatherToggle::Try, - rules: GatherToggle::Try, - check_app_id: true, - } - } - - pub const fn into_extra(self) -> ExtraRequestSettings { - ExtraRequestSettings { - hostname: None, - protocol_version: None, - gather_players: Some(self.players), - gather_rules: Some(self.rules), - check_app_id: Some(self.check_app_id), - } - } -} - -impl Default for GatheringSettings { - fn default() -> Self { Self::default() } -} - -impl From for GatheringSettings { - fn from(value: ExtraRequestSettings) -> Self { - let default = Self::default(); - Self { - players: value.gather_players.unwrap_or(default.players), - rules: value.gather_rules.unwrap_or(default.rules), - check_app_id: value.check_app_id.unwrap_or(default.check_app_id), - } - } -} - -/// Generic response types that are used by many games, they are the protocol -/// ones, but without the unnecessary bits (example: the **The Ship**-only -/// fields). -pub mod game { - use super::{Server, ServerPlayer}; - use crate::protocols::valve::types::get_optional_extracted_data; - use std::collections::HashMap; - - #[cfg(feature = "serde")] - use serde::{Deserialize, Serialize}; - - /// A player's details. - #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] - #[derive(Debug, Clone, PartialEq, PartialOrd)] - pub struct Player { - /// Player's name. - pub name: String, - /// Player's score. - pub score: i32, - /// How long a player has been in the server (seconds). - pub duration: f32, - } - - impl Player { - pub fn from_valve_response(player: &ServerPlayer) -> Self { - Self { - name: player.name.clone(), - score: player.score, - duration: player.duration, - } - } - } - - /// The query response. - #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] - #[derive(Debug, Clone, PartialEq)] - pub struct Response { - /// Protocol used by the server. - pub protocol: u8, - /// Name of the server. - pub name: String, - /// Map name. - pub map: String, - /// The name of the game. - pub game: String, - /// Server's app id. - pub appid: u32, - /// Number of players on the server. - pub players_online: u8, - /// Details about the server's players (not all players necessarily). - pub players_details: Vec, - /// Maximum number of players the server reports it can hold. - pub players_maximum: u8, - /// Number of bots on the server. - pub players_bots: u8, - /// Dedicated, NonDedicated or SourceTV - pub server_type: Server, - /// Indicates whether the server requires a password. - pub has_password: bool, - /// Indicated whether the server uses VAC. - pub vac_secured: bool, - /// Version of the game installed on the server. - pub version: String, - /// The server's reported connection port. - pub port: Option, - /// Server's SteamID. - pub steam_id: Option, - /// SourceTV's connection port. - pub tv_port: Option, - /// SourceTV's name. - pub tv_name: Option, - /// Keywords that describe the server according to it. - pub keywords: Option, - /// Server's rules. - pub rules: HashMap, - } - - impl Response { - pub fn new_from_valve_response(response: super::Response) -> Self { - let (port, steam_id, tv_port, tv_name, keywords) = get_optional_extracted_data(response.info.extra_data); - - Self { - protocol: response.info.protocol_version, - name: response.info.name, - map: response.info.map, - game: response.info.game_mode, - appid: response.info.appid, - players_online: response.info.players_online, - players_details: response - .players - .unwrap_or_default() - .iter() - .map(Player::from_valve_response) - .collect(), - players_maximum: response.info.players_maximum, - players_bots: response.info.players_bots, - server_type: response.info.server_type, - has_password: response.info.has_password, - vac_secured: response.info.vac_secured, - version: response.info.game_version, - port, - steam_id, - tv_port, - tv_name, - keywords, - rules: response.rules.unwrap_or_default(), - } - } - } -} diff --git a/crates/lib/src/services/minetest_master_server/mod.rs b/crates/lib/src/services/minetest_master_server/mod.rs deleted file mode 100644 index 908f8f6..0000000 --- a/crates/lib/src/services/minetest_master_server/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -/// The implementation. -pub mod service; -/// All types used by the implementation. -pub mod types; - -pub use service::*; -pub use types::*; diff --git a/crates/lib/src/services/minetest_master_server/service.rs b/crates/lib/src/services/minetest_master_server/service.rs deleted file mode 100644 index 1829866..0000000 --- a/crates/lib/src/services/minetest_master_server/service.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::http::HttpClient; -use crate::minetest_master_server::types::Response; -use crate::{GDResult, TimeoutSettings}; - -pub fn query(timeout_settings: TimeoutSettings) -> GDResult { - let mut client = HttpClient::from_url( - "https://servers.minetest.net", - &Some(timeout_settings), - None, - )?; - - client.get_json("/list", None) -} diff --git a/crates/lib/src/services/minetest_master_server/types.rs b/crates/lib/src/services/minetest_master_server/types.rs deleted file mode 100644 index 88486d7..0000000 --- a/crates/lib/src/services/minetest_master_server/types.rs +++ /dev/null @@ -1,46 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct Server { - pub address: String, - pub clients: u32, - pub clients_list: Option>, - pub clients_max: u32, - pub creative: Option, - pub damage: bool, - pub description: String, - pub game_time: u32, - pub gameid: String, - pub lag: Option, - pub name: String, - pub password: Option, - pub port: u16, - pub proto_max: u16, - pub proto_min: u16, - pub pvp: bool, - pub uptime: u32, - pub url: Option, - pub version: String, - pub ip: String, - pub update_time: u32, - pub start: u32, - pub clients_top: u32, - pub updates: u32, - pub total_clients: u32, - pub pop_v: f32, - pub geo_continent: Option, - pub ping: f32, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct ServersClients { - pub servers: u32, - pub clients: u32, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Response { - pub total: ServersClients, - pub total_max: ServersClients, - pub list: Vec, -} diff --git a/crates/lib/src/services/mod.rs b/crates/lib/src/services/mod.rs deleted file mode 100644 index 0fc5453..0000000 --- a/crates/lib/src/services/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! Services that are currently implemented. - -/// Reference: [Master Server Query Protocol](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol) -pub mod valve_master_server; - -/// Reference: [Node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/minetest.js) -#[cfg(all(feature = "serde", feature = "tls"))] -pub mod minetest_master_server; diff --git a/crates/lib/src/services/valve_master_server/mod.rs b/crates/lib/src/services/valve_master_server/mod.rs deleted file mode 100644 index 908f8f6..0000000 --- a/crates/lib/src/services/valve_master_server/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -/// The implementation. -pub mod service; -/// All types used by the implementation. -pub mod types; - -pub use service::*; -pub use types::*; diff --git a/crates/lib/src/services/valve_master_server/service.rs b/crates/lib/src/services/valve_master_server/service.rs deleted file mode 100644 index 7a43090..0000000 --- a/crates/lib/src/services/valve_master_server/service.rs +++ /dev/null @@ -1,154 +0,0 @@ -use crate::{ - buffer::Buffer, - socket::{Socket, UdpSocket}, - valve_master_server::{Region, SearchFilters}, - GDErrorKind::PacketBad, - GDResult, -}; - -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; - -use byteorder::BigEndian; - -/// The default master ip, which is the one for Source. -pub fn default_master_address() -> SocketAddr { - SocketAddr::new(IpAddr::V4(Ipv4Addr::new(208, 64, 201, 194)), 27011) // hl2master.steampowered.com -} - -fn construct_payload(region: Region, filters: &Option, last_ip: &str, last_port: u16) -> Vec { - let filters_bytes: Vec = filters - .as_ref() - .map_or_else(|| vec![0x00], SearchFilters::to_bytes); - - let region_byte = &[region as u8]; - - [ - // Packet has to begin with the character '1' - &[0x31], - // The region byte is next - region_byte, - // The last fetched ip as a string - last_ip.as_bytes(), - // Followed by an ':' - b":", - // And the port, as a string - last_port.to_string().as_bytes(), - // Which needs to end with a NULL byte - &[0x00], - // Then the filters - &filters_bytes, - ] - .concat() -} - -/// The implementation, use this if you want to keep the same socket. -pub struct ValveMasterServer { - socket: UdpSocket, -} - -impl ValveMasterServer { - /// Construct a new struct. - pub fn new(master_address: &SocketAddr) -> GDResult { - let socket = UdpSocket::new(master_address, &None)?; - - Ok(Self { socket }) - } - - /// Make just a single query, providing `0.0.0.0` as the last ip and `0` as - /// the last port will give the initial packet. - pub fn query_specific( - &mut self, - region: Region, - search_filters: &Option, - last_address_ip: &str, - last_address_port: u16, - ) -> GDResult> { - let payload = construct_payload(region, search_filters, last_address_ip, last_address_port); - self.socket.send(&payload)?; - - let received_data = self.socket.receive(Some(1400))?; - let mut buf = Buffer::::new(&received_data); - - if buf.read::()? != u32::MAX || buf.read::()? != 26122 { - return Err(PacketBad.context("Expected 4294967295 followed by 26122")); - } - - let mut ips: Vec<(IpAddr, u16)> = Vec::new(); - - while buf.remaining_length() > 0 { - let ip = IpAddr::V4(Ipv4Addr::new( - buf.read::()?, - buf.read::()?, - buf.read::()?, - buf.read::()?, - )); - let port = buf.read::()?; - - ips.push((ip, port)); - } - - Ok(ips) - } - - /// Make a complete query. - pub fn query(&mut self, region: Region, search_filters: Option) -> GDResult> { - let mut ips: Vec<(IpAddr, u16)> = Vec::new(); - - let mut exit_fetching = false; - let mut last_ip: String = "0.0.0.0".to_string(); - let mut last_port: u16 = 0; - - while !exit_fetching { - let new_ips = self.query_specific(region, &search_filters, last_ip.as_str(), last_port)?; - - match new_ips.last() { - None => exit_fetching = true, - Some((latest_ip, latest_port)) => { - let mut remove_last = false; - - let latest_ip_string = latest_ip.to_string(); - if latest_ip_string == "0.0.0.0" && *latest_port == 0 { - exit_fetching = true; - remove_last = true; - } else if latest_ip_string == last_ip && *latest_port == last_port { - exit_fetching = true; - } else { - last_ip = latest_ip_string; - last_port = *latest_port; - } - - ips.extend(new_ips); - if remove_last { - ips.pop(); - } - } - } - } - - Ok(ips) - } -} - -/// Take only the first response of (what would be a) complete query. This is -/// faster as it results in less packets being sent, received and processed but -/// yields less ips. -pub fn query_singular(region: Region, search_filters: Option) -> GDResult> { - let mut master_server = ValveMasterServer::new(&default_master_address())?; - - let mut ips = master_server.query_specific(region, &search_filters, "0.0.0.0", 0)?; - - if let Some((last_ip, last_port)) = ips.last() { - if last_ip.to_string() == "0.0.0.0" && *last_port == 0 { - ips.pop(); - } - } - - Ok(ips) -} - -/// Make a complete query. -pub fn query(region: Region, search_filters: Option) -> GDResult> { - let mut master_server = ValveMasterServer::new(&default_master_address())?; - - master_server.query(region, search_filters) -} diff --git a/crates/lib/src/services/valve_master_server/types.rs b/crates/lib/src/services/valve_master_server/types.rs deleted file mode 100644 index f7050b1..0000000 --- a/crates/lib/src/services/valve_master_server/types.rs +++ /dev/null @@ -1,234 +0,0 @@ -use std::collections::HashMap; -use std::mem::Discriminant; - -/// A query filter. -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum Filter { - IsSecured(bool), - RunsMap(String), - CanHavePassword(bool), - CanBeEmpty(bool), - IsEmpty(bool), - CanBeFull(bool), - RunsAppID(u32), - NotAppID(u32), - HasTags(Vec), - MatchName(String), - MatchVersion(String), - /// Restrict to only a server if an IP hosts (on different ports) multiple - /// servers. - RestrictUniqueIP(bool), - /// Query for servers on a specific address. - OnAddress(String), - Whitelisted(bool), - SpectatorProxy(bool), - IsDedicated(bool), - RunsLinux(bool), - HasGameDir(String), -} - -const fn bool_as_char_u8(b: &bool) -> u8 { - match b { - true => b'1', - false => b'0', - } -} - -impl Filter { - pub(crate) fn to_bytes(&self) -> Vec { - let mut bytes: Vec = Vec::new(); - - match self { - Self::IsSecured(secured) => { - bytes = b"\\secure\\".to_vec(); - bytes.extend([bool_as_char_u8(secured)]); - } - Self::RunsMap(map) => { - bytes = b"\\map\\".to_vec(); - bytes.extend(map.as_bytes()); - } - Self::CanHavePassword(password) => { - bytes = b"\\password\\".to_vec(); - bytes.extend([bool_as_char_u8(password)]); - } - Self::CanBeEmpty(empty) => { - bytes = b"\\empty\\".to_vec(); - bytes.extend([bool_as_char_u8(empty)]); - } - Self::CanBeFull(full) => { - bytes = b"\\full\\".to_vec(); - bytes.extend([bool_as_char_u8(full)]); - } - Self::RunsAppID(id) => { - bytes = b"\\appid\\".to_vec(); - bytes.extend(id.to_string().as_bytes()); - } - Self::HasTags(tags) => { - if !tags.is_empty() { - bytes = b"\\gametype\\".to_vec(); - for tag in tags.iter() { - bytes.extend(tag.as_bytes()); - bytes.extend([b',']); - } - - bytes.pop(); - } - } - Self::NotAppID(id) => { - bytes = b"\\napp\\".to_vec(); - bytes.extend(id.to_string().as_bytes()); - } - Self::IsEmpty(empty) => { - bytes = b"\\noplayers\\".to_vec(); - bytes.extend([bool_as_char_u8(empty)]); - } - Self::MatchName(name) => { - bytes = b"\\name_match\\".to_vec(); - bytes.extend(name.as_bytes()); - } - Self::MatchVersion(version) => { - bytes = b"\\version_match\\".to_vec(); - bytes.extend(version.as_bytes()); - } - Self::RestrictUniqueIP(unique) => { - bytes = b"\\collapse_addr_hash\\".to_vec(); - bytes.extend([bool_as_char_u8(unique)]); - } - Self::OnAddress(address) => { - bytes = b"\\gameaddr\\".to_vec(); - bytes.extend(address.as_bytes()); - } - Self::Whitelisted(whitelisted) => { - bytes = b"\\white\\".to_vec(); - bytes.extend([bool_as_char_u8(whitelisted)]); - } - Self::SpectatorProxy(condition) => { - bytes = b"\\proxy\\".to_vec(); - bytes.extend([bool_as_char_u8(condition)]); - } - Self::IsDedicated(dedicated) => { - bytes = b"\\dedicated\\".to_vec(); - bytes.extend([bool_as_char_u8(dedicated)]); - } - Self::RunsLinux(linux) => { - bytes = b"\\linux\\".to_vec(); - bytes.extend([bool_as_char_u8(linux)]); - } - Self::HasGameDir(game_dir) => { - bytes = b"\\gamedir\\".to_vec(); - bytes.extend(game_dir.as_bytes()); - } - } - - bytes - } -} - -/// Query search filters. -/// An example of constructing one: -/// ```rust -/// use gamedig::valve_master_server::{Filter, SearchFilters}; -/// -/// let search_filters = SearchFilters::new() -/// .insert(Filter::RunsAppID(440)) -/// .insert(Filter::IsEmpty(false)) -/// .insert(Filter::CanHavePassword(false)); -/// ``` -/// This will construct filters that search for servers that can't have a -/// password, are not empty and run App ID 440. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SearchFilters { - filters: HashMap, Filter>, - nor_filters: HashMap, Filter>, - nand_filters: HashMap, Filter>, -} - -impl Default for SearchFilters { - fn default() -> Self { Self::new() } -} - -impl SearchFilters { - pub fn new() -> Self { - Self { - filters: HashMap::new(), - nor_filters: HashMap::new(), - nand_filters: HashMap::new(), - } - } - - pub fn insert(self, filter: Filter) -> Self { - let mut updated_fitler = self.filters; - updated_fitler.insert(std::mem::discriminant(&filter), filter); - - Self { - filters: updated_fitler, - nand_filters: self.nand_filters, - nor_filters: self.nor_filters, - } - } - - pub fn insert_nand(self, filter: Filter) -> Self { - let mut updated_fitler = self.nor_filters; - updated_fitler.insert(std::mem::discriminant(&filter), filter); - - Self { - filters: self.filters, - nand_filters: self.nand_filters, - nor_filters: updated_fitler, - } - } - - pub fn insert_nor(self, filter: Filter) -> Self { - let mut updated_fitler = self.nand_filters; - updated_fitler.insert(std::mem::discriminant(&filter), filter); - - Self { - filters: self.filters, - nand_filters: updated_fitler, - nor_filters: self.nor_filters, - } - } - - fn special_filter_to_bytes(name: &str, filters: &HashMap, Filter>) -> Vec { - let mut bytes = Vec::new(); - - if !filters.is_empty() { - bytes.extend(name.as_bytes()); - bytes.extend(filters.len().to_string().as_bytes()); - for filter in filters.values() { - bytes.extend(filter.to_bytes()); - } - } - - bytes - } - - pub(crate) fn to_bytes(&self) -> Vec { - let mut bytes: Vec = Vec::new(); - - for filter in self.filters.values() { - bytes.extend(filter.to_bytes()) - } - - bytes.extend(Self::special_filter_to_bytes("nand", &self.nand_filters)); - bytes.extend(Self::special_filter_to_bytes("nor", &self.nor_filters)); - - bytes.extend([0x00]); - bytes - } -} - -/// The region that you want to query server for. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[repr(u8)] -pub enum Region { - UsEast = 0x00, - UsWest = 0x01, - AmericaSouth = 0x02, - Europe = 0x03, - Asia = 0x04, - Australia = 0x05, - MiddleEast = 0x06, - Africa = 0x07, - Others = 0xFF, -} diff --git a/crates/lib/src/socket.rs b/crates/lib/src/socket.rs deleted file mode 100644 index c76d0ef..0000000 --- a/crates/lib/src/socket.rs +++ /dev/null @@ -1,253 +0,0 @@ -use crate::{ - protocols::types::TimeoutSettings, - GDErrorKind::{PacketReceive, PacketSend, SocketBind, SocketConnect}, - GDResult, -}; - -use std::{ - io::{Read, Write}, - net::{self, SocketAddr}, -}; - -const DEFAULT_PACKET_SIZE: usize = 1024; - -/// A trait defining the basic functionalities of a network socket. -pub trait Socket { - /// Create a new socket and connect to the remote address. - /// - /// # Arguments - /// * `address` - The address to connect the socket to. - /// * `timeout_settings` - Optional timeout settings for the socket. - /// - /// # Returns - /// A result containing the socket instance or an error. - fn new(address: &SocketAddr, timeout_settings: &Option) -> GDResult - where Self: Sized; - - /// Apply read and write timeouts to the socket. - /// - /// # Arguments - /// * `timeout_settings` - Optional timeout settings to apply. - /// - /// # Returns - /// A result indicating success or error in applying timeouts. - fn apply_timeout(&self, timeout_settings: &Option) -> GDResult<()>; - - /// Send data over the socket. - /// - /// # Arguments - /// * `data` - Data to be sent. - /// - /// # Returns - /// A result indicating success or error in sending data. - fn send(&mut self, data: &[u8]) -> GDResult<()>; - - /// Receive data from the socket. - /// - /// # Arguments - /// * `size` - Optional size of data to receive. - /// - /// # Returns - /// A result containing received data or an error. - fn receive(&mut self, size: Option) -> GDResult>; - - /// Get the remote port of the socket. - /// - /// # Returns - /// The port number. - fn port(&self) -> u16; - - /// Get the local SocketAddr. - /// - /// # Returns - /// The local SocketAddr. - #[cfg(feature = "packet_capture")] - fn local_addr(&self) -> std::io::Result; -} - -/// Implementation of a TCP socket. -pub struct TcpSocketImpl { - /// The underlying TCP socket stream. - socket: net::TcpStream, - /// The address of the remote host. - address: SocketAddr, -} - -impl Socket for TcpSocketImpl { - fn new(address: &SocketAddr, timeout_settings: &Option) -> GDResult { - let socket = TimeoutSettings::get_connect_or_default(timeout_settings).map_or_else( - || net::TcpStream::connect(address), - |timeout| net::TcpStream::connect_timeout(address, timeout), - ); - - let socket = Self { - socket: socket.map_err(|e| SocketConnect.context(e))?, - address: *address, - }; - - socket.apply_timeout(timeout_settings)?; - - Ok(socket) - } - - fn apply_timeout(&self, timeout_settings: &Option) -> GDResult<()> { - let (read, write) = TimeoutSettings::get_read_and_write_or_defaults(timeout_settings); - self.socket.set_read_timeout(read).unwrap(); // unwrapping because TimeoutSettings::new - self.socket.set_write_timeout(write).unwrap(); // checks if these are 0 and throws an error - - Ok(()) - } - - fn send(&mut self, data: &[u8]) -> GDResult<()> { - self.socket.write(data).map_err(|e| PacketSend.context(e))?; - Ok(()) - } - - fn receive(&mut self, size: Option) -> GDResult> { - let mut buf = Vec::with_capacity(size.unwrap_or(DEFAULT_PACKET_SIZE)); - self.socket - .read_to_end(&mut buf) - .map_err(|e| PacketReceive.context(e))?; - - Ok(buf) - } - - fn port(&self) -> u16 { self.address.port() } - - #[cfg(feature = "packet_capture")] - fn local_addr(&self) -> std::io::Result { self.socket.local_addr() } -} - -/// Implementation of a UDP socket. -pub struct UdpSocketImpl { - /// The underlying UDP socket. - socket: net::UdpSocket, - /// The address of the remote host. - address: SocketAddr, -} - -impl Socket for UdpSocketImpl { - fn new(address: &SocketAddr, timeout_settings: &Option) -> GDResult { - let socket = net::UdpSocket::bind("0.0.0.0:0").map_err(|e| SocketBind.context(e))?; - - let socket = Self { - socket, - address: *address, - }; - - socket.apply_timeout(timeout_settings)?; - - Ok(socket) - } - - fn apply_timeout(&self, timeout_settings: &Option) -> GDResult<()> { - let (read, write) = TimeoutSettings::get_read_and_write_or_defaults(timeout_settings); - self.socket.set_read_timeout(read).unwrap(); // unwrapping because TimeoutSettings::new - self.socket.set_write_timeout(write).unwrap(); // checks if these are 0 and throws an error - - Ok(()) - } - - fn send(&mut self, data: &[u8]) -> GDResult<()> { - self.socket - .send_to(data, self.address) - .map_err(|e| PacketSend.context(e))?; - - Ok(()) - } - - fn receive(&mut self, size: Option) -> GDResult> { - let mut buf: Vec = vec![0; size.unwrap_or(DEFAULT_PACKET_SIZE)]; - let (number_of_bytes_received, _) = self - .socket - .recv_from(&mut buf) - .map_err(|e| PacketReceive.context(e))?; - - Ok(buf[.. number_of_bytes_received].to_vec()) - } - - fn port(&self) -> u16 { self.address.port() } - - #[cfg(feature = "packet_capture")] - fn local_addr(&self) -> std::io::Result { self.socket.local_addr() } -} - -#[cfg(not(feature = "packet_capture"))] -pub type UdpSocket = UdpSocketImpl; -#[cfg(not(feature = "packet_capture"))] -pub type TcpSocket = TcpSocketImpl; - -#[cfg(feature = "packet_capture")] -pub(crate) type UdpSocket = crate::capture::socket::CapturedUdpSocket; -#[cfg(feature = "packet_capture")] -pub(crate) type TcpSocket = crate::capture::socket::CapturedTcpSocket; - -#[cfg(test)] -mod tests { - use std::thread; - - use super::*; - - #[test] - fn test_tcp_socket_send_and_receive() { - // Spawn a thread to run the server - let listener = net::TcpListener::bind("127.0.0.1:0").unwrap(); - let bound_address = listener.local_addr().unwrap(); - let server_thread = thread::spawn(move || { - let (mut stream, _) = listener.accept().unwrap(); - let mut buf = [0; 1024]; - let _ = stream.read(&mut buf).unwrap(); - let _ = stream.write(&buf).unwrap(); - }); - - // Create a TCP socket and send a message to the server - let mut socket = TcpSocket::new(&bound_address, &None).unwrap(); - let message = b"hello, world!"; - socket.send(message).unwrap(); - - // Receive the response from the server - let received_message: Vec = socket - .receive(None) - .unwrap() - // Iterate over the buffer and remove 0s that are alone in the buffer - // just added to pass default size - .into_iter() - .filter(|&x| x != 0) - .collect(); - - server_thread.join().expect("server thread panicked"); - - assert_eq!(message, &received_message[..]); - } - - #[test] - fn test_udp_socket_send_and_receive() { - // Spawn a thread to run the server - let socket = net::UdpSocket::bind("127.0.0.1:0").unwrap(); - let bound_address = socket.local_addr().unwrap(); - let server_thread = thread::spawn(move || { - let mut buf = [0; 1024]; - let (_, src_addr) = socket.recv_from(&mut buf).unwrap(); - socket.send_to(&buf, src_addr).unwrap(); - }); - - // Create a UDP socket and send a message to the server - let mut socket = UdpSocket::new(&bound_address, &None).unwrap(); - let message = b"hello, world!"; - socket.send(message).unwrap(); - - // Receive the response from the server - let received_message: Vec = socket - .receive(None) - .unwrap() - // Iterate over the buffer and remove 0s that are alone in the buffer - // just added to pass default size - .into_iter() - .filter(|&x| x != 0) - .collect(); - - server_thread.join().expect("server thread panicked"); - - assert_eq!(message, &received_message[..]); - } -} diff --git a/crates/lib/src/utils.rs b/crates/lib/src/utils.rs deleted file mode 100644 index 6eea361..0000000 --- a/crates/lib/src/utils.rs +++ /dev/null @@ -1,208 +0,0 @@ -use crate::GDErrorKind::{PacketOverflow, PacketReceive, PacketSend, PacketUnderflow}; -use crate::GDResult; -use std::cmp::Ordering; - -pub fn error_by_expected_size(expected: usize, size: usize) -> GDResult<()> { - match size.cmp(&expected) { - Ordering::Greater => Err(PacketOverflow.into()), - Ordering::Less => Err(PacketUnderflow.into()), - Ordering::Equal => Ok(()), - } -} - -pub const fn u8_lower_upper(n: u8) -> (u8, u8) { (n & 15, n >> 4) } - -/// Run a closure `retry_count+1` times while it returns [PacketReceive] or -/// [PacketSend] errors, returning the first success, other Error, or after -/// `retry_count+1` tries the last [PacketReceive] or [PacketSend] error. -pub fn retry_on_timeout(mut retry_count: usize, mut fetch: impl FnMut() -> GDResult) -> GDResult { - let mut last_err = PacketReceive.context("Retry count was 0"); - retry_count += 1; - while retry_count > 0 { - last_err = match fetch() { - Ok(r) => return Ok(r), - Err(e) if e.kind == PacketReceive || e.kind == PacketSend => e, - Err(e) => return Err(e), - }; - retry_count -= 1; - } - Err(last_err) -} - -/// Run gather_fn based on the value of gather_toggle. -/// -/// # Parameters -/// - `gather_toggle` should be an expression resolving to a -/// [crate::protocols::types::GatherToggle]. -/// - `gather_fn` should be an expression that returns a [crate::GDResult]. -/// -/// # States -/// - [DontGather](crate::protocols::types::GatherToggle::DontGather) - Don't -/// run gather function, returns None. -/// - [AttemptGather](crate::protocols::types::GatherToggle::AttemptGather) - -/// Runs the gather function, if it returns an error return None, else return -/// Some. -/// - [Required](crate::protocols::types::GatherToggle::Required) - Runs the -/// gather function, if it returns an error propagate it using the `?` -/// operator, else return Some. -/// -/// # Examples -/// -/// ```ignore,Doctests cannot access private items -/// use gamedig::protocols::types::GatherToggle; -/// use gamedig::utils::maybe_gather; -/// -/// let query_fn = || { Err("Query error") }; -/// -/// // query_fn() is not called -/// let response = maybe_gather!(GatherToggle::DontGather, query_fn()); -/// assert!(response.is_none()); -/// -/// // query_fn() is called but Err is converted to None -/// let response = maybe_gather!(GatherToggle::AttemptGather, query_fn()); -/// assert!(response.is_none()); -/// -/// // query_fn() is called and Err is propagated. -/// let response = maybe_gather!(GatherToggle::Required, query_fn()); -/// unreachable!(); -/// ``` -macro_rules! maybe_gather { - ($gather_toggle: expr, $gather_fn: expr) => { - match $gather_toggle { - crate::protocols::types::GatherToggle::Skip => None, - crate::protocols::types::GatherToggle::Try => $gather_fn.ok(), - crate::protocols::types::GatherToggle::Enforce => Some($gather_fn?), - } - }; -} - -pub(crate) use maybe_gather; - -#[cfg(test)] -mod tests { - use super::retry_on_timeout; - use crate::{ - protocols::types::GatherToggle, - GDError, - GDErrorKind::{self, PacketBad, PacketReceive, PacketSend}, - GDResult, - }; - - #[test] - fn u8_lower_upper() { - assert_eq!(super::u8_lower_upper(171), (11, 10)); - } - - #[test] - fn error_by_expected_size() { - assert!(super::error_by_expected_size(69, 69).is_ok()); - assert!(super::error_by_expected_size(69, 68).is_err()); - assert!(super::error_by_expected_size(69, 70).is_err()); - } - - #[test] - fn retry_success_on_first() { - let r = retry_on_timeout(0, || Ok(())); - assert!(r.is_ok()); - } - - #[test] - fn retry_no_success() { - let r: GDResult<()> = retry_on_timeout(100, || Err(PacketSend.context("test"))); - assert!(r.is_err()); - assert_eq!(r.unwrap_err().kind, PacketSend); - } - - #[test] - fn retry_success_on_third() { - let mut i = 0u8; - let r = retry_on_timeout(2, || { - i += 1; - if i < 3 { - Err(PacketReceive.context("test")) - } else { - Ok(()) - } - }); - assert!(r.is_ok()); - } - - #[test] - fn retry_success_on_third_but_less_retries() { - let mut i = 0u8; - let r = retry_on_timeout(1, || { - i += 1; - if i < 3 { - Err(PacketReceive.context("test")) - } else { - Ok(()) - } - }); - assert!(r.is_err()); - assert_eq!(r.unwrap_err().kind, PacketReceive); - } - - #[test] - fn retry_with_non_timeout_error() { - let mut i = 0u8; - let r = retry_on_timeout(50, || { - i += 1; - match i { - 1 => Err(PacketSend.context("test")), - 2 => Err(PacketBad.context("test")), - _ => Ok(()), - } - }); - assert!(r.is_err()); - assert_eq!(r.unwrap_err().kind, PacketBad); - } - - fn gather_success(n: i32) -> GDResult { Ok(n) } - - fn gather_fail(err: &'static str) -> GDResult { Err(GDErrorKind::PacketSend.context(err)) } - - #[test] - fn gather_success_dont_gather() -> GDResult<()> { - let result = maybe_gather!(GatherToggle::Skip, gather_success(5)); - assert!(result.is_none()); - Ok(()) - } - - #[test] - fn gather_success_attempt_gather() -> GDResult<()> { - let result = maybe_gather!(GatherToggle::Try, gather_success(10)); - assert_eq!(result, Some(10)); - Ok(()) - } - - #[test] - fn gather_success_required() -> GDResult<()> { - let result = maybe_gather!(GatherToggle::Enforce, gather_success(15)); - assert_eq!(result, Some(15)); - Ok(()) - } - - #[test] - fn gather_fail_dont_gather() -> GDResult<()> { - let result = maybe_gather!(GatherToggle::Skip, gather_fail("dont")); - assert!(result.is_none()); - Ok(()) - } - - #[test] - fn gather_fail_attempt_gather() -> GDResult<()> { - let result = maybe_gather!(GatherToggle::Try, gather_fail("attempt")); - assert!(result.is_none()); - Ok(()) - } - - #[test] - fn gather_fail_required() { - let inner = || { - let result = maybe_gather!(GatherToggle::Enforce, gather_fail("required")); - assert_eq!(result, Some(10)); - Ok::<(), GDError>(()) - }; - assert!(inner().is_err()); - } -} diff --git a/crates/lib/tests/game_ids.rs b/crates/lib/tests/game_ids.rs deleted file mode 100644 index d7925a9..0000000 --- a/crates/lib/tests/game_ids.rs +++ /dev/null @@ -1,11 +0,0 @@ -#![cfg(all(test, feature = "game_defs"))] - -use gamedig::GAMES; - -use gamedig_id_tests::test_game_name_rules; - -#[test] -fn check_definitions_match_name_rules() { - let wrong = test_game_name_rules(GAMES.entries().map(|(id, game)| (id.to_owned(), game.name))); - assert!(wrong.is_empty()); -} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7d12d9a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +}