diff --git a/.github/badges/node.svg b/.github/badges/node.svg index 18d8a03..bff5a5b 100644 --- a/.github/badges/node.svg +++ b/.github/badges/node.svg @@ -1,5 +1,5 @@ - - Node game coverage: 12% + + Node game coverage: 13% @@ -13,8 +13,8 @@ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b373c2b..6d29a3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,24 @@ Who knows what the future holds... # 0.X.Y - DD/MM/YYYY ### Changes: -Nothing yet. +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. +- Added a valve protocol query example. + +Protocols: +- Added the unreal2 protocol and its associated games: Darkest Hour, Devastation, Killing Floor, Red Orchestra, Unreal Tournament 2003, Unreal Tournament 2004 (by @Douile). ### Breaking: -None, yaay! +Game: +- Changed identifications of the following games as they weren't properly expecting the naming rules: +- - Left 4 Dead: `left4dead` -> `l4d`. +- - 7 Days to Die: `7d2d` in definitions and `sd2d` in game declaration -> `sdtd`. +- - Quake 3 Arena: `quake3arena` -> `q3a`. + +Protocols: +- Valve: Removed `SteamApp` due to it not being really useful at all, replaced all instances with `Engine`. # 0.4.1 - 13/10/2023 ### Changes: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cfdd666..9151602 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,18 +52,23 @@ and 2017 would be `swb22017`). (`Day of Defeat` -> `dod`), then the new name should ignore rule #2 (`Day of Dragons` -> `dayofdragons`). 5. Roman numbering will be converted to arabic numbering (`XIV` -> `14`). -6. Unless numbers 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`). -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. +6. Unless numbers (years included) are at the end of a name, they will be considered +words. If a number is not in the first position, its entire numeric digits will be +used instead of the acronym of that number's digits (`Left 4 Dead` -> `l4d`). If the +number is in the first position the longhand (words: 5 -> five) representation of the +number will be used to create an acronym (`7 Days to Die` -> `sdtd`). Other examples: +`Team Fortress 2` -> `teamfortress2`, `Unreal Tournament 2003` -> +`unrealtournament2003`. +7. If a game supports multiple protocols, multiple entries will be done for said game +where the edition/protocol name (first disposable in this order) will be appended to +the base game id's: `` (where the protocol id will follow all +rules except #2) (Minecraft is mainly divided by 2 editions, Java and Bedrock +which will be `minecraftjava` and `minecraftbedrock` respectively, but it also has +legacy versions, which use another protocol, an example would be the one for `1.6`, +so the name would be `Legacy 1.6` which its id will be `legacy16`, resulting in the +entry of `minecraftlegacy16`). One more entry can be added by the base name of the +game, which queries in a group said supported protocols to make generic queries +easier and disposable. 8. If its actually about a mod that adds the ability for queries to be performed, process only the mod name. diff --git a/GAMES.md b/GAMES.md index 4a71917..5d7bad6 100644 --- a/GAMES.md +++ b/GAMES.md @@ -62,6 +62,15 @@ Beware of the `Notes` column, as it contains information about query port offset | Creativerse | CREATIVERSE | Valve | Query Port offset: 1. | | Garry's Mod | GARRYSMOD | Valve | | | Barotrauma | BAROTRAUMA | Valve | Query Port offset: 1. | +| Valheim | VALHEIM | Valve | Query Port offset: 1. Does not respond to the A2S rules. | +| The Front | THEFRONT | Valve | Responds with wrong values on `name` (gives out a SteamID instead of the server name) and `players_maximum` (always 200). | +| Conan Exiles | CONANEXILES | Valve | Does not respond to the players query. | +| Darkest Hour: Europe '44-'45 | DARKESTHOUR | Unreal2 | Query port offset: 1 | +| Devastation | DEVASTATION | Unreal2 | Query port offset: 1 | +| Killing Floor | KILLINGFLOOR | Unreal2 | Query port offset: 1 | +| Red Orchestra | REDORCHESTRA | Unreal2 | Query port offset: 1 | +| Unreal Tournament 2003 | UT2003 | Unreal2 | Query port offset: 1 | +| Unreal Tournament 2004 | UT2004 | Unreal2 | Query port offset: 1 | ## Planned to add support: _ diff --git a/PROTOCOLS.md b/PROTOCOLS.md index 0b838b3..6b1ef0c 100644 --- a/PROTOCOLS.md +++ b/PROTOCOLS.md @@ -7,6 +7,7 @@ A protocol is defined as proprietary if it is being used only for a single scope | Minecraft | Games | Yes | Java: [List Server Protocol](https://wiki.vg/Server_List_Ping)
Bedrock: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/minecraftbedrock.js) | | | GameSpy | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy1.js) Two: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy2.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy3.js) | These protocols are not really standardized, gamedig tries to get the most common fields amongst its supported games, if there are parsing problems, use the `query_vars` function. | | Quake | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake1.js) Two: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake2.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake3.js) | | +| 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. | ## Planned to add support: _ diff --git a/README.md b/README.md index 2f7601d..cda8c46 100644 --- a/README.md +++ b/README.md @@ -89,3 +89,5 @@ Curious about the history and what changed between versions? Everything is in th ## Contributing If you want to see your favorite game/service being supported here, open an issue, and I'll prioritize it (or do a pull request if you want to implement it yourself)! + +Before contributing please read [CONTRIBUTING](CONTRIBUTING.md). diff --git a/RESPONSES.md b/RESPONSES.md index aa1eeaa..14415fe 100644 --- a/RESPONSES.md +++ b/RESPONSES.md @@ -5,50 +5,53 @@ 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 | Proprietary: FFOW | Proprietary: TheShip | Proprietary: JC2MP | -|:---------------------|------------------|---------------------------|---------------|---------------------------|-----------------------|----------------------|----------------------------------|---------------------------|-------------------|--------------------------|--------------------| -| name | `Option` | `String` | `String` | `String` | | `String` | `String` | `String` | `String` | `String` | `String` | -| description | `Option` | | | | `String` | | | | `String` | | `String` | -| game_mode | `Option` | `String` | | `String` | | `Option` | `String` | | `String` | `String` | | -| game_version | `Option` | `String` | | `String` | `String` | | `String` | `String` | `String` | `String` | `String` | -| map | `Option` | `String` | `String` | `String` | | `Option` | `String` | `String` | `String` | `String` | | -| players_maxmimum | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u8` | `u8` | `u32` | -| players_online | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u8` | `u8` | `u32` | -| players_bots | `Option` | | | | | | `u8` | | | `u8` | | -| has_password | `Option` | `bool` | `bool` | `bool` | | | `bool` | | `bool` | `bool` | `bool` | -| players_minimum | | `Option` | `Option` | `Option` | | | | | | | | -| players | | `Vec` | `Vec` | `Vec` | `Option>` | | `Option>` | `Vec

` | | `Vec` | `Vec` | -| tournament | | `bool` | | `bool` | | | | | | | | -| unused_entries | | `Hashmap` | | `HashMap` | | | | `HashMap` | | | | -| teams | | | `Vec` | `Vec` | | | | | | | | -| protocol_version | | | | | `i32` | `String` | `u8` | | `u8` | `u8` | | -| server_type | | | | | `Server` | `Server` | `Server` | | | `Server` | | -| rules | | | | | | | `Option>` | | | `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` | | | | | | -| 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 | | | | | | | | | | `Option` | | -| steam_id | | | | | | | | | | `Option` | | -| tv_port | | | | | | | | | | `Option` | | -| tv_name | | | | | | | | | | `Option` | | -| keywords | | | | | | | | | | `Option` | | -| mode | | | | | | | | | | `u8` | | -| witnesses | | | | | | | | | | `u8` | | -| duration | | | | | | | | | | `u8` | | +| 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` | +| description | `Option` | | | | `String` | | | | | `String` | | `String` | +| game_mode | `Option` | `String` | | `String` | | `Option` | `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` | | +| 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` | | | | | | `u8` | | | | `u8` | | +| has_password | `Option` | `bool` | `bool` | `bool` | | | `bool` | | | `bool` | `bool` | `bool` | +| players_minimum | | `Option` | `Option` | `Option` | | | | | | | | | +| 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` | | +| 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` | | | | diff --git a/crates/lib/examples/generic.rs b/crates/lib/examples/generic.rs index 4935bee..0365dbf 100644 --- a/crates/lib/examples/generic.rs +++ b/crates/lib/examples/generic.rs @@ -2,23 +2,21 @@ use gamedig::{ protocols::types::{CommonResponse, ExtraRequestSettings, TimeoutSettings}, query_with_timeout_and_extra_settings, GDResult, + Game, GAMES, }; use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; /// Make a query given the name of a game +/// The `game` argument is taken from the [GAMES](gamedig::GAMES) map. fn generic_query( - game_name: &str, + game: &Game, addr: &IpAddr, port: Option, timeout_settings: Option, extra_settings: Option, ) -> GDResult> { - let game = GAMES - .get(game_name) - .expect("Game doesn't exist, run without arguments to see a list of games"); - println!("Querying {:#?} with game {:#?}.", addr, game); let response = query_with_timeout_and_extra_settings(game, addr, port, timeout_settings, extra_settings)?; @@ -51,14 +49,18 @@ fn main() { ) .unwrap(); - let extra_settings = ExtraRequestSettings::default() + let game = GAMES + .get(&game_name) + .expect("Game doesn't exist, run without arguments to see a list of games"); + + let extra_settings = game + .request_settings + .clone() .set_hostname(hostname.to_string()) - .set_gather_rules(true) - .set_gather_players(true) .set_check_app_id(false); generic_query( - &game_name, + game, &addr.ip(), port, Some(timeout_settings), @@ -67,8 +69,7 @@ fn main() { .unwrap(); } else { // Without arguments print a list of games - - for (name, game) in gamedig::games::GAMES.entries() { + for (name, game) in GAMES.entries() { println!("{}\t{}", name, game.name); } } @@ -95,7 +96,12 @@ mod test { ) .unwrap(), ); - assert!(generic_query(game_name, &ADDR, None, timeout_settings, None).is_err()); + + let game = GAMES + .get(game_name) + .expect("Game doesn't exist, run without arguments to see a list of games"); + + assert!(generic_query(game, &ADDR, None, timeout_settings, None).is_err()); } #[test] @@ -108,7 +114,7 @@ mod test { fn teamfortress2() { test_game("teamfortress2"); } #[test] - fn quake() { test_game("quake3"); } + fn quake2() { test_game("quake2"); } #[test] fn all_games() { diff --git a/crates/lib/examples/valve_protocol_query.rs b/crates/lib/examples/valve_protocol_query.rs new file mode 100644 index 0000000..5e9de3f --- /dev/null +++ b/crates/lib/examples/valve_protocol_query.rs @@ -0,0 +1,28 @@ +use gamedig::protocols::types::TimeoutSettings; +use gamedig::protocols::valve; +use gamedig::protocols::valve::{Engine, GatheringSettings}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::time::Duration; + +fn main() { + let address = &SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 27015); + let engine = Engine::Source(None); // We don't specify a steam app id, let the query try to find it. + let gather_settings = GatheringSettings { + players: true, // We want to query for players + rules: false, // We don't want to query for rules + check_app_id: false, // Loosen up the query a bit by not checking app id + }; + + let read_timeout = Duration::from_secs(2); + let write_timeout = Duration::from_secs(3); + let retries = 1; // does another request if the first one fails. + let timeout_settings = TimeoutSettings::new(Some(read_timeout), Some(write_timeout), retries).unwrap(); + + let response = valve::query( + address, + engine, + Some(gather_settings), + Some(timeout_settings), + ); + println!("{response:#?}"); +} diff --git a/crates/lib/src/games/battalion1944.rs b/crates/lib/src/games/battalion1944.rs index dc49d9e..b6d4851 100644 --- a/crates/lib/src/games/battalion1944.rs +++ b/crates/lib/src/games/battalion1944.rs @@ -1,5 +1,6 @@ +use crate::protocols::valve::Engine; use crate::{ - protocols::valve::{self, game, SteamApp}, + protocols::valve::{self, game}, GDErrorKind::TypeParse, GDResult, }; @@ -8,7 +9,7 @@ use std::net::{IpAddr, SocketAddr}; pub fn query(address: &IpAddr, port: Option) -> GDResult { let mut valve_response = valve::query( &SocketAddr::new(*address, port.unwrap_or(7780)), - SteamApp::BATTALION1944.as_engine(), + Engine::new(489_940), None, None, )?; diff --git a/crates/lib/src/games/definitions.rs b/crates/lib/src/games/definitions.rs index 4b3a7f9..b620ea0 100644 --- a/crates/lib/src/games/definitions.rs +++ b/crates/lib/src/games/definitions.rs @@ -4,20 +4,31 @@ use crate::protocols::{ gamespy::GameSpyVersion, minecraft::{LegacyGroup, Server}, quake::QuakeVersion, - valve::SteamApp, + valve::Engine, Protocol, }; use crate::Game; use crate::protocols::types::ProprietaryProtocol; +use crate::protocols::valve::GatheringSettings; use phf::{phf_map, Map}; macro_rules! game { ($name: literal, $default_port: literal, $protocol: expr) => { + game!( + $name, + $default_port, + $protocol, + GatheringSettings::default().into_extra() + ) + }; + + ($name: literal, $default_port: literal, $protocol: expr, $extra_request_settings: expr) => { Game { name: $name, default_port: $default_port, protocol: $protocol, + request_settings: $extra_request_settings, } }; } @@ -33,60 +44,77 @@ pub static GAMES: Map<&'static str, Game> = phf_map! { "minecraftlegacy16" => game!("Minecraft (legacy v1.6)", 25565, Protocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_6)))), "minecraftlegacy15" => game!("Minecraft (legacy v1.4-1.5)", 25565, Protocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_5)))), "minecraftlegacy13" => game!("Minecraft (legacy vB1.8-1.3)", 25565, Protocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_3)))), - "alienswarm" => game!("Alien Swarm", 27015, Protocol::Valve(SteamApp::ALIENSWARM)), - "aoc" => game!("Age of Chivalry", 27015, Protocol::Valve(SteamApp::AOC)), - "a2oa" => game!("ARMA 2: Operation Arrowhead", 2304, Protocol::Valve(SteamApp::A2OA)), - "ase" => game!("ARK: Survival Evolved", 27015, Protocol::Valve(SteamApp::ASE)), - "asrd" => game!("Alien Swarm: Reactive Drop", 2304, Protocol::Valve(SteamApp::ASRD)), - "avorion" => game!("Avorion", 27020, Protocol::Valve(SteamApp::AVORION)), - "barotrauma" => game!("Barotrauma", 27016, Protocol::Valve(SteamApp::BAROTRAUMA)), - "battalion1944" => game!("Battalion 1944", 7780, Protocol::Valve(SteamApp::BATTALION1944)), - "brainbread2" => game!("BrainBread 2", 27015, Protocol::Valve(SteamApp::BRAINBREAD2)), + "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))), + "avorion" => game!("Avorion", 27020, Protocol::Valve(Engine::new(445_220))), + "barotrauma" => game!("Barotrauma", 27016, Protocol::Valve(Engine::new(602_960))), + "battalion1944" => game!("Battalion 1944", 7780, Protocol::Valve(Engine::new(489_940))), + "brainbread2" => game!("BrainBread 2", 27015, Protocol::Valve(Engine::new(346_330))), "battlefield1942" => game!("Battlefield 1942", 23000, Protocol::Gamespy(GameSpyVersion::One)), - "blackmesa" => game!("Black Mesa", 27015, Protocol::Valve(SteamApp::BLACKMESA)), - "ballisticoverkill" => game!("Ballistic Overkill", 27016, Protocol::Valve(SteamApp::BALLISTICOVERKILL)), - "codenamecure" => game!("Codename CURE", 27015, Protocol::Valve(SteamApp::CODENAMECURE)), - "colonysurvival" => game!("Colony Survival", 27004, Protocol::Valve(SteamApp::COLONYSURVIVAL)), - "counterstrike" => game!("Counter-Strike", 27015, Protocol::Valve(SteamApp::COUNTERSTRIKE)), - "cscz" => game!("Counter Strike: Condition Zero", 27015, Protocol::Valve(SteamApp::CSCZ)), - "csgo" => game!("Counter-Strike: Global Offensive", 27015, Protocol::Valve(SteamApp::CSGO)), - "css" => game!("Counter-Strike: Source", 27015, Protocol::Valve(SteamApp::CSS)), - "creativerse" => game!("Creativerse", 26901, Protocol::Valve(SteamApp::CREATIVERSE)), + "blackmesa" => game!("Black Mesa", 27015, Protocol::Valve(Engine::new(362_890))), + "ballisticoverkill" => game!("Ballistic Overkill", 27016, Protocol::Valve(Engine::new(296_300))), + "codenamecure" => game!("Codename CURE", 27015, Protocol::Valve(Engine::new(355_180))), + "colonysurvival" => game!("Colony Survival", 27004, Protocol::Valve(Engine::new(366_090))), + "conanexiles" => game!("Conan Exiles", 27015, Protocol::Valve(Engine::new(440_900)), GatheringSettings { + players: false, + rules: true, + check_app_id: true, + }.into_extra()), + "counterstrike" => game!("Counter-Strike", 27015, Protocol::Valve(Engine::new_gold_src(false))), + "cscz" => game!("Counter Strike: Condition Zero", 27015, Protocol::Valve(Engine::new_gold_src(false))), + "csgo" => game!("Counter-Strike: Global Offensive", 27015, Protocol::Valve(Engine::new(730))), + "css" => game!("Counter-Strike: Source", 27015, Protocol::Valve(Engine::new(240))), + "creativerse" => game!("Creativerse", 26901, Protocol::Valve(Engine::new(280_790))), "crysiswars" => game!("Crysis Wars", 64100, Protocol::Gamespy(GameSpyVersion::Three)), - "dod" => game!("Day of Defeat", 27015, Protocol::Valve(SteamApp::DOD)), - "dods" => game!("Day of Defeat: Source", 27015, Protocol::Valve(SteamApp::DODS)), - "doi" => game!("Day of Infamy", 27015, Protocol::Valve(SteamApp::DOI)), - "dst" => game!("Don't Starve Together", 27016, Protocol::Valve(SteamApp::DST)), + "dod" => game!("Day of Defeat", 27015, Protocol::Valve(Engine::new_gold_src(false))), + "dods" => game!("Day of Defeat: Source", 27015, Protocol::Valve(Engine::new(300))), + "doi" => game!("Day of Infamy", 27015, Protocol::Valve(Engine::new(447_820))), + "dst" => game!("Don't Starve Together", 27016, Protocol::Valve(Engine::new(322_320))), "ffow" => game!("Frontlines: Fuel of War", 5478, Protocol::PROPRIETARY(ProprietaryProtocol::FFOW)), - "garrysmod" => game!("Garry's Mod", 27016, Protocol::Valve(SteamApp::GARRYSMOD)), - "hl2d" => game!("Half-Life 2 Deathmatch", 27015, Protocol::Valve(SteamApp::HL2D)), + "garrysmod" => game!("Garry's Mod", 27016, Protocol::Valve(Engine::new(4000))), + "hl2d" => game!("Half-Life 2 Deathmatch", 27015, Protocol::Valve(Engine::new(320))), "hce" => game!("Halo: Combat Evolved", 2302, Protocol::Gamespy(GameSpyVersion::Two)), - "hlds" => game!("Half-Life Deathmatch: Source", 27015, Protocol::Valve(SteamApp::HLDS)), - "hll" => game!("Hell Let Loose", 26420, Protocol::Valve(SteamApp::HLL)), - "insurgency" => game!("Insurgency", 27015, Protocol::Valve(SteamApp::INSURGENCY)), - "imic" => game!("Insurgency: Modern Infantry Combat", 27015, Protocol::Valve(SteamApp::IMIC)), - "insurgencysandstorm" => game!("Insurgency: Sandstorm", 27131, Protocol::Valve(SteamApp::INSURGENCYSANDSTORM)), - "left4dead" => game!("Left 4 Dead", 27015, Protocol::Valve(SteamApp::LEFT4DEAD)), - "left4dead2" => game!("Left 4 Dead 2", 27015, Protocol::Valve(SteamApp::LEFT4DEAD2)), - "ohd" => game!("Operation: Harsh Doorstop", 27005, Protocol::Valve(SteamApp::OHD)), - "onset" => game!("Onset", 7776, Protocol::Valve(SteamApp::ONSET)), - "projectzomboid" => game!("Project Zomboid", 16261, Protocol::Valve(SteamApp::PROJECTZOMBOID)), + "hlds" => game!("Half-Life Deathmatch: Source", 27015, Protocol::Valve(Engine::new(360))), + "hll" => game!("Hell Let Loose", 26420, Protocol::Valve(Engine::new(686_810))), + "insurgency" => game!("Insurgency", 27015, Protocol::Valve(Engine::new(222_880))), + "imic" => game!("Insurgency: Modern Infantry Combat", 27015, Protocol::Valve(Engine::new(17700))), + "insurgencysandstorm" => game!("Insurgency: Sandstorm", 27131, Protocol::Valve(Engine::new(581_320))), + "l4d" => game!("Left 4 Dead", 27015, Protocol::Valve(Engine::new(500))), + "l4d2" => game!("Left 4 Dead 2", 27015, Protocol::Valve(Engine::new(550))), + "ohd" => game!("Operation: Harsh Doorstop", 27005, Protocol::Valve(Engine::new_with_dedicated(736_590, 950_900))), + "onset" => game!("Onset", 7776, Protocol::Valve(Engine::new(1_105_810))), + "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)), - "quake3" => game!("Quake 3: Arena", 27960, Protocol::Quake(QuakeVersion::Three)), - "ror2" => game!("Risk of Rain 2", 27016, Protocol::Valve(SteamApp::ROR2)), - "rust" => game!("Rust", 27015, Protocol::Valve(SteamApp::RUST)), - "sco" => game!("Sven Co-op", 27015, Protocol::Valve(SteamApp::SCO)), - "7d2d" => game!("7 Days To Die", 26900, Protocol::Valve(SteamApp::SD2D)), + "q3a" => game!("Quake 3 Arena", 27960, Protocol::Quake(QuakeVersion::Three)), + "ror2" => game!("Risk of Rain 2", 27016, Protocol::Valve(Engine::new(632_360))), + "rust" => game!("Rust", 27015, Protocol::Valve(Engine::new(252_490))), + "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)), - "theforest" => game!("The Forest", 27016, Protocol::Valve(SteamApp::THEFOREST)), - "teamfortress2" => game!("Team Fortress 2", 27015, Protocol::Valve(SteamApp::TEAMFORTRESS2)), - "tfc" => game!("Team Fortress Classic", 27015, Protocol::Valve(SteamApp::TFC)), + "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))), + "tfc" => game!("Team Fortress Classic", 27015, Protocol::Valve(Engine::new_gold_src(false))), "theship" => game!("The Ship", 27015, Protocol::PROPRIETARY(ProprietaryProtocol::TheShip)), - "unturned" => game!("Unturned", 27015, Protocol::Valve(SteamApp::UNTURNED)), + "unturned" => game!("Unturned", 27015, Protocol::Valve(Engine::new(304_930))), "unrealtournament" => game!("Unreal Tournament", 7778, Protocol::Gamespy(GameSpyVersion::One)), - "vrising" => game!("V Rising", 27016, Protocol::Valve(SteamApp::VRISING)), + "valheim" => game!("Valheim", 2457, Protocol::Valve(Engine::new(892_970)), GatheringSettings { + players: true, + rules: false, + check_app_id: true, + }.into_extra()), + "vrising" => game!("V Rising", 27016, Protocol::Valve(Engine::new(1_604_030))), "jc2m" => game!("Just Cause 2: Multiplayer", 7777, Protocol::PROPRIETARY(ProprietaryProtocol::JC2M)), "warsow" => game!("Warsow", 44400, Protocol::Quake(QuakeVersion::Three)), + "darkesthour" => 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), }; diff --git a/crates/lib/src/games/mod.rs b/crates/lib/src/games/mod.rs index 10129f7..7749d5b 100644 --- a/crates/lib/src/games/mod.rs +++ b/crates/lib/src/games/mod.rs @@ -5,10 +5,12 @@ use serde::{Deserialize, Serialize}; pub mod gamespy; pub mod quake; +pub mod unreal2; pub mod valve; pub use gamespy::*; pub use quake::*; +pub use unreal2::*; pub use valve::*; /// Battalion 1944 @@ -39,6 +41,8 @@ pub struct Game { pub default_port: u16, /// The protocol the game's query uses pub protocol: Protocol, + /// Request settings. + pub request_settings: ExtraRequestSettings, } #[cfg(feature = "game_defs")] @@ -74,11 +78,13 @@ pub fn query_with_timeout_and_extra_settings( ) -> GDResult> { let socket_addr = SocketAddr::new(*address, port.unwrap_or(game.default_port)); Ok(match &game.protocol { - Protocol::Valve(steam_app) => { + Protocol::Valve(engine) => { protocols::valve::query( &socket_addr, - steam_app.as_engine(), - extra_settings.map(ExtraRequestSettings::into), + *engine, + extra_settings + .or(Option::from(game.request_settings.clone())) + .map(ExtraRequestSettings::into), timeout_settings, ) .map(Box::new)? @@ -125,6 +131,16 @@ pub fn query_with_timeout_and_extra_settings( 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 => { diff --git a/crates/lib/src/games/quake.rs b/crates/lib/src/games/quake.rs index 948e18c..dff9d79 100644 --- a/crates/lib/src/games/quake.rs +++ b/crates/lib/src/games/quake.rs @@ -4,6 +4,6 @@ use crate::protocols::quake::game_query_mod; game_query_mod!(quake1, "Quake 1", one, 27500); game_query_mod!(quake2, "Quake 2", two, 27910); -game_query_mod!(quake3, "Quake 3: Arena", three, 27960); +game_query_mod!(q3a, "Quake 3 Arena", three, 27960); game_query_mod!(sof2, "Soldier of Fortune 2", three, 20100); game_query_mod!(warsow, "Warsow", three, 44400); diff --git a/crates/lib/src/games/theship.rs b/crates/lib/src/games/theship.rs index 47de24f..5084200 100644 --- a/crates/lib/src/games/theship.rs +++ b/crates/lib/src/games/theship.rs @@ -1,7 +1,7 @@ use crate::{ protocols::{ types::{CommonPlayer, CommonResponse, GenericPlayer, TimeoutSettings}, - valve::{self, get_optional_extracted_data, Server, ServerPlayer, SteamApp}, + valve::{self, get_optional_extracted_data, Server, ServerPlayer}, GenericResponse, }, GDErrorKind::PacketBad, @@ -11,6 +11,7 @@ use std::net::{IpAddr, SocketAddr}; use std::collections::HashMap; +use crate::protocols::valve::Engine; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -136,7 +137,7 @@ pub fn query_with_timeout( ) -> GDResult { let valve_response = valve::query( &SocketAddr::new(*address, port.unwrap_or(27015)), - SteamApp::THESHIP.as_engine(), + Engine::new(2400), None, timeout_settings, )?; diff --git a/crates/lib/src/games/unreal2.rs b/crates/lib/src/games/unreal2.rs new file mode 100644 index 0000000..1ce4585 --- /dev/null +++ b/crates/lib/src/games/unreal2.rs @@ -0,0 +1,10 @@ +//! Unreal2 game query modules + +use crate::protocols::unreal2::game_query_mod; + +game_query_mod!(darkesthour, "Darkest Hour: Europe '44-'45 (2008)", 7758); +game_query_mod!(devastation, "Devastation (2003)", 7778); +game_query_mod!(killingfloor, "Killing Floor", 7708); +game_query_mod!(redorchestra, "Red Orchestra", 7759); +game_query_mod!(ut2003, "Unreal Tournament 2003", 7758); +game_query_mod!(ut2004, "Unreal Tournament 2004", 7778); diff --git a/crates/lib/src/games/valve.rs b/crates/lib/src/games/valve.rs index e55594b..acd612c 100644 --- a/crates/lib/src/games/valve.rs +++ b/crates/lib/src/games/valve.rs @@ -2,55 +2,133 @@ use crate::protocols::valve::game_query_mod; -game_query_mod!(a2oa, "ARMA 2: Operation Arrowhead", A2OA, 2304); -game_query_mod!(alienswarm, "Alien Swarm", ALIENSWARM, 27015); -game_query_mod!(aoc, "Age of Chivalry", AOC, 27015); -game_query_mod!(ase, "ARK: Survival Evolved", ASE, 27015); -game_query_mod!(asrd, "Alien Swarm: Reactive Drop", ASRD, 2304); -game_query_mod!(avorion, "Avorion", AVORION, 27020); +game_query_mod!( + a2oa, + "ARMA 2: Operation Arrowhead", + Engine::new(33930), + 2304 +); +game_query_mod!(alienswarm, "Alien Swarm", Engine::new(630), 27015); +game_query_mod!(aoc, "Age of Chivalry", Engine::new(17510), 27015); +game_query_mod!(ase, "ARK: Survival Evolved", Engine::new(346_110), 27015); +game_query_mod!( + asrd, + "Alien Swarm: Reactive Drop", + Engine::new(563_560), + 2304 +); +game_query_mod!(avorion, "Avorion", Engine::new(445_220), 27020); game_query_mod!( ballisticoverkill, "Ballistic Overkill", - BALLISTICOVERKILL, + Engine::new(296_300), 27016 ); -game_query_mod!(barotrauma, "Barotrauma", BAROTRAUMA, 27016); -game_query_mod!(blackmesa, "Black Mesa", BLACKMESA, 27015); -game_query_mod!(brainbread2, "BrainBread 2", BRAINBREAD2, 27015); -game_query_mod!(codenamecure, "Codename CURE", CODENAMECURE, 27015); -game_query_mod!(colonysurvival, "Colony Survival", COLONYSURVIVAL, 27004); -game_query_mod!(counterstrike, "Counter-Strike", COUNTERSTRIKE, 27015); -game_query_mod!(creativerse, "Creativerse", CREATIVERSE, 26901); -game_query_mod!(cscz, "Counter Strike: Condition Zero", CSCZ, 27015); -game_query_mod!(csgo, "Counter-Strike: Global Offensive", CSGO, 27015); -game_query_mod!(css, "Counter-Strike: Source", CSS, 27015); -game_query_mod!(dod, "Day of Defeat", DOD, 27015); -game_query_mod!(dods, "Day of Defeat: Source", DODS, 27015); -game_query_mod!(doi, "Day of Infamy", DOI, 27015); -game_query_mod!(dst, "Don't Starve Together", DST, 27016); -game_query_mod!(garrysmod, "Garry's Mod", GARRYSMOD, 27016); -game_query_mod!(hl2d, "Half-Life 2 Deathmatch", HL2D, 27015); -game_query_mod!(hlds, "Half-Life Deathmatch: Source", HLDS, 27015); -game_query_mod!(hll, "Hell Let Loose", HLL, 26420); -game_query_mod!(imic, "Insurgency: Modern Infantry Combat", IMIC, 27015); -game_query_mod!(insurgency, "Insurgency", INSURGENCY, 27015); +game_query_mod!(barotrauma, "Barotrauma", Engine::new(602_960), 27016); +game_query_mod!(blackmesa, "Black Mesa", Engine::new(362_890), 27015); +game_query_mod!(brainbread2, "BrainBread 2", Engine::new(346_330), 27015); +game_query_mod!(codenamecure, "Codename CURE", Engine::new(355_180), 27015); +game_query_mod!( + colonysurvival, + "Colony Survival", + Engine::new(366_090), + 27004 +); +game_query_mod!( + conanexiles, + "Conan Exiles", + Engine::new(440_900), + 27015, + GatheringSettings { + players: false, + rules: true, + check_app_id: true, + } +); +game_query_mod!( + counterstrike, + "Counter-Strike", + Engine::new_gold_src(false), + 27015 +); +game_query_mod!(creativerse, "Creativerse", Engine::new(280_790), 26901); +game_query_mod!( + cscz, + "Counter Strike: Condition Zero", + Engine::new_gold_src(false), + 27015 +); +game_query_mod!( + csgo, + "Counter-Strike: Global Offensive", + Engine::new(730), + 27015 +); +game_query_mod!(css, "Counter-Strike: Source", Engine::new(240), 27015); +game_query_mod!(dod, "Day of Defeat", Engine::new_gold_src(false), 27015); +game_query_mod!(dods, "Day of Defeat: Source", Engine::new(300), 27015); +game_query_mod!(doi, "Day of Infamy", Engine::new(447_820), 27015); +game_query_mod!(dst, "Don't Starve Together", Engine::new(322_320), 27016); +game_query_mod!(garrysmod, "Garry's Mod", Engine::new(4000), 27016); +game_query_mod!(hl2d, "Half-Life 2 Deathmatch", Engine::new(320), 27015); +game_query_mod!( + hlds, + "Half-Life Deathmatch: Source", + Engine::new(360), + 27015 +); +game_query_mod!(hll, "Hell Let Loose", Engine::new(686_810), 26420); +game_query_mod!( + imic, + "Insurgency: Modern Infantry Combat", + Engine::new(17700), + 27015 +); +game_query_mod!(insurgency, "Insurgency", Engine::new(222_880), 27015); game_query_mod!( insurgencysandstorm, "Insurgency: Sandstorm", - INSURGENCYSANDSTORM, + Engine::new(581_320), 27131 ); -game_query_mod!(left4dead, "Left 4 Dead", LEFT4DEAD, 27015); -game_query_mod!(left4dead2, "Left 4 Dead 2", LEFT4DEAD2, 27015); -game_query_mod!(ohd, "Operation: Harsh Doorstop", OHD, 27005); -game_query_mod!(onset, "Onset", ONSET, 7776); -game_query_mod!(projectzomboid, "Project Zomboid", PROJECTZOMBOID, 16261); -game_query_mod!(ror2, "Risk of Rain 2", ROR2, 27016); -game_query_mod!(rust, "Rust", RUST, 27015); -game_query_mod!(sco, "Sven Co-op", SCO, 27015); -game_query_mod!(sd2d, "7 Days To Die", SD2D, 26900); -game_query_mod!(teamfortress2, "Team Fortress 2", TEAMFORTRESS2, 27015); -game_query_mod!(tfc, "Team Fortress Classic", TFC, 27015); -game_query_mod!(theforest, "The Forest", THEFOREST, 27016); -game_query_mod!(unturned, "Unturned", UNTURNED, 27015); -game_query_mod!(vrising, "V Rising", VRISING, 27016); +game_query_mod!(l4d, "Left 4 Dead", Engine::new(500), 27015); +game_query_mod!(l4d2, "Left 4 Dead 2", Engine::new(550), 27015); +game_query_mod!( + ohd, + "Operation: Harsh Doorstop", + Engine::new_with_dedicated(736_590, 950_900), + 27005 +); +game_query_mod!(onset, "Onset", Engine::new(1_105_810), 7776); +game_query_mod!( + projectzomboid, + "Project Zomboid", + Engine::new(108_600), + 16261 +); +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!(teamfortress2, "Team Fortress 2", Engine::new(440), 27015); +game_query_mod!( + tfc, + "Team Fortress Classic", + Engine::new_gold_src(false), + 27015 +); +game_query_mod!(theforest, "The Forest", Engine::new(556_450), 27016); +game_query_mod!(thefront, "The Front", Engine::new(2_285_150), 27015); +game_query_mod!(unturned, "Unturned", Engine::new(304_930), 27015); +game_query_mod!( + valheim, + "Valheim", + Engine::new(892_970), + 2457, + GatheringSettings { + players: true, + rules: false, + check_app_id: true, + } +); +game_query_mod!(vrising, "V Rising", Engine::new(1_604_030), 27016); diff --git a/crates/lib/src/protocols/gamespy/mod.rs b/crates/lib/src/protocols/gamespy/mod.rs index 41d51f8..ef50096 100644 --- a/crates/lib/src/protocols/gamespy/mod.rs +++ b/crates/lib/src/protocols/gamespy/mod.rs @@ -39,6 +39,7 @@ pub enum VersionedPlayer<'a> { /// * `pretty_name` - The full name of the game, will be used as the /// documentation for the created module. /// * `gamespy_ver`, `default_port` - Passed through to [game_query_fn]. +#[cfg(feature = "games")] macro_rules! game_query_mod { ($mod_name: ident, $pretty_name: expr, $gamespy_ver: ident, $default_port: literal) => { #[doc = $pretty_name] @@ -48,6 +49,7 @@ macro_rules! game_query_mod { }; } +#[cfg(feature = "games")] pub(crate) use game_query_mod; // Allow generating doc comments: @@ -62,6 +64,7 @@ pub(crate) use game_query_mod; /// use crate::protocols::gamespy::game_query_fn; /// game_query_fn!(one, 7778); /// ``` +#[cfg(feature = "games")] macro_rules! game_query_fn { ($gamespy_ver: ident, $default_port: literal) => { crate::protocols::gamespy::game_query_fn! {@gen $gamespy_ver, $default_port, concat!( @@ -83,4 +86,5 @@ macro_rules! game_query_fn { }; } +#[cfg(feature = "games")] pub(crate) use game_query_fn; diff --git a/crates/lib/src/protocols/minecraft/mod.rs b/crates/lib/src/protocols/minecraft/mod.rs index da17668..3d98ab9 100644 --- a/crates/lib/src/protocols/minecraft/mod.rs +++ b/crates/lib/src/protocols/minecraft/mod.rs @@ -3,6 +3,5 @@ pub mod protocol; /// All types used by the implementation. pub mod types; -pub use protocol::*; pub use protocol::*; pub use types::*; diff --git a/crates/lib/src/protocols/minecraft/protocol/mod.rs b/crates/lib/src/protocols/minecraft/protocol/mod.rs index 885f990..79fa6ab 100644 --- a/crates/lib/src/protocols/minecraft/protocol/mod.rs +++ b/crates/lib/src/protocols/minecraft/protocol/mod.rs @@ -31,11 +31,11 @@ pub fn query( timeout_settings: Option, request_settings: Option, ) -> GDResult { - if let Ok(response) = query_java(address, timeout_settings.clone(), request_settings) { + if let Ok(response) = query_java(address, timeout_settings, request_settings) { return Ok(response); } - if let Ok(response) = query_bedrock(address, timeout_settings.clone()) { + if let Ok(response) = query_bedrock(address, timeout_settings) { return Ok(JavaResponse::from_bedrock_response(response)); } @@ -57,11 +57,11 @@ pub fn query_java( /// Query a (Java) Legacy Server (1.6 -> 1.4 -> Beta 1.8). pub fn query_legacy(address: &SocketAddr, timeout_settings: Option) -> GDResult { - if let Ok(response) = query_legacy_specific(LegacyGroup::V1_6, address, timeout_settings.clone()) { + if let Ok(response) = query_legacy_specific(LegacyGroup::V1_6, address, timeout_settings) { return Ok(response); } - if let Ok(response) = query_legacy_specific(LegacyGroup::V1_5, address, timeout_settings.clone()) { + if let Ok(response) = query_legacy_specific(LegacyGroup::V1_5, address, timeout_settings) { return Ok(response); } diff --git a/crates/lib/src/protocols/mod.rs b/crates/lib/src/protocols/mod.rs index 1b5128f..697d7e8 100644 --- a/crates/lib/src/protocols/mod.rs +++ b/crates/lib/src/protocols/mod.rs @@ -12,6 +12,8 @@ pub mod minecraft; pub mod quake; /// General types that are used by all protocols. pub mod types; +/// Reference: [node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/unreal2.js) +pub mod unreal2; /// Reference: [Server Query](https://developer.valvesoftware.com/wiki/Server_queries) pub mod valve; diff --git a/crates/lib/src/protocols/quake/mod.rs b/crates/lib/src/protocols/quake/mod.rs index dd3c213..2368738 100644 --- a/crates/lib/src/protocols/quake/mod.rs +++ b/crates/lib/src/protocols/quake/mod.rs @@ -26,6 +26,7 @@ pub enum QuakeVersion { /// * `pretty_name` - The full name of the game, will be used as the /// documentation for the created module. /// * `quake_ver`, `default_port` - Passed through to [game_query_fn]. +#[cfg(feature = "games")] macro_rules! game_query_mod { ($mod_name: ident, $pretty_name: expr, $quake_ver: ident, $default_port: literal) => { #[doc = $pretty_name] @@ -35,6 +36,7 @@ macro_rules! game_query_mod { }; } +#[cfg(feature = "games")] pub(crate) use game_query_mod; // Allow generating doc comments: @@ -49,6 +51,7 @@ pub(crate) use game_query_mod; /// use crate::protocols::quake::game_query_fn; /// game_query_fn!(one, 27500); /// ``` +#[cfg(feature = "games")] macro_rules! game_query_fn { ($quake_ver: ident, $default_port: literal) => { use crate::protocols::quake::$quake_ver::Player; @@ -71,4 +74,5 @@ macro_rules! game_query_fn { }; } +#[cfg(feature = "games")] pub(crate) use game_query_fn; diff --git a/crates/lib/src/protocols/types.rs b/crates/lib/src/protocols/types.rs index 83a9cf9..67e7d13 100644 --- a/crates/lib/src/protocols/types.rs +++ b/crates/lib/src/protocols/types.rs @@ -1,4 +1,4 @@ -use crate::protocols::{gamespy, minecraft, quake, valve}; +use crate::protocols::{gamespy, minecraft, quake, unreal2, valve}; use crate::GDErrorKind::InvalidInput; use crate::GDResult; @@ -23,7 +23,8 @@ pub enum Protocol { Gamespy(gamespy::GameSpyVersion), Minecraft(Option), Quake(quake::QuakeVersion), - Valve(valve::SteamApp), + Valve(valve::Engine), + Unreal2, #[cfg(feature = "games")] PROPRIETARY(ProprietaryProtocol), } @@ -35,6 +36,7 @@ pub enum GenericResponse<'a> { Minecraft(minecraft::VersionedResponse<'a>), Quake(quake::VersionedResponse<'a>), Valve(&'a valve::Response), + Unreal2(&'a unreal2::Response), #[cfg(feature = "games")] TheShip(&'a crate::games::theship::Response), #[cfg(feature = "games")] @@ -51,6 +53,7 @@ pub enum GenericPlayer<'a> { QuakeTwo(&'a quake::two::Player), Minecraft(&'a minecraft::Player), Gamespy(gamespy::VersionedPlayer<'a>), + Unreal2(&'a unreal2::Player), #[cfg(feature = "games")] TheShip(&'a crate::games::theship::TheShipPlayer), #[cfg(feature = "games")] diff --git a/crates/lib/src/protocols/unreal2/mod.rs b/crates/lib/src/protocols/unreal2/mod.rs new file mode 100644 index 0000000..7cf5e5c --- /dev/null +++ b/crates/lib/src/protocols/unreal2/mod.rs @@ -0,0 +1,58 @@ +/// The implementation. +pub mod protocol; +/// All types used by the implementation. +pub mod types; + +pub use protocol::*; +pub use types::*; + +/// Generate a module containing a query function for a valve game. +/// +/// * `mod_name` - The name to be given to the game module (see ID naming +/// conventions in CONTRIBUTING.md). +/// * `pretty_name` - The full name of the game, will be used as the +/// documentation for the created module. +/// * `default_port` - Passed through to [game_query_fn]. +#[cfg(feature = "games")] +macro_rules! game_query_mod { + ($mod_name: ident, $pretty_name: expr, $default_port: literal) => { + #[doc = $pretty_name] + pub mod $mod_name { + crate::protocols::unreal2::game_query_fn!($default_port); + } + }; +} + +#[cfg(feature = "games")] +pub(crate) use game_query_mod; + +// Allow generating doc comments: +// https://users.rust-lang.org/t/macros-filling-text-in-comments/20473 +/// Generate a query function for a valve game. +/// +/// * `default_port` - The default port the game uses. +#[cfg(feature = "games")] +macro_rules! game_query_fn { + ($default_port: literal) => { + crate::protocols::unreal2::game_query_fn! {@gen $default_port, concat!( + "Make a Unreal2 query for with default timeout settings and default extra request settings.\n\n", + "If port is `None`, then the default port (", stringify!($default_port), ") will be used.")} + }; + + (@gen $default_port: literal, $doc: expr) => { + #[doc = $doc] + pub fn query( + address: &std::net::IpAddr, + port: Option, + ) -> crate::GDResult { + crate::protocols::unreal2::query( + &std::net::SocketAddr::new(*address, port.unwrap_or($default_port)), + &crate::protocols::unreal2::GatheringSettings::default(), + None, + ) + } + }; +} + +#[cfg(feature = "games")] +pub(crate) use game_query_fn; diff --git a/crates/lib/src/protocols/unreal2/protocol.rs b/crates/lib/src/protocols/unreal2/protocol.rs new file mode 100644 index 0000000..0dde6ca --- /dev/null +++ b/crates/lib/src/protocols/unreal2/protocol.rs @@ -0,0 +1,308 @@ +use crate::buffer::{Buffer, StringDecoder}; +use crate::errors::GDErrorKind::PacketBad; +use crate::protocols::types::TimeoutSettings; +use crate::socket::{Socket, UdpSocket}; +use crate::utils::retry_on_timeout; +use crate::GDResult; + +use super::{GatheringSettings, MutatorsAndRules, PacketKind, Players, Response, ServerInfo}; + +use std::net::SocketAddr; + +use byteorder::{ByteOrder, LittleEndian}; +use encoding_rs::{UTF_16LE, WINDOWS_1252}; + +/// Response packets don't seem to exceed 500 bytes, set to 1024 just to be +/// safe. +const PACKET_SIZE: usize = 1024; + +/// Default amount of players to pre-allocate if numplayers was not included in +/// server info response. +const DEFAULT_PLAYER_PREALLOCATION: usize = 10; + +/// Maximum amount of players to pre-allocate: if the server specifies a number +/// larger than this in serverinfo we don't allocate that many. +const MAXIMUM_PLAYER_PREALLOCATION: usize = 50; + +/// The Unreal2 protocol implementation. +pub(crate) struct Unreal2Protocol { + socket: UdpSocket, + retry_count: usize, +} + +impl Unreal2Protocol { + pub fn new(address: &SocketAddr, timeout_settings: Option) -> GDResult { + let socket = UdpSocket::new(address)?; + let retry_count = timeout_settings + .as_ref() + .map(|t| t.get_retries()) + .unwrap_or_else(|| TimeoutSettings::default().get_retries()); + socket.apply_timeout(&timeout_settings)?; + + Ok(Self { + socket, + retry_count, + }) + } + + /// Send a request packet and recieve the first response (with retries). + fn get_request_data(&mut self, packet_type: PacketKind) -> GDResult> { + retry_on_timeout(self.retry_count, move || { + self.get_request_data_impl(packet_type) + }) + } + + /// Send a request packet + fn get_request_data_impl(&mut self, packet_type: PacketKind) -> GDResult> { + let request = [0x79, 0, 0, 0, packet_type as u8]; + self.socket.send(&request)?; + + let data = self.socket.receive(Some(PACKET_SIZE))?; + + Ok(data) + } + + /// Consume the header part of a response packet, validate that the packet + /// type matches what is expected. + fn consume_response_headers( + buffer: &mut Buffer, + expected_packet_type: PacketKind, + ) -> GDResult<()> { + // Skip header + buffer.move_cursor(4)?; + + let packet_type: u8 = buffer.read()?; + + let packet_type: PacketKind = packet_type.try_into()?; + + if packet_type != expected_packet_type { + Err(PacketBad.context(format!( + "Packet response ({:?}) didn't match request ({:?}) packet type", + packet_type, expected_packet_type + ))) + } else { + Ok(()) + } + } + + /// Send server info query. + pub fn query_server_info(&mut self) -> GDResult { + let data = self.get_request_data(PacketKind::ServerInfo)?; + let mut buffer = Buffer::::new(&data); + // TODO: Maybe put consume headers in individual packet parse methods + Self::consume_response_headers(&mut buffer, PacketKind::ServerInfo)?; + ServerInfo::parse(&mut buffer) + } + + /// Send mutators and rules query. + pub fn query_mutators_and_rules(&mut self) -> GDResult { + // This is a required packet so we validate that we get at least one response. + // However there can be many packets in response to a single request so + // we greedily handle packets until we get a timeout (or any receive + // error). + + let mut mutators_and_rules = MutatorsAndRules::default(); + { + let data = self.get_request_data(PacketKind::MutatorsAndRules)?; + let mut buffer = Buffer::::new(&data); + // TODO: Maybe put consume headers in individual packet parse methods + Self::consume_response_headers(&mut buffer, PacketKind::MutatorsAndRules)?; + mutators_and_rules.parse(&mut buffer)? + }; + + // We could receive multiple packets in response + while let Ok(data) = self.socket.receive(Some(PACKET_SIZE)) { + let mut buffer = Buffer::::new(&data); + + let r = Self::consume_response_headers(&mut buffer, PacketKind::MutatorsAndRules); + if r.is_err() { + println!("{:?}", r); + break; + } + + mutators_and_rules.parse(&mut buffer)?; + } + + Ok(mutators_and_rules) + } + + /// Send players query. + pub fn query_players(&mut self, server_info: Option<&ServerInfo>) -> GDResult { + // Pre-allocate the player arrays, but don't over allocate memory if the server + // specifies an insane number of players. + let num_players: Option = server_info.and_then(|i| i.num_players.try_into().ok()); + + let mut players = Players::with_capacity( + num_players + .unwrap_or(DEFAULT_PLAYER_PREALLOCATION) + .min(MAXIMUM_PLAYER_PREALLOCATION), + ); + + // Fetch first players packet (with retries) + let mut players_data = self.get_request_data(PacketKind::Players); + // Players are non required so if we don't get any responses we continue to + // return + while let Ok(data) = players_data { + let mut buffer = Buffer::::new(&data); + + Self::consume_response_headers(&mut buffer, PacketKind::Players)?; + + players.parse(&mut buffer)?; + + if let Some(num_players) = num_players { + if players.total_len() >= num_players { + // If we have already received the amount of players specified in server info + // then we don't need to wait for more player packets to time out. + break; + } + } + + // Receive next packet + players_data = self.socket.receive(Some(PACKET_SIZE)); + } + + Ok(players) + } + + /// Make a full server query. + pub fn query(&mut self, gather_settings: &GatheringSettings) -> GDResult { + // Fetch the server info, this can only handle one response packet + let server_info = self.query_server_info()?; + + let mutators_and_rules = if gather_settings.mutators_and_rules { + self.query_mutators_and_rules()? + } else { + MutatorsAndRules::default() + }; + + let players = if gather_settings.players { + self.query_players(Some(&server_info))? + } else { + Players::with_capacity(0) + }; + + // TODO: Handle extra info parsing when we detect certain game types (or maybe + // include that in gather settings). + + Ok(Response { + server_info, + mutators_and_rules, + players, + }) + } +} + +/// Unreal 2 string decoder +pub struct Unreal2StringDecoder; +impl StringDecoder for Unreal2StringDecoder { + type Delimiter = [u8; 1]; + + const DELIMITER: Self::Delimiter = [0x00]; + + fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult { + let mut ucs2 = false; + let mut length: usize = (*data + .first() + .ok_or(PacketBad.context("Tried to decode string without length"))?) + .into(); + + let mut start = 0; + + // Check if it is a UCS-2 string + if length >= 0x80 { + ucs2 = true; + + length = (length & 0x7f) * 2; + + start += 1; + + // For UCS-2 strings, some unreal 2 games randomly insert an extra 0x01 here, + // not included in the length. Skip it if present (hopefully this never happens + // legitimately) + if let Some(1) = data[start ..].first() { + start += 1; + } + } + + // If UCS2 the first byte is the masked length of the string + let result = if ucs2 { + let string_data = &data[start .. start + length]; + if string_data.len() != length { + return Err(PacketBad.context("Not enough data in buffer to read string")); + } + + // When node decodes UCS2 it uses the UFT16LE encoding. + // https://github.com/nodejs/node/blob/2aaa21f9f684484edb54be30589c4af0b923cdef/lib/buffer.js#L637-L645 + let (result, _, invalid_sequences) = UTF_16LE.decode(string_data); + + if invalid_sequences { + return Err(PacketBad.context("UTF-8 string contained invalid character(s)")); + } + + result + } else { + // Else the string is null-delimited latin1 + + // TODO: Replace this with delimiter finder helper + let position = data + // Create an iterator over the data. + .iter() + // Find the position of the delimiter + .position(|&b| b == delimiter.as_ref()[0]) + // If the delimiter is not found, use the whole data slice. + .unwrap_or(data.len()); + + length = position + 1; + + // Decode as latin1 + let (result, _, invalid_sequences) = WINDOWS_1252.decode(&data[0 .. position]); + + if invalid_sequences { + return Err(PacketBad.context("latin1 string contained invalid character(s)")); + } + + result + }; + + // Strip color encodings + // TODO: Improve efficiency + // TODO: There might be a nicer way to do this once string patterns are stable + // https://github.com/rust-lang/rust/issues/27721 + + // After '0x1b' skip 3 characters (including the '0x1b') + let mut char_skip = 0usize; + let result: String = result + .chars() + .filter(|c: &char| { + if '\x1b'.eq(c) { + char_skip = 4; + return false; + } + char_skip = char_skip.saturating_sub(1); + + char_skip == 0 + }) + .collect(); + + // Remove all characters between 0x00 and 0x1a + let result = result.replace(|c: char| c > '\x00' && c <= '\x1a', ""); + + *cursor += start + length; + + // Strip delimiter that wasn't included in length + Ok(result.trim_matches('\0').to_string()) + } +} + +/// Make an unreal2 query. +pub fn query( + address: &SocketAddr, + gather_settings: &GatheringSettings, + timeout_settings: Option, +) -> GDResult { + let mut client = Unreal2Protocol::new(address, timeout_settings)?; + + client.query(gather_settings) +} + +// TODO: Add tests diff --git a/crates/lib/src/protocols/unreal2/types.rs b/crates/lib/src/protocols/unreal2/types.rs new file mode 100644 index 0000000..594e76f --- /dev/null +++ b/crates/lib/src/protocols/unreal2/types.rs @@ -0,0 +1,246 @@ +use crate::buffer::Buffer; +use crate::errors::GDErrorKind::PacketBad; +use crate::protocols::types::{CommonPlayer, CommonResponse, ExtraRequestSettings, GenericPlayer}; +use crate::protocols::GenericResponse; +use crate::{GDError, GDResult}; + +use super::Unreal2StringDecoder; + +use std::collections::{HashMap, HashSet}; + +use byteorder::ByteOrder; + +/// Unreal 2 packet types. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[repr(u8)] +pub enum PacketKind { + ServerInfo = 0, + MutatorsAndRules = 1, + Players = 2, +} + +impl TryFrom for PacketKind { + type Error = GDError; + fn try_from(value: u8) -> GDResult { + match value { + 0 => Ok(Self::ServerInfo), + 1 => Ok(Self::MutatorsAndRules), + 2 => Ok(Self::Players), + _ => Err(PacketBad.context("Unknown packet type")), + } + } +} + +/// Unreal 2 server info. +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ServerInfo { + pub server_id: u32, + pub ip: String, + pub game_port: u32, + pub query_port: u32, + pub name: String, + pub map: String, + pub game_type: String, + pub num_players: u32, + pub max_players: u32, +} + +impl ServerInfo { + pub fn parse(buffer: &mut Buffer) -> GDResult { + Ok(ServerInfo { + server_id: buffer.read()?, + ip: buffer.read_string::(None)?, + game_port: buffer.read()?, + query_port: buffer.read()?, + name: buffer.read_string::(None)?, + map: buffer.read_string::(None)?, + game_type: buffer.read_string::(None)?, + num_players: buffer.read()?, + max_players: buffer.read()?, + }) + } +} + +/// Unreal 2 mutators and rules. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct MutatorsAndRules { + pub mutators: HashSet, + pub rules: HashMap>, +} + +impl MutatorsAndRules { + pub fn parse(&mut self, buffer: &mut Buffer) -> GDResult<()> { + while buffer.remaining_length() > 0 { + let key = buffer.read_string::(None)?; + let value = buffer.read_string::(None).ok(); + + if key.eq_ignore_ascii_case("mutator") { + if let Some(value) = value { + self.mutators.insert(value); + } + } else { + let rule_vec = self.rules.get_mut(&key); + + let rule_vec = if let Some(rule_vec) = rule_vec { + rule_vec + } else { + self.rules.insert(key.clone(), Vec::default()); + self.rules + .get_mut(&key) + .expect("Value should be in HashMap after we inserted") + }; + + if let Some(value) = value { + rule_vec.push(value); + } + } + } + Ok(()) + } +} + +/// Unreal 2 players and bots. +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Players { + /// List of players returned by server (without 0 ping). + pub players: Vec, + /// List of bots returned by server (players with 0 ping). + pub bots: Vec, +} + +impl Players { + /// Pre-allocate the vectors inside the players struct based on the provided + /// capacity. + pub fn with_capacity(capacity: usize) -> Self { + Players { + players: Vec::with_capacity(capacity), + // Allocate half as many bots as we don't expect there to be as many + bots: Vec::with_capacity(capacity / 2), + } + } + + /// Parse a raw buffer of players into the current struct. + pub fn parse(&mut self, buffer: &mut Buffer) -> GDResult<()> { + while buffer.remaining_length() > 0 { + let player = Player { + id: buffer.read()?, + name: buffer.read_string::(None)?, + ping: buffer.read()?, + score: buffer.read()?, + stats_id: buffer.read()?, + }; + + // If ping is 0 the player is a bot + if player.ping == 0 { + self.bots.push(player); + } else { + self.players.push(player); + } + } + + Ok(()) + } + + /// Length of both players and bots. + pub fn total_len(&self) -> usize { self.players.len() + self.bots.len() } +} + +/// Unreal 2 player info. +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Player { + pub id: u32, + pub name: String, + pub ping: u32, + pub score: i32, + pub stats_id: u32, +} + +impl CommonPlayer for Player { + fn name(&self) -> &str { &self.name } + + fn score(&self) -> Option { Some(self.score) } + + fn as_original(&self) -> GenericPlayer { GenericPlayer::Unreal2(self) } +} + +/// Unreal 2 response. +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Response { + pub server_info: ServerInfo, + pub mutators_and_rules: MutatorsAndRules, + pub players: Players, +} + +impl CommonResponse for Response { + fn map(&self) -> Option<&str> { Some(&self.server_info.map) } + + fn name(&self) -> Option<&str> { Some(&self.server_info.name) } + + fn game_mode(&self) -> Option<&str> { Some(&self.server_info.game_type) } + + fn players_online(&self) -> u32 { self.server_info.num_players } + + fn players_maximum(&self) -> u32 { self.server_info.max_players } + + fn players(&self) -> Option> { + Some( + self.players + .players + .iter() + .map(|player| player as _) + .collect(), + ) + } + + fn as_original(&self) -> GenericResponse { GenericResponse::Unreal2(self) } +} + +/// What data to gather, purely used only with the query function. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct GatheringSettings { + pub players: bool, + pub mutators_and_rules: bool, +} + +impl GatheringSettings { + /// Default values are true for both the players and the rules. + pub const fn default() -> Self { + Self { + players: true, + mutators_and_rules: true, + } + } + + pub const fn into_extra(self) -> ExtraRequestSettings { + ExtraRequestSettings { + hostname: None, + protocol_version: None, + gather_players: Some(self.players), + gather_rules: Some(self.mutators_and_rules), + check_app_id: None, + } + } +} + +impl Default for GatheringSettings { + fn default() -> Self { GatheringSettings::default() } +} + +impl From for GatheringSettings { + fn from(value: ExtraRequestSettings) -> Self { + let default = Self::default(); + Self { + players: value.gather_players.unwrap_or(default.players), + mutators_and_rules: value.gather_rules.unwrap_or(default.mutators_and_rules), + } + } +} + +// TODO: Add tests diff --git a/crates/lib/src/protocols/valve/mod.rs b/crates/lib/src/protocols/valve/mod.rs index ab935b4..1468f14 100644 --- a/crates/lib/src/protocols/valve/mod.rs +++ b/crates/lib/src/protocols/valve/mod.rs @@ -13,42 +13,58 @@ pub use types::*; /// * `pretty_name` - The full name of the game, will be used as the /// documentation for the created module. /// * `steam_app`, `default_port` - Passed through to [game_query_fn]. +#[cfg(feature = "games")] macro_rules! game_query_mod { - ($mod_name: ident, $pretty_name: expr, $steam_app: ident, $default_port: literal) => { + ($mod_name: ident, $pretty_name: expr, $engine: expr, $default_port: literal) => { + crate::protocols::valve::game_query_mod!( + $mod_name, + $pretty_name, + $engine, + $default_port, + GatheringSettings::default() + ); + }; + + ($mod_name: ident, $pretty_name: expr, $engine: expr, $default_port: literal, $gathering_settings: expr) => { #[doc = $pretty_name] pub mod $mod_name { - crate::protocols::valve::game_query_fn!($steam_app, $default_port); + use crate::protocols::valve::{Engine, GatheringSettings}; + + crate::protocols::valve::game_query_fn!($pretty_name, $engine, $default_port, $gathering_settings); } }; } +#[cfg(feature = "games")] pub(crate) use game_query_mod; // Allow generating doc comments: // https://users.rust-lang.org/t/macros-filling-text-in-comments/20473 /// Generate a query function for a valve game. /// -/// * `steam_app` - The entry in the [SteamApp] enum that the game uses. +/// * `engine` - The [Engine] that the game uses. /// * `default_port` - The default port the game uses. /// /// ```rust,ignore /// use crate::protocols::valve::game_query_fn; /// game_query_fn!(TEAMFORTRESS2, 27015); /// ``` +#[cfg(feature = "games")] macro_rules! game_query_fn { - ($steam_app: ident, $default_port: literal) => { - crate::protocols::valve::game_query_fn!{@gen $steam_app, $default_port, concat!( - "Make a valve query for ", stringify!($steam_app), " with default timeout settings and default extra request settings.\n\n", - "If port is `None`, then the default port (", stringify!($default_port), ") will be used.")} + ($pretty_name: expr, $engine: expr, $default_port: literal, $gathering_settings: expr) => { + // TODO: By using $gathering_settings, also add to doc if a game doesnt respond to certain gathering settings + crate::protocols::valve::game_query_fn!{@gen $engine, $default_port, concat!( + "Make a valve query for ", $pretty_name, " with default timeout settings and default extra request settings.\n\n", + "If port is `None`, then the default port (", stringify!($default_port), ") will be used."), $gathering_settings} }; - (@gen $steam_app: ident, $default_port: literal, $doc: expr) => { + (@gen $engine: expr, $default_port: literal, $doc: expr, $gathering_settings: expr) => { #[doc = $doc] pub fn query(address: &std::net::IpAddr, port: Option) -> crate::GDResult { let valve_response = crate::protocols::valve::query( &std::net::SocketAddr::new(*address, port.unwrap_or($default_port)), - crate::protocols::valve::SteamApp::$steam_app.as_engine(), - None, + $engine, + Some($gathering_settings), None, )?; @@ -57,4 +73,5 @@ macro_rules! game_query_fn { }; } +#[cfg(feature = "games")] pub(crate) use game_query_fn; diff --git a/crates/lib/src/protocols/valve/protocol.rs b/crates/lib/src/protocols/valve/protocol.rs index 4cde7e9..7fc44cb 100644 --- a/crates/lib/src/protocols/valve/protocol.rs +++ b/crates/lib/src/protocols/valve/protocol.rs @@ -16,7 +16,6 @@ use crate::{ }, Engine, ModData, - SteamApp, }, }, socket::{Socket, UdpSocket}, @@ -59,8 +58,8 @@ impl SplitPacket { Engine::Source(_) => { let total = buffer.read()?; let number = buffer.read()?; - let size = match protocol == 7 && (*engine == SteamApp::CSS.as_engine()) { - // certain apps with protocol = 7 dont have this field + let size = match protocol == 7 && (*engine == Engine::new(240)) { + // certain apps with protocol = 7 dont have this field, such as CSS false => buffer.read()?, true => 1248, }; @@ -304,7 +303,7 @@ impl ValveProtocol { let environment_type = Environment::from_gldsrc(buffer.read()?)?; let has_password = buffer.read::()? == 1; let vac_secured = buffer.read::()? == 1; - let the_ship = match *engine == SteamApp::THESHIP.as_engine() { + let the_ship = match *engine == Engine::new(2400) { false => None, true => { Some(TheShip { @@ -389,11 +388,11 @@ impl ValveProtocol { name: buffer.read_string::(None)?, score: buffer.read()?, duration: buffer.read()?, - deaths: match *engine == SteamApp::THESHIP.as_engine() { + deaths: match *engine == Engine::new(2400) { false => None, true => Some(buffer.read()?), }, - money: match *engine == SteamApp::THESHIP.as_engine() { + money: match *engine == Engine::new(2400) { false => None, true => Some(buffer.read()?), }, @@ -418,7 +417,8 @@ impl ValveProtocol { rules.insert(name, value); } - if *engine == SteamApp::ROR2.as_engine() { + if *engine == Engine::new(632_360) { + // ROR2 rules.remove("Test"); } diff --git a/crates/lib/src/protocols/valve/types.rs b/crates/lib/src/protocols/valve/types.rs index e3c888a..ebe46ad 100644 --- a/crates/lib/src/protocols/valve/types.rs +++ b/crates/lib/src/protocols/valve/types.rs @@ -249,156 +249,20 @@ impl Request { } } -/// Supported steam apps -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum SteamApp { - /// Counter-Strike - COUNTERSTRIKE, - /// Creativerse - CREATIVERSE, - /// Team Fortress Classic - TFC, - /// Day of Defeat - DOD, - /// Counter-Strike: Condition Zero - CSCZ, - /// Counter-Strike: Source - CSS, - /// Day of Defeat: Source - DODS, - /// Half-Life 2 Deathmatch - HL2D, - /// Half-Life Deathmatch: Source - HLDS, - /// Team Fortress 2 - TEAMFORTRESS2, - /// Left 4 Dead - LEFT4DEAD, - /// Left 4 Dead - LEFT4DEAD2, - /// Alien Swarm - ALIENSWARM, - /// Counter-Strike: Global Offensive - CSGO, - /// The Ship - THESHIP, - /// Garry's Mod - GARRYSMOD, - /// Age of Chivalry - AOC, - /// Insurgency: Modern Infantry Combat - IMIC, - /// ARMA 2: Operation Arrowhead - A2OA, - /// Project Zomboid - PROJECTZOMBOID, - /// Insurgency - INSURGENCY, - /// Sven Co-op - SCO, - /// 7 Days To Die - SD2D, - /// Rust - RUST, - /// Ballistic Overkill - BALLISTICOVERKILL, - /// Don't Starve Together - DST, - /// BrainBread 2 - BRAINBREAD2, - /// Codename CURE - CODENAMECURE, - /// Black Mesa - BLACKMESA, - /// Colony Survival - COLONYSURVIVAL, - /// Avorion - AVORION, - /// Day of Infamy - DOI, - /// The Forest - THEFOREST, - /// Unturned - UNTURNED, - /// ARK: Survival Evolved - ASE, - /// Battalion 1944 - BATTALION1944, - /// Insurgency: Sandstorm - INSURGENCYSANDSTORM, - /// Alien Swarm: Reactive Drop - ASRD, - /// Risk of Rain 2 - ROR2, - /// Operation: Harsh Doorstop - OHD, - /// Onset - ONSET, - /// V Rising - VRISING, - /// Hell Let Loose - HLL, - /// Barotrauma - BAROTRAUMA, -} - -impl SteamApp { - /// Get the specified app as engine. - pub const fn as_engine(&self) -> Engine { - match self { - Self::CSS => Engine::new_source(240), - Self::DODS => Engine::new_source(300), - Self::HL2D => Engine::new_source(320), - Self::HLDS => Engine::new_source(360), - Self::TEAMFORTRESS2 => Engine::new_source(440), - Self::LEFT4DEAD => Engine::new_source(500), - Self::LEFT4DEAD2 => Engine::new_source(550), - Self::ALIENSWARM => Engine::new_source(630), - Self::CSGO => Engine::new_source(730), - Self::THESHIP => Engine::new_source(2400), - Self::GARRYSMOD => Engine::new_source(4000), - Self::AOC => Engine::new_source(17510), - Self::IMIC => Engine::new_source(17700), - Self::A2OA => Engine::new_source(33930), - Self::PROJECTZOMBOID => Engine::new_source(108_600), - Self::INSURGENCY => Engine::new_source(222_880), - Self::SD2D => Engine::new_source(251_570), - Self::RUST => Engine::new_source(252_490), - Self::CREATIVERSE => Engine::new_source(280_790), - Self::BALLISTICOVERKILL => Engine::new_source(296_300), - Self::DST => Engine::new_source(322_320), - Self::BRAINBREAD2 => Engine::new_source(346_330), - Self::CODENAMECURE => Engine::new_source(355_180), - Self::BLACKMESA => Engine::new_source(362_890), - Self::COLONYSURVIVAL => Engine::new_source(366_090), - Self::AVORION => Engine::new_source(445_220), - Self::DOI => Engine::new_source(447_820), - Self::THEFOREST => Engine::new_source(556_450), - Self::UNTURNED => Engine::new_source(304_930), - Self::ASE => Engine::new_source(346_110), - Self::BATTALION1944 => Engine::new_source(489_940), - Self::INSURGENCYSANDSTORM => Engine::new_source(581_320), - Self::ASRD => Engine::new_source(563_560), - Self::BAROTRAUMA => Engine::new_source(602960), - Self::ROR2 => Engine::new_source(632_360), - Self::OHD => Engine::new_source_with_dedicated(736_590, 950_900), - Self::ONSET => Engine::new_source(1_105_810), - Self::VRISING => Engine::new_source(1_604_030), - Self::HLL => Engine::new_source(686_810), - _ => Engine::GoldSrc(false), // CS - 10, TFC - 20, DOD - 30, CSCZ - 80, SC - 225840 - } - } -} - -/// Engine type. +/// Every supported Valve game references this enum, represents the behaviour +/// of server requests and responses. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum Engine { - /// A Source game, the argument represents the possible steam app ids, if - /// its **None**, let the query find it, if its **Some**, the query - /// fails if the response id is not the first one, which is the game app - /// id, or the other one, which is the dedicated server app id. + /// A Source game, the argument represents the possible steam app ids. + /// If its **None**, let the query find it (could come with some drawbacks, + /// some games do not respond on certain protocol versions (CSS on 7), + /// some have additional data (The Ship). + /// If its **Some**, the first value is the main steam app id, the second + /// could be a secondly used id, as some games use a different one for + /// dedicated servers. Beware if **check_app_id** is set to true in + /// [GatheringSettings], as the query will fail if the server doesnt respond + /// with the expected ids. Source(Option<(u32, Option)>), /// A GoldSrc game, the argument indicates whether to enforce /// requesting the obsolete A2S_INFO response or not. @@ -406,9 +270,11 @@ pub enum Engine { } impl Engine { - pub const fn new_source(appid: u32) -> Self { Self::Source(Some((appid, None))) } + pub const fn new(appid: u32) -> Self { Self::Source(Some((appid, None))) } - pub const fn new_source_with_dedicated(appid: u32, dedicated_appid: u32) -> Self { + pub const fn new_gold_src(force: bool) -> Self { Self::GoldSrc(force) } + + pub const fn new_with_dedicated(appid: u32, dedicated_appid: u32) -> Self { Self::Source(Some((appid, Some(dedicated_appid)))) } } @@ -422,15 +288,29 @@ pub struct GatheringSettings { pub check_app_id: bool, } -impl Default for GatheringSettings { +impl GatheringSettings { /// Default values are true for both the players and the rules. - fn default() -> Self { + pub const fn default() -> Self { Self { players: true, rules: true, check_app_id: true, } } + + pub const fn into_extra(self) -> ExtraRequestSettings { + ExtraRequestSettings { + hostname: None, + protocol_version: None, + gather_players: Some(self.players), + gather_rules: Some(self.rules), + check_app_id: Some(self.check_app_id), + } + } +} + +impl Default for GatheringSettings { + fn default() -> Self { GatheringSettings::default() } } impl From for GatheringSettings {