mirror of
https://github.com/tribufu/rust-gamedig
synced 2026-05-18 09:35:50 +00:00
Merge branch 'main' into feat/rootless-capture
This commit is contained in:
commit
36d957ceb4
52 changed files with 1483 additions and 457 deletions
8
.github/badges/node.svg
vendored
8
.github/badges/node.svg
vendored
|
|
@ -1,5 +1,5 @@
|
|||
<svg width="181.6" height="20" viewBox="0 0 1816 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Node game coverage: 14.06%">
|
||||
<title>Node game coverage: 14.06%</title>
|
||||
<svg width="181.6" height="20" viewBox="0 0 1816 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Node game coverage: 20.99%">
|
||||
<title>Node game coverage: 20.99%</title>
|
||||
<linearGradient id="a" x2="0" y2="100%">
|
||||
<stop offset="0" stop-opacity=".1" stop-color="#EEE"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
|
|
@ -13,8 +13,8 @@
|
|||
<g aria-hidden="true" fill="#fff" text-anchor="start" font-family="Verdana,DejaVu Sans,sans-serif" font-size="110">
|
||||
<text x="60" y="148" textLength="1176" fill="#000" opacity="0.25">Node game coverage</text>
|
||||
<text x="50" y="138" textLength="1176">Node game coverage</text>
|
||||
<text x="1331" y="148" textLength="440" fill="#000" opacity="0.25">14.06%</text>
|
||||
<text x="1321" y="138" textLength="440">14.06%</text>
|
||||
<text x="1331" y="148" textLength="440" fill="#000" opacity="0.25">20.99%</text>
|
||||
<text x="1321" y="138" textLength="440">20.99%</text>
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1 KiB |
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -7,7 +7,17 @@ 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.
|
||||
|
||||
Protocols:
|
||||
- Added the unreal2 protocol and its associated games: Darkest Hour, Devastation, Killing Floor, Red Orchestra, Unreal Tournament 2003, Unreal Tournament 2004 (by @Douile).
|
||||
|
|
@ -25,6 +35,9 @@ Game:
|
|||
- - 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.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
[workspace]
|
||||
name = "gamedig-workspace"
|
||||
members = ["crates/cli", "crates/lib"]
|
||||
members = ["crates/cli", "crates/lib", "crates/id-tests"]
|
||||
|
||||
# Edition 2021, uses resolver = 2
|
||||
resolver = "2"
|
||||
|
|
|
|||
20
GAMES.md
20
GAMES.md
|
|
@ -65,12 +65,20 @@ Beware of the `Notes` column, as it contains information about query port offset
|
|||
| 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 |
|
||||
| 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. |
|
||||
|
||||
## Planned to add support:
|
||||
_
|
||||
|
|
|
|||
16
PROTOCOLS.md
16
PROTOCOLS.md
|
|
@ -1,13 +1,15 @@
|
|||
A protocol is defined as proprietary if it is being used only for a single scope (or series, like Minecraft).
|
||||
|
||||
# Supported protocols:
|
||||
| Name | For | Proprietary? | Documentation reference | Notes |
|
||||
|----------------|-------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Valve Protocol | Games | No | [Server Queries](https://developer.valvesoftware.com/wiki/Server_queries) | In some cases, the players details query might contain some 0-length named players. Multi-packet decompression not tested. |
|
||||
| Minecraft | Games | Yes | Java: [List Server Protocol](https://wiki.vg/Server_List_Ping) <br> Bedrock: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/minecraftbedrock.js) | |
|
||||
| GameSpy | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy1.js) Two: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy2.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy3.js) | These protocols are not really standardized, gamedig tries to get the most common fields amongst its supported games, if there are parsing problems, use the `query_vars` function. |
|
||||
| Quake | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake1.js) Two: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake2.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake3.js) | |
|
||||
| Unreal2 | Games | Yes | [Node-GameDig Source](https://github.com/gamedig/node-gamedig) | 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. |
|
||||
| Name | For | Proprietary? | Documentation reference | Notes |
|
||||
|----------------------------|-------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Valve Protocol | Games | No | [Server Queries](https://developer.valvesoftware.com/wiki/Server_queries) | In some cases, the players details query might contain some 0-length named players. Multi-packet decompression not tested. |
|
||||
| Minecraft | Games | Yes | Java: [List Server Protocol](https://wiki.vg/Server_List_Ping) <br> Bedrock: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/minecraftbedrock.js) | |
|
||||
| GameSpy | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy1.js) Two: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy2.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy3.js) | These protocols are not really standardized, gamedig tries to get the most common fields amongst its supported games, if there are parsing problems, use the `query_vars` function. |
|
||||
| Quake | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake1.js) Two: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake2.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake3.js) | |
|
||||
| Just Cause 2: Multiplayer | Games | Yes | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/jc2mp.js) |
|
||||
| Unreal 2 | Games | No | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/unreal2.js) | Sometimes servers send strings that node-gamedig would treat as latin1 that are UTF-8 encoded, when this happens the remove color code breaks because latin1 decodes the colour sequences differently. Some games provide additional info at the end of the server info packet, this is not currently handled (see the node implementation). Some games use a bot player to denote the team names, this is not currently handled. |
|
||||
| Savage 2 | Games | Yes | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/savage2.js) | |
|
||||
|
||||
## Planned to add support:
|
||||
_
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ Response (note that some games have a different structure):
|
|||
}
|
||||
```
|
||||
|
||||
Want to see more examples? Checkout the [examples](examples) folder.
|
||||
Want to see more examples? Checkout the [examples](crates/lib/examples) folder.
|
||||
|
||||
## Documentation
|
||||
The documentation is available at [docs.rs](https://docs.rs/gamedig/latest/gamedig/).
|
||||
|
|
|
|||
104
RESPONSES.md
104
RESPONSES.md
|
|
@ -5,53 +5,57 @@ In the case that a field that performs the same function exists in the current c
|
|||
|
||||
# Response table
|
||||
|
||||
| Field | Generic | GameSpy(1) | GameSpy(2) | GameSpy(3) | Minecraft(Java) | Minecraft(Bedrock) | Valve | Quake | Unreal2 | Proprietary: FFOW | Proprietary: TheShip | Proprietary: JC2MP |
|
||||
|----------------------|------------------|---------------------------|---------------|---------------------------|-----------------------|--------------------|-----------------------------------|---------------------------|--------------------------------|-------------------|---------------------------|--------------------|
|
||||
| name | `Option<String>` | `String` | `String` | `String` | | `String` | `String` | `String` | `String` | `String` | `String` | `String` |
|
||||
| description | `Option<String>` | | | | `String` | | | | | `String` | | `String` |
|
||||
| game_mode | `Option<String>` | `String` | | `String` | | `Option<GameMode>` | `String` | | `String` | `String` | `String` | |
|
||||
| game_version | `Option<String>` | `String` | | `String` | `String` | | `String` | `String` | | `String` | `String` | `String` |
|
||||
| map | `Option<String>` | `String` | `String` | `String` | | `Option<String>` | `String` | `String` | `String` | `String` | `String` | |
|
||||
| players_maxmimum | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u32` | `u8` | `u8` | `u32` |
|
||||
| players_online | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u32` | `u8` | `u8` | `u32` |
|
||||
| players_bots | `Option<u32>` | | | | | | `u8` | | | | `u8` | |
|
||||
| has_password | `Option<bool>` | `bool` | `bool` | `bool` | | | `bool` | | | `bool` | `bool` | `bool` |
|
||||
| players_minimum | | `Option<u8>` | `Option<u8>` | `Option<u8>` | | | | | | | | |
|
||||
| players | | `Vec<Player>` | `Vec<Player>` | `Vec<Player>` | `Option<Vec<Player>>` | | `Option<Vec<ServerPlayer>>` | `Vec<P>` | `Vec<Player>` | | `Vec<TheShipPlayer>` | `Vec<Player>` |
|
||||
| tournament | | `bool` | | `bool` | | | | | | | | |
|
||||
| unused_entries | | `Hashmap<String, String>` | | `HashMap<String, String>` | | | | `HashMap<String, String>` | | | | |
|
||||
| teams | | | `Vec<Team>` | `Vec<Team>` | | | | | | | | |
|
||||
| protocol_version | | | | | `i32` | `String` | `u8` | | | `u8` | `u8` | |
|
||||
| server_type | | | | | `Server` | `Server` | `Server` | | | | `Server` | |
|
||||
| rules | | | | | | | `Option<HashMap<String, String>>` | | `HashMap<String, Vec<String>>` | | `HashMap<String, String>` | |
|
||||
| environment_type | | | | | | | `Environment` | | | `Environment` | | |
|
||||
| vac_secured | | | | | | | `bool` | | | `bool` | `bool` | |
|
||||
| map_title | | `Option<String>` | | | | | | | | | | |
|
||||
| admin_contact | | `Option<String>` | | | | | | | | | | |
|
||||
| admin_name | | `Option<String>` | | | | | | | | | | |
|
||||
| favicon | | | | | `Option<String>` | | | | | | | |
|
||||
| previews_chat | | | | | `Option<bool>` | | | | | | | |
|
||||
| enforces_secure_chat | | | | | `Option<bool>` | | | | | | | |
|
||||
| edition | | | | | | `String` | | | | | | |
|
||||
| id | | | | | | `String` | | | `String` | | | |
|
||||
| the_ship | | | | | | | `Option<TheShip>` | | | | | |
|
||||
| is_mod | | | | | | | `bool` | | | | | |
|
||||
| extra_data | | | | | | | `Option<ExtraData>` | | | | | |
|
||||
| mod_data | | | | | | | `Option<ModData>` | | | | | |
|
||||
| folder | | | | | | | `String` | | | | | |
|
||||
| appid | | | | | | | `u32` | | | | | |
|
||||
| active_mod | | | | | | | | | | `String` | | |
|
||||
| round | | | | | | | | | | `u8` | | |
|
||||
| rounds_maximum | | | | | | | | | | `u8` | | |
|
||||
| time_left | | | | | | | | | | `u16` | | |
|
||||
| port | | | | | | | | | `u32` | | `Option<u16>` | |
|
||||
| steam_id | | | | | | | | | | | `Option<u64>` | |
|
||||
| tv_port | | | | | | | | | | | `Option<u16>` | |
|
||||
| tv_name | | | | | | | | | | | `Option<String>` | |
|
||||
| keywords | | | | | | | | | | | `Option<String>` | |
|
||||
| mode | | | | | | | | | | | `u8` | |
|
||||
| witnesses | | | | | | | | | | | `u8` | |
|
||||
| duration | | | | | | | | | | | `u8` | |
|
||||
| query_port | | | | | | | | | `u32` | | | |
|
||||
| ip | | | | | | | | | `String` | | | |
|
||||
| mutators | | | | | | | | | `HashSet<String>` | | | |
|
||||
| Field | Generic | GameSpy(1) | GameSpy(2) | GameSpy(3) | Minecraft(Java) | Minecraft(Bedrock) | Valve | Quake | Unreal2 | Proprietary: FFOW | Proprietary: TheShip | Proprietary: JC2MP | Proprietary: Savage 2 |
|
||||
|----------------------|----------|------------|------------|------------|-----------------|--------------------|---------------|-----------|------------|-------------------|----------------------|--------------------|-----------------------|
|
||||
| name | `Option` | `String` | `String` | `String` | | `String` | `String` | `String` | `String` | `String` | `String` | `String` | `String` |
|
||||
| description | `Option` | | | | `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` | |
|
||||
| map | `Option` | `String` | `String` | `String` | | `Option` | `String` | `String` | `String` | `String` | `String` | | `String` |
|
||||
| players_maxmimum | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u32` | `u8` | `u8` | `u32` | `u8` |
|
||||
| players_online | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u32` | `u8` | `u8` | `u32` | `u8` |
|
||||
| players_bots | `Option` | | | | | | `u8` | | | | `u8` | | |
|
||||
| has_password | `Option` | `bool` | `bool` | `bool` | | | `bool` | | | `bool` | `bool` | `bool` | |
|
||||
| players_minimum | | `Option` | `Option` | `Option` | | | | | | | | | `u8` |
|
||||
| players | | `Vec` | `Vec` | `Vec` | `Option>` | | `Option>` | `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` | | | | |
|
||||
| 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` | | |
|
||||
| steam_id | | | | | | | | | | | `Option` | | |
|
||||
| tv_port | | | | | | | | | | | `Option` | | |
|
||||
| tv_name | | | | | | | | | | | `Option` | | |
|
||||
| keywords | | | | | | | | | | | `Option` | | |
|
||||
| mode | | | | | | | | | | | `u8` | | |
|
||||
| witnesses | | | | | | | | | | | `u8` | | |
|
||||
| duration | | | | | | | | | | | `u8` | | |
|
||||
| query_port | | | | | | | | | `u32` | | | | |
|
||||
| ip | | | | | | | | | `String` | | | | |
|
||||
| mutators | | | | | | | | | `HashSet` | | | | |
|
||||
| next_map | | | | | | | | | | | | | `String` |
|
||||
| location | | | | | | | | | | | | | `String` |
|
||||
| level_minimum | | | | | | | | | | | | | `String` |
|
||||
| time | | | | | | | | | | | | | `String` |
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ fn resolve_ip_or_domain<T: AsRef<str>>(host: T, extra_options: &mut Option<Extra
|
|||
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).
|
||||
|
|
|
|||
29
crates/id-tests/Cargo.toml
Normal file
29
crates/id-tests/Cargo.toml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
[package]
|
||||
name = "gamedig-id-tests"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"rust-GameDig contributors [https://github.com/gamedig/rust-gamedig/contributors]",
|
||||
"node-GameDig contributors [https://github.com/gamedig/node-gamedig/contributors]",
|
||||
]
|
||||
license = "MIT"
|
||||
description = "Test if IDs match the gamedig rules"
|
||||
homepage = "https://github.com/gamedig/rust-gamedig/CONTRIBUTING.md#naming"
|
||||
repository = "https://github.com/gamedig/rust-gamedig"
|
||||
readme = "README.md"
|
||||
rust-version = "1.65.0"
|
||||
|
||||
[features]
|
||||
cli = ["dep:serde_json", "dep:serde"]
|
||||
default = ["cli"]
|
||||
|
||||
[[bin]]
|
||||
name = "gamedig-id-tests"
|
||||
required-features = ["cli"]
|
||||
|
||||
[dependencies]
|
||||
number_to_words = "0.1"
|
||||
roman_numeral = "0.1"
|
||||
|
||||
serde_json = { version = "1", optional = true }
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
451
crates/id-tests/src/lib.rs
Normal file
451
crates/id-tests/src/lib.rs
Normal file
|
|
@ -0,0 +1,451 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
mod utils;
|
||||
use utils::{extract_bracketed_suffix, split_on_switch_between_alpha_numeric};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum IDRule {
|
||||
IDsMustBeLowerCase,
|
||||
NumbersAreTheirOwnWord,
|
||||
IfFirstWordNumberNoDigits,
|
||||
IfLastWordNumberMustBeAppended,
|
||||
ConvertRomanNumeralsToArabic,
|
||||
TwoWordsOrLessUseFullWords,
|
||||
MoreThanTwoWordsMakeAcronym,
|
||||
IfIDDuplicateSameGameAppendYearToNewer,
|
||||
IfIDDuplicateSameGameAppendProtocol,
|
||||
IfIDDuplicateNoAcronym,
|
||||
IfModForQueriesProcessOnlyModName,
|
||||
NoDuplicates,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IDFail {
|
||||
pub game_id: String,
|
||||
pub game_name: String,
|
||||
pub expected_id: String,
|
||||
pub rule_stack: Vec<IDRule>,
|
||||
}
|
||||
|
||||
impl IDFail {
|
||||
fn new(game_id: String, game_name: String, expected_id: String, rule_stack: Vec<IDRule>) -> Self {
|
||||
Self {
|
||||
game_id,
|
||||
game_name,
|
||||
expected_id,
|
||||
rule_stack,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test a single game against the rules
|
||||
pub fn test_game_name_rule(
|
||||
seen_ids: &mut HashMap<String, Vec<String>>,
|
||||
id: &str,
|
||||
mut game: GameNameParsed,
|
||||
is_mod_name: bool,
|
||||
) -> Vec<IDFail> {
|
||||
let mut wrong_ids = Vec::new();
|
||||
|
||||
let mut rule_stack = Vec::new();
|
||||
if is_mod_name {
|
||||
rule_stack.push(IDRule::IfModForQueriesProcessOnlyModName);
|
||||
}
|
||||
|
||||
let mut suffix = String::new();
|
||||
|
||||
// A game's identification is a lowercase alphanumeric string will and be forged
|
||||
// following these rules:
|
||||
if id.to_lowercase().ne(id) {
|
||||
wrong_ids.push(IDFail::new(
|
||||
id.to_owned(),
|
||||
game.name.to_owned(),
|
||||
id.to_lowercase(),
|
||||
vec![IDRule::IDsMustBeLowerCase],
|
||||
));
|
||||
}
|
||||
|
||||
// 5. Roman numbering will be converted to arabic numbering (XIV -> 14).
|
||||
game.words = {
|
||||
let mut is_first = true;
|
||||
game.words
|
||||
.into_iter()
|
||||
.map(|w| {
|
||||
// First word will never be a numeral
|
||||
if is_first {
|
||||
is_first = false;
|
||||
w
|
||||
} else if let Ok(number) = roman_numeral::RomanNumeral::from_string(&w) {
|
||||
rule_stack.push(IDRule::ConvertRomanNumeralsToArabic);
|
||||
number.get_u32().to_string()
|
||||
} else {
|
||||
w
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
// 6. Unless numbers are at the end of a name, they will be considered words,
|
||||
// but digits will always be used instead of the acronym (counter to #2)
|
||||
// (Left 4 Dead -> l4d) unless they at the start position (7 Days to Die ->
|
||||
// sdtd), if they are at the end (such as sequel number or the year), always
|
||||
// append them (Team Fortress 2 -> teamfortress2, Unreal Tournament 2003 ->
|
||||
// unrealtournament2003).
|
||||
game.words = game
|
||||
.words
|
||||
.into_iter()
|
||||
.flat_map(|w| {
|
||||
let n = split_on_switch_between_alpha_numeric(&w);
|
||||
if n.len() > 1 {
|
||||
rule_stack.push(IDRule::NumbersAreTheirOwnWord);
|
||||
}
|
||||
n
|
||||
})
|
||||
.collect();
|
||||
|
||||
// If first word is number make text
|
||||
if !game.words.is_empty() && game.words[0].chars().next().unwrap().is_ascii_digit() {
|
||||
game.words[0] = number_to_words::number_to_words(game.words[0].parse::<f64>().unwrap(), false);
|
||||
rule_stack.push(IDRule::IfFirstWordNumberNoDigits);
|
||||
}
|
||||
|
||||
// If last word is number append full number
|
||||
if let Some(last_word) = game.words.last() {
|
||||
if last_word.chars().all(|c| c.is_ascii_digit()) {
|
||||
suffix += &game.words.pop().unwrap();
|
||||
rule_stack.push(IDRule::IfLastWordNumberMustBeAppended);
|
||||
}
|
||||
}
|
||||
|
||||
let main = if game.words.len() <= 2 {
|
||||
// 1. Names composed of a maximum of two words (unless #4 applies) will result
|
||||
// in an id where the words are concatenated (Dead Cells -> deadcells),
|
||||
// acronyms in the name count as a single word (S.T.A.L.K.E.R. -> stalker).
|
||||
|
||||
rule_stack.push(IDRule::TwoWordsOrLessUseFullWords);
|
||||
|
||||
game.words
|
||||
.iter()
|
||||
.map(|w| w.trim_matches('-').to_owned())
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
} else {
|
||||
// 2. Names of more than two words shall be made into an acronym made of the
|
||||
// initial letters (The Binding of Isaac -> tboi), hypenation composed words
|
||||
// don't count as a single word, but of how many parts they are made of (Dino
|
||||
// D-Day, 3 words, so ddd).
|
||||
|
||||
rule_stack.push(IDRule::MoreThanTwoWordsMakeAcronym);
|
||||
|
||||
game.words
|
||||
.iter()
|
||||
.map(|w| w.chars().next().unwrap())
|
||||
.filter(|c| c.is_alphanumeric())
|
||||
.collect()
|
||||
};
|
||||
|
||||
let mut expected_id = format!("{}{}", main, suffix).to_lowercase();
|
||||
|
||||
if let Some(other_game_name_words) = seen_ids.get(&expected_id) {
|
||||
let mut game_names_same = other_game_name_words.len() == game.words.len();
|
||||
// Check all words in game name are the same
|
||||
if game_names_same {
|
||||
for i in 0 .. game.words.len() {
|
||||
if game.words[i].to_lowercase() != other_game_name_words[i].to_lowercase() {
|
||||
game_names_same = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if game_names_same {
|
||||
if let Some(year) = game.year {
|
||||
// 3. If a game has the exact name as a previously existing id's game (Star Wars
|
||||
// Battlefront 2, the 2005 and 2017 one), append the release year to the
|
||||
// newer id (2005 would be swbf2 (suppose we already have this one supported)
|
||||
// and 2017 would be swbf22017).
|
||||
|
||||
rule_stack.push(IDRule::IfIDDuplicateSameGameAppendYearToNewer);
|
||||
expected_id = format!("{}{}", expected_id, year).to_lowercase();
|
||||
} else if let Some(protocol) = game.optional_parts.first() {
|
||||
// 7. If a game supports multiple protocols, multiple entries will be done for
|
||||
// said game where the edition/protocol name (first disposable in this order)
|
||||
// will be appended to the game name (Minecraft is divided by 2 editions,
|
||||
// Java and Bedrock which will be minecraftjava and minecraftbedrock
|
||||
// respectively) and one more entry can be added by the base name of the game
|
||||
// which queries in a group said supported protocols to make generic queries
|
||||
// easier and disposable.
|
||||
|
||||
rule_stack.push(IDRule::IfIDDuplicateSameGameAppendProtocol);
|
||||
|
||||
// Parse the protocol as a game name so we can remove all non-valid characters
|
||||
let protocol_parsed = extract_game_parts_from_name(protocol);
|
||||
|
||||
expected_id = format!("{}{}", expected_id, protocol_parsed.words.concat(),);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. If a new id (Day of Dragons -> dod) results in an id that already exists
|
||||
// (Day of Defeat -> dod), then the new name should ignore rule #2 (Day of
|
||||
// Dragons -> dayofdragons).
|
||||
if seen_ids.contains_key(&expected_id) {
|
||||
rule_stack.push(IDRule::IfIDDuplicateNoAcronym);
|
||||
|
||||
let main = game
|
||||
.words
|
||||
.iter()
|
||||
.map(|w| w.trim_matches('-').to_owned())
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
|
||||
expected_id = format!("{}{}", main, suffix).to_lowercase();
|
||||
}
|
||||
|
||||
// 8. If its actually about a mod that adds the ability for queries to be
|
||||
// performed, process only the mod name.
|
||||
if !is_mod_name && id != expected_id {
|
||||
if let Some((_, mod_game)) = game.name.split_once('-') {
|
||||
let mut result = test_game_name_rule(seen_ids, id, extract_game_parts_from_name(mod_game), true);
|
||||
|
||||
if result.is_empty() {
|
||||
return result;
|
||||
} else {
|
||||
wrong_ids.append(&mut result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let duplicate = if seen_ids.insert(expected_id.clone(), game.words).is_some() {
|
||||
rule_stack.push(IDRule::NoDuplicates);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// Check ID matches
|
||||
if id != expected_id || duplicate {
|
||||
wrong_ids.push(IDFail::new(
|
||||
id.to_owned(),
|
||||
game.name.to_owned(),
|
||||
expected_id,
|
||||
rule_stack,
|
||||
));
|
||||
}
|
||||
|
||||
wrong_ids
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GameNameParsed<'a> {
|
||||
name: &'a str,
|
||||
words: Vec<String>,
|
||||
optional_parts: Vec<&'a str>,
|
||||
year: Option<u16>,
|
||||
}
|
||||
|
||||
pub fn extract_game_parts_from_name(game: &str) -> GameNameParsed {
|
||||
// Separate game name into words
|
||||
// NOTE: we have to leave "-" in to prevent hyphenated prefixes being parsed as
|
||||
// numerals
|
||||
let mut optional_game_name_parts = Vec::new();
|
||||
|
||||
let (game, paren) = extract_bracketed_suffix(game);
|
||||
|
||||
if let Some(paren) = paren {
|
||||
optional_game_name_parts.push(paren);
|
||||
}
|
||||
|
||||
let mut number_accumulator: Option<String> = None;
|
||||
|
||||
// Filter map necessary to move out words
|
||||
#[allow(clippy::unnecessary_filter_map)]
|
||||
let game_name_words: Vec<_> = game
|
||||
// First split all text on space or dash
|
||||
.split_inclusive(&[' ', '-'])
|
||||
// Remove whitespace surrounding words (leave in dash because it is important information)
|
||||
.map(|w| w.trim())
|
||||
// If a word is entirely surrounded in brackets move it to optional parts
|
||||
.filter_map(|w| {
|
||||
if w.starts_with('(') && w.ends_with(')') {
|
||||
optional_game_name_parts.push(w);
|
||||
None
|
||||
} else {
|
||||
Some(w)
|
||||
}
|
||||
})
|
||||
// Remove all characters that aren't alphanumeric or dashses
|
||||
.map(|w| {
|
||||
w.replace(
|
||||
|c: char| !c.is_ascii_digit() && !c.is_alphabetic() && c != '-',
|
||||
"",
|
||||
)
|
||||
})
|
||||
// Remove words that are empty (discounting strings that are just dashes)
|
||||
.filter(|w| !w.trim_matches('-').is_empty())
|
||||
// Combine numbers that are seperated by dashes
|
||||
// e.g. 44-45 = 4445
|
||||
// Panics if there is text after number with trailing dash (44-text)
|
||||
.filter_map(|w| {
|
||||
if number_accumulator.is_some() {
|
||||
if let Some(maybe_number) = w.strip_suffix('-') {
|
||||
if maybe_number.chars().all(|c| c.is_ascii_digit()) {
|
||||
number_accumulator.as_mut().unwrap().push_str(maybe_number);
|
||||
return None;
|
||||
} else {
|
||||
panic!("Text after number-");
|
||||
}
|
||||
} else if w.chars().all(|c| c.is_ascii_digit()) {
|
||||
let mut accumulator = number_accumulator.as_ref().unwrap().clone();
|
||||
number_accumulator = None;
|
||||
accumulator.push_str(&w);
|
||||
return Some(accumulator);
|
||||
} else {
|
||||
panic!("Text after number-");
|
||||
}
|
||||
} else if let Some(maybe_number) = w.strip_suffix('-') {
|
||||
if maybe_number.chars().all(|c| c.is_ascii_digit()) {
|
||||
number_accumulator = Some(maybe_number.to_string());
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(w)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut game_year: Option<u16> = None;
|
||||
for optional_part in &optional_game_name_parts {
|
||||
if let Some(game_year_text) = optional_part
|
||||
.strip_prefix('(')
|
||||
.and_then(|s| s.strip_suffix(')'))
|
||||
{
|
||||
if let Ok(year) = game_year_text.parse() {
|
||||
game_year = Some(year);
|
||||
break;
|
||||
}
|
||||
} else if let Ok(year) = optional_part.parse() {
|
||||
game_year = Some(year);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
GameNameParsed {
|
||||
name: game,
|
||||
words: game_name_words,
|
||||
optional_parts: optional_game_name_parts,
|
||||
year: game_year,
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate game entries and validate the id matches current rules
|
||||
pub fn test_game_name_rules<'a, I: Iterator<Item = (&'a str, &'a str)>>(games: I) -> Vec<IDFail> {
|
||||
let mut wrong_ids = Vec::with_capacity(games.size_hint().0);
|
||||
|
||||
let mut seen_ids: HashMap<String, Vec<String>> = HashMap::new();
|
||||
|
||||
// We must sort games by year so that rule 3 is applied correctly
|
||||
let mut sorted_games: Vec<_> = games
|
||||
.map(|(id, game)| {
|
||||
let game = extract_game_parts_from_name(game);
|
||||
|
||||
(id, game)
|
||||
})
|
||||
.collect();
|
||||
|
||||
sorted_games.sort_by(|(_, a_game), (_, b_game)| {
|
||||
a_game
|
||||
.year
|
||||
.cmp(&b_game.year)
|
||||
.then(a_game.name.len().cmp(&b_game.name.len()))
|
||||
});
|
||||
|
||||
let game_count = sorted_games.len();
|
||||
|
||||
for (id, game) in sorted_games {
|
||||
wrong_ids.append(&mut test_game_name_rule(&mut seen_ids, id, game, false))
|
||||
}
|
||||
|
||||
if !wrong_ids.is_empty() {
|
||||
for fail in &wrong_ids {
|
||||
println!("{:#?}", fail);
|
||||
}
|
||||
let percentage = (wrong_ids.len() * 100) / game_count;
|
||||
println!(
|
||||
"{} ({}%) IDs didn't match naming rules",
|
||||
wrong_ids.len(),
|
||||
percentage
|
||||
);
|
||||
}
|
||||
|
||||
wrong_ids
|
||||
}
|
||||
|
||||
pub fn test_single_game_rule(id: &str, name: &str) -> Vec<IDFail> { test_game_name_rules(std::iter::once((id, name))) }
|
||||
|
||||
#[cfg(test)]
|
||||
mod id_tests {
|
||||
use super::{test_game_name_rules, test_single_game_rule};
|
||||
#[test]
|
||||
fn id_rule_one() {
|
||||
assert!(test_single_game_rule("testgame", "Test Game").is_empty());
|
||||
assert!(test_single_game_rule("testgame", "TestGame").is_empty());
|
||||
|
||||
assert!(test_single_game_rule("deadcells", "Dead Cells").is_empty());
|
||||
assert!(test_single_game_rule("stalker", "S.T.A.L.K.E.R").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_rule_two() {
|
||||
assert!(test_single_game_rule("tgt", "Test Game Three").is_empty());
|
||||
assert!(test_single_game_rule("tgt", "Test Game-Three").is_empty());
|
||||
|
||||
assert!(test_single_game_rule("tboi", "The Binding of Isaac").is_empty());
|
||||
assert!(test_single_game_rule("ddd", "Dino D-Day").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_rule_three() {
|
||||
let games = vec![
|
||||
("swb22017", "Star Wars Battlefront 2 (2017)"),
|
||||
("swb2", "Star Wars Battlefront 2 (2015)"),
|
||||
];
|
||||
assert!(test_game_name_rules(games.into_iter()).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_rule_four() {
|
||||
let games = vec![("dod", "Day of Defeat"), ("dayofdragons", "Day of Dragons")];
|
||||
assert!(test_game_name_rules(games.into_iter()).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_rule_five() {
|
||||
assert!(test_single_game_rule("gta14", "Grand Theft Auto XIV").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_rule_six() {
|
||||
assert!(test_single_game_rule("l4d", "Left 4 Dead").is_empty());
|
||||
assert!(test_single_game_rule("sdtd", "7 Days to Die").is_empty());
|
||||
assert!(test_single_game_rule("teamfortress2", "Team Fortress 2").is_empty());
|
||||
assert!(test_single_game_rule("unrealtournament2003", "Unreal Tournament 2003").is_empty());
|
||||
assert!(test_single_game_rule("dhe4445", "Darkest Hour: Europe '44-'45").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_rule_seven() {
|
||||
let games = vec![
|
||||
("minecraft", "Minecraft"),
|
||||
("minecraftjava", "Minecraft (java)"),
|
||||
("minecraftbedrock", "Minecraft (bedrock)"),
|
||||
];
|
||||
assert!(test_game_name_rules(games.into_iter()).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_rule_eight() {
|
||||
assert!(test_single_game_rule("fivem", "Grand Theft Auto V - FiveM (2013)").is_empty());
|
||||
assert!(test_single_game_rule("jc3m", "Just Cause 3 - Multiplayer").is_empty());
|
||||
}
|
||||
}
|
||||
31
crates/id-tests/src/main.rs
Normal file
31
crates/id-tests/src/main.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#![cfg(feature = "cli")]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Format for input games (the same as used in node-gamedig/lib/games.js).
|
||||
type GamesInput = HashMap<String, Game>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
|
||||
struct Game {
|
||||
name: String,
|
||||
}
|
||||
|
||||
use gamedig_id_tests::test_game_name_rules;
|
||||
|
||||
fn main() {
|
||||
let games: GamesInput = if let Some(file) = std::env::args_os().skip(1).next() {
|
||||
let file = std::fs::OpenOptions::new().read(true).open(file).unwrap();
|
||||
|
||||
serde_json::from_reader(file).unwrap()
|
||||
} else {
|
||||
serde_json::from_reader(std::io::stdin().lock()).unwrap()
|
||||
};
|
||||
|
||||
let failed = test_game_name_rules(
|
||||
games
|
||||
.iter()
|
||||
.map(|(key, game)| (key.as_str(), game.name.as_str())),
|
||||
);
|
||||
|
||||
assert!(failed.is_empty());
|
||||
}
|
||||
66
crates/id-tests/src/utils.rs
Normal file
66
crates/id-tests/src/utils.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/// Split a str when characters swap between being digits and not digits.
|
||||
pub fn split_on_switch_between_alpha_numeric(text: &str) -> Vec<String> {
|
||||
if text.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let mut parts = Vec::with_capacity(text.len());
|
||||
let mut current = Vec::with_capacity(text.len());
|
||||
|
||||
let mut iter = text.chars();
|
||||
let c = iter.next().unwrap();
|
||||
let mut last_was_numeric = c.is_ascii_digit();
|
||||
current.push(c);
|
||||
|
||||
for c in iter {
|
||||
if c.is_ascii_digit() == last_was_numeric {
|
||||
current.push(c);
|
||||
} else {
|
||||
parts.push(current.iter().collect());
|
||||
current.clear();
|
||||
current.push(c);
|
||||
last_was_numeric = !last_was_numeric;
|
||||
}
|
||||
}
|
||||
|
||||
parts.push(current.into_iter().collect());
|
||||
|
||||
parts
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_correctly() {
|
||||
assert_eq!(
|
||||
split_on_switch_between_alpha_numeric("2D45A"),
|
||||
&["2", "D", "45", "A"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_symbol_broken_numbers() {
|
||||
let game_name = super::extract_game_parts_from_name("Darkest Hour: Europe '44-'45");
|
||||
assert_eq!(game_name.words, &["Darkest", "Hour", "Europe", "4445"]);
|
||||
}
|
||||
|
||||
/// Extract parts at end of string enclosed in brackets.
|
||||
pub fn extract_bracketed_suffix(text: &str) -> (&str, Option<&str>) {
|
||||
if let Some(text) = text.strip_suffix(')') {
|
||||
if let Some((text, extra)) = text.rsplit_once('(') {
|
||||
return (text, Some(extra));
|
||||
}
|
||||
}
|
||||
|
||||
(text, None)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_brackets_correctly() {
|
||||
assert_eq!(
|
||||
extract_bracketed_suffix("no brackets here"),
|
||||
("no brackets here", None)
|
||||
);
|
||||
assert_eq!(
|
||||
extract_bracketed_suffix("Game name (with protocol here)"),
|
||||
("Game name ", Some("with protocol here"))
|
||||
);
|
||||
}
|
||||
|
|
@ -42,6 +42,9 @@ pcap-file = { version = "2.0", optional = true }
|
|||
pnet_packet = { version = "0.34", optional = true }
|
||||
lazy_static = { version = "1.4", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
gamedig-id-tests = { path = "../id-tests", no-default-features = true }
|
||||
|
||||
# Examples
|
||||
[[example]]
|
||||
name = "minecraft"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
use gamedig::{
|
||||
protocols::types::{CommonResponse, ExtraRequestSettings, TimeoutSettings},
|
||||
protocols::types::CommonResponse,
|
||||
query_with_timeout_and_extra_settings,
|
||||
ExtraRequestSettings,
|
||||
GDResult,
|
||||
Game,
|
||||
TimeoutSettings,
|
||||
GAMES,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use gamedig::protocols::types::TimeoutSettings;
|
||||
use gamedig::protocols::valve;
|
||||
use gamedig::protocols::valve::{Engine, GatheringSettings};
|
||||
use gamedig::TimeoutSettings;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::time::Duration;
|
||||
|
||||
|
|
|
|||
|
|
@ -361,6 +361,55 @@ impl StringDecoder for Utf8Decoder {
|
|||
}
|
||||
}
|
||||
|
||||
/// A decoder for UTF-8 encoded strings prefixed by a single byte denoting the
|
||||
/// string's length.
|
||||
///
|
||||
/// This decoder uses a single null byte (`0x00`) as the default delimiter.
|
||||
pub struct Utf8LengthPrefixedDecoder;
|
||||
|
||||
impl StringDecoder for Utf8LengthPrefixedDecoder {
|
||||
type Delimiter = [u8; 1];
|
||||
|
||||
const DELIMITER: Self::Delimiter = [0x00];
|
||||
|
||||
/// Decodes a UTF-8 string from the given data, updating the cursor position
|
||||
/// accordingly.
|
||||
fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult<String> {
|
||||
// Find the maximum length of the string
|
||||
let length = *data
|
||||
.first()
|
||||
.ok_or(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
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_gdresult_ok() {
|
||||
let result: GDResult<u32> = Ok(42);
|
||||
assert_eq!(result.unwrap(), 42);
|
||||
assert_eq!(result, Ok(42));
|
||||
}
|
||||
|
||||
// Testing Err variant of the GDResult type
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use crate::protocols::valve::GatheringSettings;
|
|||
use phf::{phf_map, Map};
|
||||
|
||||
macro_rules! game {
|
||||
($name: literal, $default_port: literal, $protocol: expr) => {
|
||||
($name: literal, $default_port: expr, $protocol: expr) => {
|
||||
game!(
|
||||
$name,
|
||||
$default_port,
|
||||
|
|
@ -18,7 +18,7 @@ macro_rules! game {
|
|||
)
|
||||
};
|
||||
|
||||
($name: literal, $default_port: literal, $protocol: expr, $extra_request_settings: expr) => {
|
||||
($name: literal, $default_port: expr, $protocol: expr, $extra_request_settings: expr) => {
|
||||
Game {
|
||||
name: $name,
|
||||
default_port: $default_port,
|
||||
|
|
@ -39,13 +39,24 @@ pub static GAMES: Map<&'static str, Game> = phf_map! {
|
|||
"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: true,
|
||||
rules: false,
|
||||
check_app_id: true,
|
||||
}.into_extra()),
|
||||
"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))),
|
||||
"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))),
|
||||
"atlas" => game!("ATLAS", 57561, Protocol::Valve(Engine::new(834_910))),
|
||||
"avorion" => game!("Avorion", 27020, Protocol::Valve(Engine::new(445_220))),
|
||||
"barotrauma" => game!("Barotrauma", 27016, Protocol::Valve(Engine::new(602_960))),
|
||||
"basedefense" => game!("Base Defense", 27015, Protocol::Valve(Engine::new(632_730)), GatheringSettings {
|
||||
players: true,
|
||||
rules: false,
|
||||
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)),
|
||||
|
|
@ -81,16 +92,24 @@ pub static GAMES: Map<&'static str, Game> = phf_map! {
|
|||
"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))),
|
||||
"postscriptum" => game!("Post Scriptum", 10037, Protocol::Valve(Engine::new(736_220))),
|
||||
"projectzomboid" => game!("Project Zomboid", 16261, Protocol::Valve(Engine::new(108_600))),
|
||||
"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: true,
|
||||
rules: false,
|
||||
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)),
|
||||
"serioussam" => game!("Serious Sam", 25601, Protocol::Gamespy(GameSpyVersion::One)),
|
||||
"squad" => game!("Squad", 27165, Protocol::Valve(Engine::new(393_380))),
|
||||
"theforest" => game!("The Forest", 27016, Protocol::Valve(Engine::new(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))),
|
||||
|
|
@ -106,10 +125,12 @@ pub static GAMES: Map<&'static str, Game> = phf_map! {
|
|||
"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)),
|
||||
"darkesthour" => game!("Darkest Hour: Europe '44-'45 (2008)", 7758, Protocol::Unreal2),
|
||||
"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),
|
||||
"ut2003" => game!("Unreal Tournament 2003", 7758, Protocol::Unreal2),
|
||||
"ut2004" => game!("Unreal Tournament 2004", 7778, Protocol::Unreal2),
|
||||
"unrealtournament2003" => game!("Unreal Tournament 2003", 7758, Protocol::Unreal2),
|
||||
"unrealtournament2004" => game!("Unreal Tournament 2004", 7778, Protocol::Unreal2),
|
||||
"zps" => game!("Zombie Panic: Source", 27015, Protocol::Valve(Engine::new(17_500))),
|
||||
"mindustry" => game!("Mindustry", crate::games::mindustry::DEFAULT_PORT, Protocol::PROPRIETARY(ProprietaryProtocol::Mindustry)),
|
||||
};
|
||||
|
|
|
|||
8
crates/lib/src/games/ffow/mod.rs
Normal file
8
crates/lib/src/games/ffow/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/// The implementation.
|
||||
/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/ffow.js)
|
||||
pub mod protocol;
|
||||
/// All types used by the implementation.
|
||||
pub mod types;
|
||||
|
||||
pub use protocol::*;
|
||||
pub use types::*;
|
||||
|
|
@ -1,64 +1,11 @@
|
|||
use crate::buffer::{Buffer, Utf8Decoder};
|
||||
use crate::protocols::types::{CommonResponse, TimeoutSettings};
|
||||
use crate::games::ffow::types::Response;
|
||||
use crate::protocols::types::TimeoutSettings;
|
||||
use crate::protocols::valve::{Engine, Environment, Server, ValveProtocol};
|
||||
use crate::protocols::GenericResponse;
|
||||
use crate::GDResult;
|
||||
use byteorder::LittleEndian;
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
/// The query response.
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct Response {
|
||||
/// Protocol used by the server.
|
||||
pub protocol_version: u8,
|
||||
/// Name of the server.
|
||||
pub name: String,
|
||||
/// Map name.
|
||||
pub active_mod: String,
|
||||
/// Running game mode.
|
||||
pub game_mode: String,
|
||||
/// The version that the server is running on.
|
||||
pub game_version: String,
|
||||
/// Description of the server.
|
||||
pub description: String,
|
||||
/// Current map.
|
||||
pub map: String,
|
||||
/// Number of players on the server.
|
||||
pub players_online: u8,
|
||||
/// Maximum number of players the server reports it can hold.
|
||||
pub players_maximum: u8,
|
||||
/// Dedicated, NonDedicated or SourceTV
|
||||
pub server_type: Server,
|
||||
/// The Operating System that the server is on.
|
||||
pub environment_type: Environment,
|
||||
/// Indicates whether the server requires a password.
|
||||
pub has_password: bool,
|
||||
/// Indicates whether the server uses VAC.
|
||||
pub vac_secured: bool,
|
||||
/// Current round index.
|
||||
pub round: u8,
|
||||
/// Maximum amount of rounds.
|
||||
pub rounds_maximum: u8,
|
||||
/// Time left for the current round in seconds.
|
||||
pub time_left: u16,
|
||||
}
|
||||
|
||||
impl CommonResponse for Response {
|
||||
fn as_original(&self) -> GenericResponse { GenericResponse::FFOW(self) }
|
||||
|
||||
fn name(&self) -> Option<&str> { Some(&self.name) }
|
||||
fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
|
||||
fn description(&self) -> Option<&str> { Some(&self.description) }
|
||||
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
|
||||
fn map(&self) -> Option<&str> { Some(&self.map) }
|
||||
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
|
||||
fn players_maximum(&self) -> u32 { self.players_maximum.into() }
|
||||
fn players_online(&self) -> u32 { self.players_online.into() }
|
||||
}
|
||||
|
||||
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
|
||||
|
||||
pub fn query_with_timeout(
|
||||
56
crates/lib/src/games/ffow/types.rs
Normal file
56
crates/lib/src/games/ffow/types.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
use crate::protocols::types::CommonResponse;
|
||||
use crate::protocols::valve::{Environment, Server};
|
||||
use crate::protocols::GenericResponse;
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The query response.
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct Response {
|
||||
/// Protocol used by the server.
|
||||
pub protocol_version: u8,
|
||||
/// Name of the server.
|
||||
pub name: String,
|
||||
/// Map name.
|
||||
pub active_mod: String,
|
||||
/// Running game mode.
|
||||
pub game_mode: String,
|
||||
/// The version that the server is running on.
|
||||
pub game_version: String,
|
||||
/// Description of the server.
|
||||
pub description: String,
|
||||
/// Current map.
|
||||
pub map: String,
|
||||
/// Number of players on the server.
|
||||
pub players_online: u8,
|
||||
/// Maximum number of players the server reports it can hold.
|
||||
pub players_maximum: u8,
|
||||
/// Dedicated, NonDedicated or SourceTV
|
||||
pub server_type: Server,
|
||||
/// The Operating System that the server is on.
|
||||
pub environment_type: Environment,
|
||||
/// Indicates whether the server requires a password.
|
||||
pub has_password: bool,
|
||||
/// Indicates whether the server uses VAC.
|
||||
pub vac_secured: bool,
|
||||
/// Current round index.
|
||||
pub round: u8,
|
||||
/// Maximum amount of rounds.
|
||||
pub rounds_maximum: u8,
|
||||
/// Time left for the current round in seconds.
|
||||
pub time_left: u16,
|
||||
}
|
||||
|
||||
impl CommonResponse for Response {
|
||||
fn as_original(&self) -> GenericResponse { GenericResponse::FFOW(self) }
|
||||
|
||||
fn name(&self) -> Option<&str> { Some(&self.name) }
|
||||
fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
|
||||
fn description(&self) -> Option<&str> { Some(&self.description) }
|
||||
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
|
||||
fn map(&self) -> Option<&str> { Some(&self.map) }
|
||||
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
|
||||
fn players_maximum(&self) -> u32 { self.players_maximum.into() }
|
||||
fn players_online(&self) -> u32 { self.players_online.into() }
|
||||
}
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
use crate::buffer::{Buffer, Utf8Decoder};
|
||||
use crate::protocols::gamespy::common::has_password;
|
||||
use crate::protocols::gamespy::three::{data_to_map, GameSpy3};
|
||||
use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer, TimeoutSettings};
|
||||
use crate::protocols::GenericResponse;
|
||||
use crate::GDErrorKind::{PacketBad, TypeParse};
|
||||
use crate::{GDErrorKind, GDResult};
|
||||
use byteorder::BigEndian;
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct Player {
|
||||
name: String,
|
||||
steam_id: String,
|
||||
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 {
|
||||
game_version: String,
|
||||
description: String,
|
||||
name: String,
|
||||
has_password: bool,
|
||||
players: Vec<Player>,
|
||||
players_maximum: u32,
|
||||
players_online: u32,
|
||||
}
|
||||
|
||||
impl CommonResponse for Response {
|
||||
fn as_original(&self) -> GenericResponse { GenericResponse::JC2M(self) }
|
||||
|
||||
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
|
||||
fn description(&self) -> Option<&str> { Some(&self.description) }
|
||||
fn name(&self) -> Option<&str> { Some(&self.name) }
|
||||
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
|
||||
fn players_maximum(&self) -> u32 { self.players_maximum }
|
||||
fn players_online(&self) -> u32 { self.players_online }
|
||||
|
||||
fn players(&self) -> Option<Vec<&dyn crate::protocols::types::CommonPlayer>> {
|
||||
Some(
|
||||
self.players
|
||||
.iter()
|
||||
.map(|p| p as &dyn CommonPlayer)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_players_and_teams(packet: &[u8]) -> GDResult<Vec<Player>> {
|
||||
let mut buf = Buffer::<BigEndian>::new(packet);
|
||||
|
||||
let count = buf.read::<u16>()?;
|
||||
let mut players = Vec::with_capacity(count as usize);
|
||||
|
||||
while buf.remaining_length() != 0 {
|
||||
players.push(Player {
|
||||
name: buf.read_string::<Utf8Decoder>(None)?,
|
||||
steam_id: buf.read_string::<Utf8Decoder>(None)?,
|
||||
ping: buf.read::<u16>()?,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(players)
|
||||
}
|
||||
|
||||
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
|
||||
|
||||
pub fn query_with_timeout(
|
||||
address: &IpAddr,
|
||||
port: Option<u16>,
|
||||
timeout_settings: Option<TimeoutSettings>,
|
||||
) -> GDResult<Response> {
|
||||
let mut client = GameSpy3::new_custom(
|
||||
&SocketAddr::new(*address, port.unwrap_or(7777)),
|
||||
timeout_settings,
|
||||
[0xFF, 0xFF, 0xFF, 0x02],
|
||||
true,
|
||||
)?;
|
||||
|
||||
let packets = client.get_server_packets()?;
|
||||
let data = packets
|
||||
.get(0)
|
||||
.ok_or(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(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(GDErrorKind::PacketBad)?,
|
||||
description: server_vars
|
||||
.remove("description")
|
||||
.ok_or(GDErrorKind::PacketBad)?,
|
||||
name: server_vars
|
||||
.remove("hostname")
|
||||
.ok_or(GDErrorKind::PacketBad)?,
|
||||
has_password: has_password(&mut server_vars)?,
|
||||
players,
|
||||
players_maximum,
|
||||
players_online,
|
||||
})
|
||||
}
|
||||
8
crates/lib/src/games/jc2m/mod.rs
Normal file
8
crates/lib/src/games/jc2m/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/// The implementation.
|
||||
/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/jc2mp.js)
|
||||
pub mod protocol;
|
||||
/// All types used by the implementation.
|
||||
pub mod types;
|
||||
|
||||
pub use protocol::*;
|
||||
pub use types::*;
|
||||
75
crates/lib/src/games/jc2m/protocol.rs
Normal file
75
crates/lib/src/games/jc2m/protocol.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
use crate::buffer::{Buffer, Utf8Decoder};
|
||||
use crate::jc2m::{Player, Response};
|
||||
use crate::protocols::gamespy::common::has_password;
|
||||
use crate::protocols::gamespy::three::{data_to_map, GameSpy3};
|
||||
use crate::protocols::types::TimeoutSettings;
|
||||
use crate::GDErrorKind::{PacketBad, TypeParse};
|
||||
use crate::GDResult;
|
||||
use byteorder::BigEndian;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
fn parse_players_and_teams(packet: &[u8]) -> GDResult<Vec<Player>> {
|
||||
let mut buf = Buffer::<BigEndian>::new(packet);
|
||||
|
||||
let count = buf.read::<u16>()?;
|
||||
let mut players = Vec::with_capacity(count as usize);
|
||||
|
||||
while buf.remaining_length() != 0 {
|
||||
players.push(Player {
|
||||
name: buf.read_string::<Utf8Decoder>(None)?,
|
||||
steam_id: buf.read_string::<Utf8Decoder>(None)?,
|
||||
ping: buf.read::<u16>()?,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(players)
|
||||
}
|
||||
|
||||
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
|
||||
|
||||
pub fn query_with_timeout(
|
||||
address: &IpAddr,
|
||||
port: Option<u16>,
|
||||
timeout_settings: Option<TimeoutSettings>,
|
||||
) -> GDResult<Response> {
|
||||
let mut client = GameSpy3::new_custom(
|
||||
&SocketAddr::new(*address, port.unwrap_or(7777)),
|
||||
timeout_settings,
|
||||
[0xFF, 0xFF, 0xFF, 0x02],
|
||||
true,
|
||||
)?;
|
||||
|
||||
let packets = client.get_server_packets()?;
|
||||
let data = packets
|
||||
.get(0)
|
||||
.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,
|
||||
})
|
||||
}
|
||||
50
crates/lib/src/games/jc2m/types.rs
Normal file
50
crates/lib/src/games/jc2m/types.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer};
|
||||
use crate::protocols::GenericResponse;
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct Player {
|
||||
pub name: String,
|
||||
pub steam_id: String,
|
||||
pub ping: u16,
|
||||
}
|
||||
|
||||
impl CommonPlayer for Player {
|
||||
fn as_original(&self) -> GenericPlayer { GenericPlayer::JCMP2(self) }
|
||||
|
||||
fn name(&self) -> &str { &self.name }
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct Response {
|
||||
pub game_version: String,
|
||||
pub description: String,
|
||||
pub name: String,
|
||||
pub has_password: bool,
|
||||
pub players: Vec<Player>,
|
||||
pub players_maximum: u32,
|
||||
pub players_online: u32,
|
||||
}
|
||||
|
||||
impl CommonResponse for Response {
|
||||
fn as_original(&self) -> GenericResponse { GenericResponse::JC2M(self) }
|
||||
|
||||
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
|
||||
fn description(&self) -> Option<&str> { Some(&self.description) }
|
||||
fn name(&self) -> Option<&str> { Some(&self.name) }
|
||||
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
|
||||
fn players_maximum(&self) -> u32 { self.players_maximum }
|
||||
fn players_online(&self) -> u32 { self.players_online }
|
||||
|
||||
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
|
||||
Some(
|
||||
self.players
|
||||
.iter()
|
||||
.map(|p| p as &dyn CommonPlayer)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
25
crates/lib/src/games/mindustry/mod.rs
Normal file
25
crates/lib/src/games/mindustry/mod.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
//! Mindustry game ping (v146)
|
||||
//!
|
||||
//! [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/ArcNetProvider.java#L225-L259)
|
||||
|
||||
use std::{net::IpAddr, net::SocketAddr};
|
||||
|
||||
use crate::{GDResult, TimeoutSettings};
|
||||
|
||||
use self::types::ServerData;
|
||||
|
||||
pub mod types;
|
||||
|
||||
pub mod protocol;
|
||||
|
||||
/// Default mindustry server port
|
||||
///
|
||||
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/Vars.java#L141-L142)
|
||||
pub const DEFAULT_PORT: u16 = 6567;
|
||||
|
||||
/// Query a mindustry server.
|
||||
pub fn query(ip: &IpAddr, port: Option<u16>, timeout_settings: &Option<TimeoutSettings>) -> GDResult<ServerData> {
|
||||
let address = SocketAddr::new(*ip, port.unwrap_or(DEFAULT_PORT));
|
||||
|
||||
protocol::query_with_retries(&address, timeout_settings)
|
||||
}
|
||||
58
crates/lib/src/games/mindustry/protocol.rs
Normal file
58
crates/lib/src/games/mindustry/protocol.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
use std::net::SocketAddr;
|
||||
|
||||
use crate::{
|
||||
buffer::{self, Buffer},
|
||||
socket::{Socket, UdpSocket},
|
||||
utils,
|
||||
GDResult,
|
||||
TimeoutSettings,
|
||||
};
|
||||
|
||||
use super::types::ServerData;
|
||||
|
||||
/// Mindustry max datagram packet size.
|
||||
pub const MAX_BUFFER_SIZE: usize = 500;
|
||||
|
||||
/// Send a ping packet.
|
||||
///
|
||||
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/ArcNetProvider.java#L248)
|
||||
pub fn send_ping(socket: &mut UdpSocket) -> GDResult<()> { socket.send(&[-2i8 as u8, 1i8 as u8]) }
|
||||
|
||||
/// Parse server data.
|
||||
///
|
||||
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/NetworkIO.java#L122-L135)
|
||||
pub fn parse_server_data<B: byteorder::ByteOrder, D: buffer::StringDecoder>(
|
||||
buffer: &mut Buffer<B>,
|
||||
) -> GDResult<ServerData> {
|
||||
Ok(ServerData {
|
||||
host: buffer.read_string::<D>(None)?,
|
||||
map: buffer.read_string::<D>(None)?,
|
||||
players: buffer.read()?,
|
||||
wave: buffer.read()?,
|
||||
version: buffer.read()?,
|
||||
version_type: buffer.read_string::<D>(None)?,
|
||||
gamemode: buffer.read::<u8>()?.try_into()?,
|
||||
player_limit: buffer.read()?,
|
||||
description: buffer.read_string::<D>(None)?,
|
||||
mode_name: buffer.read_string::<D>(None).ok(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Query a Mindustry server (without retries).
|
||||
pub fn query(address: &SocketAddr, timeout_settings: &Option<TimeoutSettings>) -> GDResult<ServerData> {
|
||||
let mut socket = UdpSocket::new(address, timeout_settings)?;
|
||||
|
||||
send_ping(&mut socket)?;
|
||||
|
||||
let socket_data = socket.receive(Some(MAX_BUFFER_SIZE))?;
|
||||
let mut buffer = Buffer::new(&socket_data);
|
||||
|
||||
parse_server_data::<byteorder::BigEndian, buffer::Utf8LengthPrefixedDecoder>(&mut buffer)
|
||||
}
|
||||
|
||||
/// Query a Mindustry server.
|
||||
pub fn query_with_retries(address: &SocketAddr, timeout_settings: &Option<TimeoutSettings>) -> GDResult<ServerData> {
|
||||
let retries = TimeoutSettings::get_retries_or_default(timeout_settings);
|
||||
|
||||
utils::retry_on_timeout(retries, || query(address, timeout_settings))
|
||||
}
|
||||
108
crates/lib/src/games/mindustry/types.rs
Normal file
108
crates/lib/src/games/mindustry/types.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
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, PartialEq)]
|
||||
pub struct ServerData {
|
||||
pub host: String,
|
||||
pub map: String,
|
||||
pub players: i32,
|
||||
pub wave: i32,
|
||||
pub version: i32,
|
||||
pub version_type: String,
|
||||
pub gamemode: GameMode,
|
||||
pub player_limit: i32,
|
||||
pub description: String,
|
||||
pub mode_name: Option<String>,
|
||||
}
|
||||
|
||||
/// Mindustry game mode
|
||||
///
|
||||
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/game/Gamemode.java)
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum GameMode {
|
||||
Survival,
|
||||
Sandbox,
|
||||
Attack,
|
||||
PVP,
|
||||
Editor,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for GameMode {
|
||||
type Error = GDErrorKind;
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
use GameMode::*;
|
||||
Ok(match value {
|
||||
0 => Survival,
|
||||
1 => Sandbox,
|
||||
2 => Attack,
|
||||
3 => PVP,
|
||||
4 => Editor,
|
||||
_ => return Err(GDErrorKind::TypeParse),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl GameMode {
|
||||
fn as_str(&self) -> &'static str {
|
||||
use GameMode::*;
|
||||
match self {
|
||||
Survival => "survival",
|
||||
Sandbox => "sandbox",
|
||||
Attack => "attack",
|
||||
PVP => "pvp",
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
|
@ -115,7 +115,7 @@ impl Default for RequestSettings {
|
|||
impl RequestSettings {
|
||||
/// Make a new *RequestSettings* with just the hostname, the protocol
|
||||
/// version defaults to -1
|
||||
pub fn new_just_hostname(hostname: String) -> Self {
|
||||
pub const fn new_just_hostname(hostname: String) -> Self {
|
||||
Self {
|
||||
hostname,
|
||||
protocol_version: -1,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
//! Currently supported games.
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod gamespy;
|
||||
pub mod quake;
|
||||
pub mod unreal2;
|
||||
|
|
@ -19,137 +16,23 @@ pub mod battalion1944;
|
|||
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;
|
||||
|
||||
use crate::protocols::gamespy::GameSpyVersion;
|
||||
use crate::protocols::quake::QuakeVersion;
|
||||
use crate::protocols::types::{CommonResponse, ExtraRequestSettings, ProprietaryProtocol, TimeoutSettings};
|
||||
use crate::protocols::{self, Protocol};
|
||||
use crate::GDResult;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
pub mod types;
|
||||
pub use types::*;
|
||||
|
||||
/// 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,
|
||||
}
|
||||
pub mod query;
|
||||
pub use query::*;
|
||||
|
||||
#[cfg(feature = "game_defs")]
|
||||
mod definitions;
|
||||
|
||||
#[cfg(feature = "game_defs")]
|
||||
pub use definitions::GAMES;
|
||||
|
||||
/// Make a query given a game definition
|
||||
#[inline]
|
||||
pub fn query(game: &Game, address: &IpAddr, port: Option<u16>) -> GDResult<Box<dyn CommonResponse>> {
|
||||
query_with_timeout_and_extra_settings(game, address, port, None, None)
|
||||
}
|
||||
|
||||
/// Make a query given a game definition and timeout settings
|
||||
#[inline]
|
||||
pub fn query_with_timeout(
|
||||
game: &Game,
|
||||
address: &IpAddr,
|
||||
port: Option<u16>,
|
||||
timeout_settings: Option<TimeoutSettings>,
|
||||
) -> GDResult<Box<dyn CommonResponse>> {
|
||||
query_with_timeout_and_extra_settings(game, address, port, timeout_settings, None)
|
||||
}
|
||||
|
||||
/// Make a query given a game definition, timeout settings, and extra settings
|
||||
pub fn query_with_timeout_and_extra_settings(
|
||||
game: &Game,
|
||||
address: &IpAddr,
|
||||
port: Option<u16>,
|
||||
timeout_settings: Option<TimeoutSettings>,
|
||||
extra_settings: Option<ExtraRequestSettings>,
|
||||
) -> GDResult<Box<dyn CommonResponse>> {
|
||||
let socket_addr = SocketAddr::new(*address, port.unwrap_or(game.default_port));
|
||||
Ok(match &game.protocol {
|
||||
Protocol::Valve(engine) => {
|
||||
protocols::valve::query(
|
||||
&socket_addr,
|
||||
*engine,
|
||||
extra_settings
|
||||
.or(Option::from(game.request_settings.clone()))
|
||||
.map(ExtraRequestSettings::into),
|
||||
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::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::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)?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
118
crates/lib/src/games/query.rs
Normal file
118
crates/lib/src/games/query.rs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
//! Generic query functions
|
||||
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
use crate::games::types::Game;
|
||||
use crate::games::{ffow, jc2m, mindustry, minecraft, savage2, theship};
|
||||
use crate::protocols;
|
||||
use crate::protocols::gamespy::GameSpyVersion;
|
||||
use crate::protocols::quake::QuakeVersion;
|
||||
use crate::protocols::types::{CommonResponse, ExtraRequestSettings, ProprietaryProtocol, Protocol, TimeoutSettings};
|
||||
use crate::GDResult;
|
||||
|
||||
/// Make a query given a game definition
|
||||
#[inline]
|
||||
pub fn query(game: &Game, address: &IpAddr, port: Option<u16>) -> GDResult<Box<dyn CommonResponse>> {
|
||||
query_with_timeout_and_extra_settings(game, address, port, None, None)
|
||||
}
|
||||
|
||||
/// Make a query given a game definition and timeout settings
|
||||
#[inline]
|
||||
pub fn query_with_timeout(
|
||||
game: &Game,
|
||||
address: &IpAddr,
|
||||
port: Option<u16>,
|
||||
timeout_settings: Option<TimeoutSettings>,
|
||||
) -> GDResult<Box<dyn CommonResponse>> {
|
||||
query_with_timeout_and_extra_settings(game, address, port, timeout_settings, None)
|
||||
}
|
||||
|
||||
/// Make a query given a game definition, timeout settings, and extra settings
|
||||
pub fn query_with_timeout_and_extra_settings(
|
||||
game: &Game,
|
||||
address: &IpAddr,
|
||||
port: Option<u16>,
|
||||
timeout_settings: Option<TimeoutSettings>,
|
||||
extra_settings: Option<ExtraRequestSettings>,
|
||||
) -> GDResult<Box<dyn CommonResponse>> {
|
||||
let socket_addr = SocketAddr::new(*address, port.unwrap_or(game.default_port));
|
||||
Ok(match &game.protocol {
|
||||
Protocol::Valve(engine) => {
|
||||
protocols::valve::query(
|
||||
&socket_addr,
|
||||
*engine,
|
||||
extra_settings
|
||||
.or_else(|| Option::from(game.request_settings.clone()))
|
||||
.map(ExtraRequestSettings::into),
|
||||
timeout_settings,
|
||||
)
|
||||
.map(Box::new)?
|
||||
}
|
||||
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)?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
8
crates/lib/src/games/savage2/mod.rs
Normal file
8
crates/lib/src/games/savage2/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/// The implementation.
|
||||
/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/savage2.js)
|
||||
pub mod protocol;
|
||||
/// All types used by the implementation.
|
||||
pub mod types;
|
||||
|
||||
pub use protocol::*;
|
||||
pub use types::*;
|
||||
37
crates/lib/src/games/savage2/protocol.rs
Normal file
37
crates/lib/src/games/savage2/protocol.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
use crate::buffer::{Buffer, Utf8Decoder};
|
||||
use crate::games::savage2::types::Response;
|
||||
use crate::protocols::types::TimeoutSettings;
|
||||
use crate::socket::{Socket, UdpSocket};
|
||||
use crate::GDResult;
|
||||
use byteorder::LittleEndian;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
|
||||
|
||||
pub fn query_with_timeout(
|
||||
address: &IpAddr,
|
||||
port: Option<u16>,
|
||||
timeout_settings: Option<TimeoutSettings>,
|
||||
) -> GDResult<Response> {
|
||||
let addr = &SocketAddr::new(*address, port.unwrap_or(11235));
|
||||
let mut socket = UdpSocket::new(addr, &timeout_settings)?;
|
||||
socket.send(&[0x01])?;
|
||||
let data = socket.receive(None)?;
|
||||
let mut buffer = Buffer::<LittleEndian>::new(&data);
|
||||
|
||||
buffer.move_cursor(12)?;
|
||||
|
||||
Ok(Response {
|
||||
name: buffer.read_string::<Utf8Decoder>(None)?,
|
||||
players_online: buffer.read::<u8>()?,
|
||||
players_maximum: buffer.read::<u8>()?,
|
||||
time: buffer.read_string::<Utf8Decoder>(None)?,
|
||||
map: buffer.read_string::<Utf8Decoder>(None)?,
|
||||
next_map: buffer.read_string::<Utf8Decoder>(None)?,
|
||||
location: buffer.read_string::<Utf8Decoder>(None)?,
|
||||
players_minimum: buffer.read::<u8>()?,
|
||||
game_mode: buffer.read_string::<Utf8Decoder>(None)?,
|
||||
protocol_version: buffer.read_string::<Utf8Decoder>(None)?,
|
||||
level_minimum: buffer.read::<u8>()?,
|
||||
})
|
||||
}
|
||||
30
crates/lib/src/games/savage2/types.rs
Normal file
30
crates/lib/src/games/savage2/types.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
use crate::protocols::types::CommonResponse;
|
||||
use crate::protocols::GenericResponse;
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Response {
|
||||
pub name: String,
|
||||
pub players_online: u8,
|
||||
pub players_maximum: u8,
|
||||
pub players_minimum: u8,
|
||||
pub time: String,
|
||||
pub map: String,
|
||||
pub next_map: String,
|
||||
pub location: String,
|
||||
pub game_mode: String,
|
||||
pub protocol_version: String,
|
||||
pub level_minimum: u8,
|
||||
}
|
||||
|
||||
impl CommonResponse for Response {
|
||||
fn as_original(&self) -> GenericResponse { GenericResponse::Savage2(self) }
|
||||
|
||||
fn name(&self) -> Option<&str> { Some(&self.name) }
|
||||
fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
|
||||
fn map(&self) -> Option<&str> { Some(&self.map) }
|
||||
fn players_maximum(&self) -> u32 { self.players_maximum.into() }
|
||||
fn players_online(&self) -> u32 { self.players_online.into() }
|
||||
}
|
||||
8
crates/lib/src/games/theship/mod.rs
Normal file
8
crates/lib/src/games/theship/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/// The implementation.
|
||||
/// Reference: [server queries](https://developer.valvesoftware.com/wiki/Server_queries)
|
||||
pub mod protocol;
|
||||
/// All types used by the implementation.
|
||||
pub mod types;
|
||||
|
||||
pub use protocol::*;
|
||||
pub use types::*;
|
||||
23
crates/lib/src/games/theship/protocol.rs
Normal file
23
crates/lib/src/games/theship/protocol.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
use crate::games::theship::types::Response;
|
||||
use crate::protocols::types::TimeoutSettings;
|
||||
use crate::protocols::valve;
|
||||
use crate::protocols::valve::Engine;
|
||||
use crate::GDResult;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
|
||||
|
||||
pub fn query_with_timeout(
|
||||
address: &IpAddr,
|
||||
port: Option<u16>,
|
||||
timeout_settings: Option<TimeoutSettings>,
|
||||
) -> GDResult<Response> {
|
||||
let valve_response = valve::query(
|
||||
&SocketAddr::new(*address, port.unwrap_or(27015)),
|
||||
Engine::new(2400),
|
||||
None,
|
||||
timeout_settings,
|
||||
)?;
|
||||
|
||||
Response::new_from_valve_response(valve_response)
|
||||
}
|
||||
|
|
@ -1,17 +1,10 @@
|
|||
use crate::{
|
||||
protocols::{
|
||||
types::{CommonPlayer, CommonResponse, GenericPlayer, TimeoutSettings},
|
||||
valve::{self, get_optional_extracted_data, Server, ServerPlayer},
|
||||
GenericResponse,
|
||||
},
|
||||
GDErrorKind::PacketBad,
|
||||
GDResult,
|
||||
};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
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;
|
||||
|
||||
use crate::protocols::valve::Engine;
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
|
@ -127,20 +120,3 @@ impl Response {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
|
||||
|
||||
pub fn query_with_timeout(
|
||||
address: &IpAddr,
|
||||
port: Option<u16>,
|
||||
timeout_settings: Option<TimeoutSettings>,
|
||||
) -> GDResult<Response> {
|
||||
let valve_response = valve::query(
|
||||
&SocketAddr::new(*address, port.unwrap_or(27015)),
|
||||
Engine::new(2400),
|
||||
None,
|
||||
timeout_settings,
|
||||
)?;
|
||||
|
||||
Response::new_from_valve_response(valve_response)
|
||||
}
|
||||
20
crates/lib/src/games/types.rs
Normal file
20
crates/lib/src/games/types.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
//! Game related types
|
||||
|
||||
use crate::protocols::types::{ExtraRequestSettings, Protocol};
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Definition of a game
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Game {
|
||||
/// Full name of the game
|
||||
pub name: &'static str,
|
||||
/// Default port used by game
|
||||
pub default_port: u16,
|
||||
/// The protocol the game's query uses
|
||||
pub protocol: Protocol,
|
||||
/// Request settings.
|
||||
pub request_settings: ExtraRequestSettings,
|
||||
}
|
||||
|
|
@ -8,8 +8,20 @@ game_query_mod!(
|
|||
Engine::new(33930),
|
||||
2304
|
||||
);
|
||||
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: true,
|
||||
rules: false,
|
||||
check_app_id: true,
|
||||
}
|
||||
);
|
||||
game_query_mod!(ase, "ARK: Survival Evolved", Engine::new(346_110), 27015);
|
||||
game_query_mod!(
|
||||
asrd,
|
||||
|
|
@ -17,6 +29,7 @@ game_query_mod!(
|
|||
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,
|
||||
|
|
@ -100,16 +113,19 @@ game_query_mod!(
|
|||
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!(squad, "Squad", Engine::new(393_380), 27165);
|
||||
game_query_mod!(teamfortress2, "Team Fortress 2", Engine::new(440), 27015);
|
||||
game_query_mod!(
|
||||
tfc,
|
||||
|
|
@ -132,3 +148,4 @@ game_query_mod!(
|
|||
}
|
||||
);
|
||||
game_query_mod!(vrising, "V Rising", Engine::new(1_604_030), 27016);
|
||||
game_query_mod!(zps, "Zombie Panic: Source", Engine::new(17_500), 27015);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
//!
|
||||
//! ## Using a game definition
|
||||
//! ```
|
||||
//! use gamedig::games::{GAMES, query};
|
||||
//! 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
|
||||
|
|
@ -51,5 +51,10 @@ pub mod capture;
|
|||
pub use errors::*;
|
||||
#[cfg(feature = "games")]
|
||||
pub use games::*;
|
||||
#[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};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use std::collections::HashMap;
|
|||
pub fn has_password(server_vars: &mut HashMap<String, String>) -> GDResult<bool> {
|
||||
let password_value = server_vars
|
||||
.remove("password")
|
||||
.ok_or(GDErrorKind::PacketBad.context("Missing password (exists) field"))?
|
||||
.ok_or_else(|| GDErrorKind::PacketBad.context("Missing password (exists) field"))?
|
||||
.to_lowercase();
|
||||
|
||||
if let Ok(has) = password_value.parse::<bool>() {
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ fn get_server_values_impl(socket: &mut UdpSocket) -> GDResult<HashMap<String, St
|
|||
let key = splited[position].clone();
|
||||
let value = splited
|
||||
.get(position + 1)
|
||||
.map_or_else(String::new, |v| v.clone());
|
||||
.map_or_else(String::new, Clone::clone);
|
||||
|
||||
server_values.insert(key, value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -282,7 +282,7 @@ fn parse_players_and_teams(packets: Vec<Vec<u8>>) -> GDResult<(Vec<Player>, Vec<
|
|||
}
|
||||
|
||||
let entry_data = data.get_mut(offset).ok_or(PacketBad)?;
|
||||
entry_data.insert(field_name.to_string(), item);
|
||||
entry_data.insert((*field_name).to_string(), item);
|
||||
|
||||
offset += 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ fn get_server_values(bufferer: &mut Buffer<LittleEndian>) -> GDResult<HashMap<St
|
|||
|
||||
if let Some(k) = key {
|
||||
if let Some(v) = value {
|
||||
vars.insert(k.to_string(), v.to_string());
|
||||
vars.insert((*k).to_string(), (*v).to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -116,23 +116,23 @@ pub fn client_query<Client: QuakeClient>(
|
|||
Ok(Response {
|
||||
name: server_vars
|
||||
.remove("hostname")
|
||||
.or(server_vars.remove("sv_hostname"))
|
||||
.or_else(|| server_vars.remove("sv_hostname"))
|
||||
.ok_or(GDErrorKind::PacketBad)?,
|
||||
map: server_vars
|
||||
.remove("mapname")
|
||||
.or(server_vars.remove("map"))
|
||||
.or_else(|| server_vars.remove("map"))
|
||||
.ok_or(GDErrorKind::PacketBad)?,
|
||||
players_online: players.len() as u8,
|
||||
players_maximum: server_vars
|
||||
.remove("maxclients")
|
||||
.or(server_vars.remove("sv_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(server_vars.remove("*version")),
|
||||
.or_else(|| server_vars.remove("*version")),
|
||||
unused_entries: server_vars,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ pub enum ProprietaryProtocol {
|
|||
Minecraft(Option<minecraft::types::Server>),
|
||||
FFOW,
|
||||
JC2M,
|
||||
Savage2,
|
||||
Mindustry,
|
||||
}
|
||||
|
||||
/// Enumeration of all valid protocol types
|
||||
|
|
@ -41,6 +43,8 @@ pub enum GenericResponse<'a> {
|
|||
Valve(&'a valve::Response),
|
||||
Unreal2(&'a unreal2::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),
|
||||
|
|
@ -48,6 +52,8 @@ pub enum GenericResponse<'a> {
|
|||
FFOW(&'a crate::games::ffow::Response),
|
||||
#[cfg(feature = "games")]
|
||||
JC2M(&'a crate::games::jc2m::Response),
|
||||
#[cfg(feature = "games")]
|
||||
Savage2(&'a crate::games::savage2::Response),
|
||||
}
|
||||
|
||||
/// All player types
|
||||
|
|
@ -228,33 +234,33 @@ impl TimeoutSettings {
|
|||
|
||||
/// 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<TimeoutSettings>) -> usize {
|
||||
pub const fn get_retries_or_default(timeout_settings: &Option<Self>) -> usize {
|
||||
if let Some(timeout_settings) = timeout_settings {
|
||||
timeout_settings.get_retries()
|
||||
} else {
|
||||
TimeoutSettings::const_default().get_retries()
|
||||
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<TimeoutSettings>,
|
||||
timeout_settings: &Option<Self>,
|
||||
) -> (Option<Duration>, Option<Duration>) {
|
||||
if let Some(timeout_settings) = timeout_settings {
|
||||
(timeout_settings.get_read(), timeout_settings.get_write())
|
||||
} else {
|
||||
let default = TimeoutSettings::const_default();
|
||||
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<TimeoutSettings>) -> Option<Duration> {
|
||||
pub const fn get_connect_or_default(timeout_settings: &Option<Self>) -> Option<Duration> {
|
||||
if let Some(timeout_settings) = timeout_settings {
|
||||
timeout_settings.get_connect()
|
||||
} else {
|
||||
TimeoutSettings::const_default().get_connect()
|
||||
Self::const_default().get_connect()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -337,22 +343,22 @@ impl ExtraRequestSettings {
|
|||
}
|
||||
/// [Sets protocol
|
||||
/// version](ExtraRequestSettings#structfield.protocol_version)
|
||||
pub fn set_protocol_version(mut self, protocol_version: i32) -> Self {
|
||||
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 fn set_gather_players(mut self, gather_players: bool) -> Self {
|
||||
pub const fn set_gather_players(mut self, gather_players: bool) -> Self {
|
||||
self.gather_players = Some(gather_players);
|
||||
self
|
||||
}
|
||||
/// [Sets gather rules](ExtraRequestSettings#structfield.gather_rules)
|
||||
pub fn set_gather_rules(mut self, gather_rules: bool) -> Self {
|
||||
pub const fn set_gather_rules(mut self, gather_rules: bool) -> Self {
|
||||
self.gather_rules = Some(gather_rules);
|
||||
self
|
||||
}
|
||||
/// [Sets check app ID](ExtraRequestSettings#structfield.check_app_id)
|
||||
pub fn set_check_app_id(mut self, check_app_id: bool) -> Self {
|
||||
pub const fn set_check_app_id(mut self, check_app_id: bool) -> Self {
|
||||
self.check_app_id = Some(check_app_id);
|
||||
self
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,10 +33,10 @@ pub(crate) struct Unreal2Protocol {
|
|||
impl Unreal2Protocol {
|
||||
pub fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
|
||||
let socket = UdpSocket::new(address, &timeout_settings)?;
|
||||
let retry_count = timeout_settings
|
||||
.as_ref()
|
||||
.map(|t| t.get_retries())
|
||||
.unwrap_or_else(|| TimeoutSettings::default().get_retries());
|
||||
let retry_count = timeout_settings.as_ref().map_or_else(
|
||||
|| TimeoutSettings::default().get_retries(),
|
||||
TimeoutSettings::get_retries,
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
socket,
|
||||
|
|
@ -209,7 +209,7 @@ impl StringDecoder for Unreal2StringDecoder {
|
|||
let mut ucs2 = false;
|
||||
let mut length: usize = (*data
|
||||
.first()
|
||||
.ok_or(PacketBad.context("Tried to decode string without length"))?)
|
||||
.ok_or_else(|| PacketBad.context("Tried to decode string without length"))?)
|
||||
.into();
|
||||
|
||||
let mut start = 0;
|
||||
|
|
@ -225,7 +225,7 @@ impl StringDecoder for Unreal2StringDecoder {
|
|||
// 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 let Some(1) = data[start ..].first() {
|
||||
if data[start ..].first() == Some(&1) {
|
||||
start += 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ pub struct ServerInfo {
|
|||
|
||||
impl ServerInfo {
|
||||
pub fn parse<B: ByteOrder>(buffer: &mut Buffer<B>) -> GDResult<Self> {
|
||||
Ok(ServerInfo {
|
||||
Ok(Self {
|
||||
server_id: buffer.read()?,
|
||||
ip: buffer.read_string::<Unreal2StringDecoder>(None)?,
|
||||
game_port: buffer.read()?,
|
||||
|
|
@ -118,7 +118,7 @@ impl Players {
|
|||
/// Pre-allocate the vectors inside the players struct based on the provided
|
||||
/// capacity.
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Players {
|
||||
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),
|
||||
|
|
@ -234,7 +234,7 @@ impl GatheringSettings {
|
|||
}
|
||||
|
||||
impl Default for GatheringSettings {
|
||||
fn default() -> Self { GatheringSettings::default() }
|
||||
fn default() -> Self { Self::default() }
|
||||
}
|
||||
|
||||
impl From<ExtraRequestSettings> for GatheringSettings {
|
||||
|
|
|
|||
|
|
@ -127,10 +127,10 @@ static PACKET_SIZE: usize = 6144;
|
|||
impl ValveProtocol {
|
||||
pub fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
|
||||
let socket = UdpSocket::new(address, &timeout_settings)?;
|
||||
let retry_count = timeout_settings
|
||||
.as_ref()
|
||||
.map(|t| t.get_retries())
|
||||
.unwrap_or_else(|| TimeoutSettings::default().get_retries());
|
||||
let retry_count = timeout_settings.as_ref().map_or_else(
|
||||
|| TimeoutSettings::default().get_retries(),
|
||||
|t| t.get_retries(),
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
socket,
|
||||
|
|
@ -271,7 +271,7 @@ impl ValveProtocol {
|
|||
has_password,
|
||||
vac_secured,
|
||||
the_ship: None,
|
||||
game_version: "".to_string(), // a version field only for the mod
|
||||
game_version: String::new(), // a version field only for the mod
|
||||
extra_data: None,
|
||||
is_mod,
|
||||
mod_data,
|
||||
|
|
|
|||
|
|
@ -310,7 +310,7 @@ impl GatheringSettings {
|
|||
}
|
||||
|
||||
impl Default for GatheringSettings {
|
||||
fn default() -> Self { GatheringSettings::default() }
|
||||
fn default() -> Self { Self::default() }
|
||||
}
|
||||
|
||||
impl From<ExtraRequestSettings> for GatheringSettings {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ pub fn default_master_address() -> SocketAddr {
|
|||
fn construct_payload(region: Region, filters: &Option<SearchFilters>, last_ip: &str, last_port: u16) -> Vec<u8> {
|
||||
let filters_bytes: Vec<u8> = filters
|
||||
.as_ref()
|
||||
.map_or_else(|| vec![0x00], |f| f.to_bytes());
|
||||
.map_or_else(|| vec![0x00], SearchFilters::to_bytes);
|
||||
|
||||
let region_byte = &[region as u8];
|
||||
|
||||
|
|
|
|||
|
|
@ -75,11 +75,10 @@ pub struct TcpSocketImpl {
|
|||
|
||||
impl Socket for TcpSocketImpl {
|
||||
fn new(address: &SocketAddr, timeout_settings: &Option<TimeoutSettings>) -> GDResult<Self> {
|
||||
let socket = if let Some(timeout) = TimeoutSettings::get_connect_or_default(timeout_settings) {
|
||||
net::TcpStream::connect_timeout(address, timeout)
|
||||
} else {
|
||||
net::TcpStream::connect(address)
|
||||
};
|
||||
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))?,
|
||||
|
|
|
|||
11
crates/lib/tests/game_ids.rs
Normal file
11
crates/lib/tests/game_ids.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#![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());
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue