From 8a88e826fafadb4d9e63dfe9c85141a61ed575ba Mon Sep 17 00:00:00 2001 From: Douile Date: Tue, 17 Oct 2023 13:37:48 +0100 Subject: [PATCH 01/13] [Repo] Link to CONTRIBUTING.md in README --- README.md | 2 ++ 1 file changed, 2 insertions(+) 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). From 1ca6e6e85cbc93d338f1ef86f11bd04e45149a74 Mon Sep 17 00:00:00 2001 From: Tom <25043847+Douile@users.noreply.github.com> Date: Tue, 17 Oct 2023 16:26:03 +0000 Subject: [PATCH 02/13] [Clippy] Remove .clone() from timeout_settings as it is now Copy (#127) These clones are unnecessary when the type implements Copy and this was generating clippy warnings. --- src/protocols/minecraft/protocol/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/protocols/minecraft/protocol/mod.rs b/src/protocols/minecraft/protocol/mod.rs index 885f990..79fa6ab 100644 --- a/src/protocols/minecraft/protocol/mod.rs +++ b/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); } From 501524b0da93050870aeb7c977951ab5693fbbf7 Mon Sep 17 00:00:00 2001 From: CosminPerRam Date: Thu, 19 Oct 2023 23:15:10 +0300 Subject: [PATCH 03/13] feat: Add GatheringSettings on Valve macros (#128) * Initial macro modification for gathering settings * Initial Valheim support * Remove unused use * Fix macro Thanks bunch @Douile * docs: Add Valheim to CHANGELOG and GAMES * Add commentary regarding gathering settings comment generation * Add GatheringSettings to game! * Remove unused stuff * Fix tests and add comment regarding the game argument --- CHANGELOG.md | 2 +- GAMES.md | 1 + examples/generic.rs | 30 ++++++++++++++++++------------ src/games/definitions.rs | 16 ++++++++++++++++ src/games/mod.rs | 6 +++++- src/games/valve.rs | 11 +++++++++++ src/protocols/valve/mod.rs | 23 ++++++++++++++++++----- src/protocols/valve/types.rs | 21 +++++++++++++++++++-- 8 files changed, 89 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b373c2b..5f8f282 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ Who knows what the future holds... # 0.X.Y - DD/MM/YYYY ### Changes: -Nothing yet. +- Added [Valheim](https://store.steampowered.com/app/892970/Valheim/) support. ### Breaking: None, yaay! diff --git a/GAMES.md b/GAMES.md index 4a71917..ad1dcdf 100644 --- a/GAMES.md +++ b/GAMES.md @@ -62,6 +62,7 @@ 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. | ## Planned to add support: _ diff --git a/examples/generic.rs b/examples/generic.rs index 4935bee..7c7ad27 100644 --- a/examples/generic.rs +++ b/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] diff --git a/src/games/definitions.rs b/src/games/definitions.rs index 4b3a7f9..1e36305 100644 --- a/src/games/definitions.rs +++ b/src/games/definitions.rs @@ -10,14 +10,25 @@ use crate::protocols::{ 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, } }; } @@ -86,6 +97,11 @@ pub static GAMES: Map<&'static str, Game> = phf_map! { "theship" => game!("The Ship", 27015, Protocol::PROPRIETARY(ProprietaryProtocol::TheShip)), "unturned" => game!("Unturned", 27015, Protocol::Valve(SteamApp::UNTURNED)), "unrealtournament" => game!("Unreal Tournament", 7778, Protocol::Gamespy(GameSpyVersion::One)), + "valheim" => game!("Valheim", 2457, Protocol::Valve(SteamApp::VALHEIM), GatheringSettings { + players: true, + rules: false, + check_app_id: true, + }.into_extra()), "vrising" => game!("V Rising", 27016, Protocol::Valve(SteamApp::VRISING)), "jc2m" => game!("Just Cause 2: Multiplayer", 7777, Protocol::PROPRIETARY(ProprietaryProtocol::JC2M)), "warsow" => game!("Warsow", 44400, Protocol::Quake(QuakeVersion::Three)), diff --git a/src/games/mod.rs b/src/games/mod.rs index 10129f7..9650f1f 100644 --- a/src/games/mod.rs +++ b/src/games/mod.rs @@ -39,6 +39,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")] @@ -78,7 +80,9 @@ pub fn query_with_timeout_and_extra_settings( protocols::valve::query( &socket_addr, steam_app.as_engine(), - extra_settings.map(ExtraRequestSettings::into), + extra_settings + .or(Option::from(game.request_settings.clone())) + .map(ExtraRequestSettings::into), timeout_settings, ) .map(Box::new)? diff --git a/src/games/valve.rs b/src/games/valve.rs index e55594b..80b6ad9 100644 --- a/src/games/valve.rs +++ b/src/games/valve.rs @@ -53,4 +53,15 @@ 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!( + valheim, + "Valheim", + VALHEIM, + 2457, + GatheringSettings { + players: true, + rules: false, + check_app_id: true, + } +); game_query_mod!(vrising, "V Rising", VRISING, 27016); diff --git a/src/protocols/valve/mod.rs b/src/protocols/valve/mod.rs index ab935b4..575a200 100644 --- a/src/protocols/valve/mod.rs +++ b/src/protocols/valve/mod.rs @@ -15,9 +15,21 @@ pub use types::*; /// * `steam_app`, `default_port` - Passed through to [game_query_fn]. macro_rules! game_query_mod { ($mod_name: ident, $pretty_name: expr, $steam_app: ident, $default_port: literal) => { + crate::protocols::valve::game_query_mod!( + $mod_name, + $pretty_name, + $steam_app, + $default_port, + GatheringSettings::default() + ); + }; + + ($mod_name: ident, $pretty_name: expr, $steam_app: ident, $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::GatheringSettings; + + crate::protocols::valve::game_query_fn!($steam_app, $default_port, $gathering_settings); } }; } @@ -36,19 +48,20 @@ pub(crate) use game_query_mod; /// game_query_fn!(TEAMFORTRESS2, 27015); /// ``` macro_rules! game_query_fn { - ($steam_app: ident, $default_port: literal) => { + ($steam_app: ident, $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 $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.")} + "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 $steam_app: ident, $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, + Some($gathering_settings), None, )?; diff --git a/src/protocols/valve/types.rs b/src/protocols/valve/types.rs index e3c888a..573f920 100644 --- a/src/protocols/valve/types.rs +++ b/src/protocols/valve/types.rs @@ -341,6 +341,8 @@ pub enum SteamApp { HLL, /// Barotrauma BAROTRAUMA, + /// Valheim + VALHEIM, } impl SteamApp { @@ -383,6 +385,7 @@ impl SteamApp { 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::VALHEIM => Engine::new_source(892_970), Self::ONSET => Engine::new_source(1_105_810), Self::VRISING => Engine::new_source(1_604_030), Self::HLL => Engine::new_source(686_810), @@ -422,15 +425,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 { From 2cc9e5616832e12bd2e803d00abc0e7c75524e49 Mon Sep 17 00:00:00 2001 From: CosminPerRam Date: Sat, 21 Oct 2023 23:13:14 +0300 Subject: [PATCH 04/13] feat: Remove SteamApp (#142) * Remove the SteamApp enum. * Further removal * Replace SteamApp with Engine on game! usage * Add removal of SteamApp in changelog * docs: Update reference of SteamApp to Engine * Update the docs of the Engine type to be more descriptive. --- CHANGELOG.md | 3 +- src/games/battalion1944.rs | 5 +- src/games/definitions.rs | 90 ++++++++--------- src/games/mod.rs | 4 +- src/games/theship.rs | 5 +- src/games/valve.rs | 141 +++++++++++++++++++-------- src/protocols/types.rs | 2 +- src/protocols/valve/mod.rs | 22 ++--- src/protocols/valve/protocol.rs | 14 +-- src/protocols/valve/types.rs | 167 +++----------------------------- 10 files changed, 187 insertions(+), 266 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f8f282..4a08885 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ Who knows what the future holds... - Added [Valheim](https://store.steampowered.com/app/892970/Valheim/) support. ### Breaking: -None, yaay! +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/src/games/battalion1944.rs b/src/games/battalion1944.rs index dc49d9e..b6d4851 100644 --- a/src/games/battalion1944.rs +++ b/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/src/games/definitions.rs b/src/games/definitions.rs index 1e36305..578e3e5 100644 --- a/src/games/definitions.rs +++ b/src/games/definitions.rs @@ -4,7 +4,7 @@ use crate::protocols::{ gamespy::GameSpyVersion, minecraft::{LegacyGroup, Server}, quake::QuakeVersion, - valve::SteamApp, + valve::Engine, Protocol, }; use crate::Game; @@ -44,65 +44,65 @@ 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))), + "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))), + "left4dead" => game!("Left 4 Dead", 27015, Protocol::Valve(Engine::new(500))), + "left4dead2" => 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)), + "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))), + "7d2d" => 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))), + "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)), - "valheim" => game!("Valheim", 2457, Protocol::Valve(SteamApp::VALHEIM), GatheringSettings { + "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(SteamApp::VRISING)), + "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)), }; diff --git a/src/games/mod.rs b/src/games/mod.rs index 9650f1f..35ebbd3 100644 --- a/src/games/mod.rs +++ b/src/games/mod.rs @@ -76,10 +76,10 @@ 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(), + *engine, extra_settings .or(Option::from(game.request_settings.clone())) .map(ExtraRequestSettings::into), diff --git a/src/games/theship.rs b/src/games/theship.rs index 47de24f..5084200 100644 --- a/src/games/theship.rs +++ b/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/src/games/valve.rs b/src/games/valve.rs index 80b6ad9..c7a4a71 100644 --- a/src/games/valve.rs +++ b/src/games/valve.rs @@ -2,61 +2,116 @@ 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!( + 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!(left4dead, "Left 4 Dead", Engine::new(500), 27015); +game_query_mod!(left4dead2, "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!(sd2d, "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!(unturned, "Unturned", Engine::new(304_930), 27015); game_query_mod!( valheim, "Valheim", - VALHEIM, + Engine::new(892_970), 2457, GatheringSettings { players: true, @@ -64,4 +119,4 @@ game_query_mod!( check_app_id: true, } ); -game_query_mod!(vrising, "V Rising", VRISING, 27016); +game_query_mod!(vrising, "V Rising", Engine::new(1_604_030), 27016); diff --git a/src/protocols/types.rs b/src/protocols/types.rs index 83a9cf9..2fcd1a1 100644 --- a/src/protocols/types.rs +++ b/src/protocols/types.rs @@ -23,7 +23,7 @@ pub enum Protocol { Gamespy(gamespy::GameSpyVersion), Minecraft(Option), Quake(quake::QuakeVersion), - Valve(valve::SteamApp), + Valve(valve::Engine), #[cfg(feature = "games")] PROPRIETARY(ProprietaryProtocol), } diff --git a/src/protocols/valve/mod.rs b/src/protocols/valve/mod.rs index 575a200..9e67fae 100644 --- a/src/protocols/valve/mod.rs +++ b/src/protocols/valve/mod.rs @@ -14,22 +14,22 @@ pub use types::*; /// documentation for the created module. /// * `steam_app`, `default_port` - Passed through to [game_query_fn]. 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, - $steam_app, + $engine, $default_port, GatheringSettings::default() ); }; - ($mod_name: ident, $pretty_name: expr, $steam_app: ident, $default_port: literal, $gathering_settings: expr) => { + ($mod_name: ident, $pretty_name: expr, $engine: expr, $default_port: literal, $gathering_settings: expr) => { #[doc = $pretty_name] pub mod $mod_name { - use crate::protocols::valve::GatheringSettings; + use crate::protocols::valve::{Engine, GatheringSettings}; - crate::protocols::valve::game_query_fn!($steam_app, $default_port, $gathering_settings); + crate::protocols::valve::game_query_fn!($pretty_name, $engine, $default_port, $gathering_settings); } }; } @@ -40,7 +40,7 @@ pub(crate) use game_query_mod; // 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 @@ -48,19 +48,19 @@ pub(crate) use game_query_mod; /// game_query_fn!(TEAMFORTRESS2, 27015); /// ``` macro_rules! game_query_fn { - ($steam_app: ident, $default_port: literal, $gathering_settings: expr) => { + ($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 $steam_app, $default_port, concat!( - "Make a valve query for ", stringify!($steam_app), " with default timeout settings and default extra request settings.\n\n", + 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, $gathering_settings: 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(), + $engine, Some($gathering_settings), None, )?; diff --git a/src/protocols/valve/protocol.rs b/src/protocols/valve/protocol.rs index 4cde7e9..7fc44cb 100644 --- a/src/protocols/valve/protocol.rs +++ b/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/src/protocols/valve/types.rs b/src/protocols/valve/types.rs index 573f920..ebe46ad 100644 --- a/src/protocols/valve/types.rs +++ b/src/protocols/valve/types.rs @@ -249,159 +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, - /// Valheim - VALHEIM, -} - -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::VALHEIM => Engine::new_source(892_970), - 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. @@ -409,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)))) } } From e3dd7cd1c767298b99218d2dcb26d18d7a10ab3f Mon Sep 17 00:00:00 2001 From: CosminPerRam Date: Tue, 24 Oct 2023 22:39:36 +0300 Subject: [PATCH 05/13] Fix some game ids and clarify rules 6 and 7. (#143) * Fix wrongly named game ids of l4d, l4d2 and q3a * Specify how edition/protocol names are appended to games, rule 7 * Clarify rule 6 regarding numbers * Add/Update badge * Fix rule 7 misswording * Fix 7 Days to Die * List id changes in the changelog * Forgot to fix test * Apply Douile suggestion regarding rule 6 Co-authored-by: Tom <25043847+Douile@users.noreply.github.com> * Fix typo on rule 6 and wrap text * Clarify further rule 7 --------- Co-authored-by: GitHub Action Co-authored-by: Tom <25043847+Douile@users.noreply.github.com> --- .github/badges/node.svg | 8 ++++---- CHANGELOG.md | 6 ++++++ CONTRIBUTING.md | 29 +++++++++++++++++------------ examples/generic.rs | 2 +- src/games/definitions.rs | 8 ++++---- src/games/quake.rs | 2 +- src/games/valve.rs | 6 +++--- 7 files changed, 36 insertions(+), 25 deletions(-) diff --git a/.github/badges/node.svg b/.github/badges/node.svg index 18d8a03..c329d50 100644 --- a/.github/badges/node.svg +++ b/.github/badges/node.svg @@ -1,5 +1,5 @@ - - Node game coverage: 12% + + Node game coverage: 11% @@ -13,8 +13,8 @@ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a08885..7c4878e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ Who knows what the future holds... - Added [Valheim](https://store.steampowered.com/app/892970/Valheim/) support. ### Breaking: +Game: +- Changed identifications of the following games as they weren't properly expecting the naming rules: +- - Left 4 Dead: `left4dead` -> `l4d`. +- - 7 Days to Die: `7d2d` in definitions and `sd2d` in game declaration -> `sdtd`. +- - Quake 3 Arena: `quake3arena` -> `q3a`. + Protocols: - Valve: Removed `SteamApp` due to it not being really useful at all, replaced all instances with `Engine`. 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/examples/generic.rs b/examples/generic.rs index 7c7ad27..0365dbf 100644 --- a/examples/generic.rs +++ b/examples/generic.rs @@ -114,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/src/games/definitions.rs b/src/games/definitions.rs index 578e3e5..869287c 100644 --- a/src/games/definitions.rs +++ b/src/games/definitions.rs @@ -77,18 +77,18 @@ pub static GAMES: Map<&'static str, Game> = phf_map! { "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))), - "left4dead" => game!("Left 4 Dead", 27015, Protocol::Valve(Engine::new(500))), - "left4dead2" => game!("Left 4 Dead 2", 27015, Protocol::Valve(Engine::new(550))), + "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)), + "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))), - "7d2d" => game!("7 Days To Die", 26900, Protocol::Valve(Engine::new(251_570))), + "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(Engine::new(556_450))), diff --git a/src/games/quake.rs b/src/games/quake.rs index 948e18c..dff9d79 100644 --- a/src/games/quake.rs +++ b/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/src/games/valve.rs b/src/games/valve.rs index c7a4a71..5b841bc 100644 --- a/src/games/valve.rs +++ b/src/games/valve.rs @@ -80,8 +80,8 @@ game_query_mod!( Engine::new(581_320), 27131 ); -game_query_mod!(left4dead, "Left 4 Dead", Engine::new(500), 27015); -game_query_mod!(left4dead2, "Left 4 Dead 2", Engine::new(550), 27015); +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", @@ -98,7 +98,7 @@ game_query_mod!( 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!(sd2d, "7 Days To Die", Engine::new(251_570), 26900); +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, From 1145a064a90b1ef6d0ab4df94182e32d450c233d Mon Sep 17 00:00:00 2001 From: CosminPerRam Date: Sun, 29 Oct 2023 00:00:48 +0300 Subject: [PATCH 06/13] feat: Add The Front support. --- CHANGELOG.md | 1 + GAMES.md | 1 + src/games/definitions.rs | 1 + src/games/valve.rs | 1 + 4 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c4878e..48ae948 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Who knows what the future holds... # 0.X.Y - DD/MM/YYYY ### Changes: - Added [Valheim](https://store.steampowered.com/app/892970/Valheim/) support. +- Added [The Front](https://store.steampowered.com/app/2285150/The_Front/) support. ### Breaking: Game: diff --git a/GAMES.md b/GAMES.md index ad1dcdf..621f860 100644 --- a/GAMES.md +++ b/GAMES.md @@ -63,6 +63,7 @@ Beware of the `Notes` column, as it contains information about query port offset | 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). | ## Planned to add support: _ diff --git a/src/games/definitions.rs b/src/games/definitions.rs index 869287c..553d1c0 100644 --- a/src/games/definitions.rs +++ b/src/games/definitions.rs @@ -92,6 +92,7 @@ pub static GAMES: Map<&'static str, Game> = phf_map! { "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(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)), diff --git a/src/games/valve.rs b/src/games/valve.rs index 5b841bc..4be1add 100644 --- a/src/games/valve.rs +++ b/src/games/valve.rs @@ -107,6 +107,7 @@ game_query_mod!( 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, From f11a50a415f599667bbe0cf17f1a142b3a7d931f Mon Sep 17 00:00:00 2001 From: CosminPerRam Date: Sun, 29 Oct 2023 00:10:47 +0300 Subject: [PATCH 07/13] feat: Add Conan Exiles support. --- CHANGELOG.md | 5 +++-- GAMES.md | 1 + src/games/definitions.rs | 5 +++++ src/games/valve.rs | 11 +++++++++++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48ae948..1b5dd6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,9 @@ Who knows what the future holds... # 0.X.Y - DD/MM/YYYY ### Changes: -- Added [Valheim](https://store.steampowered.com/app/892970/Valheim/) support. -- Added [The Front](https://store.steampowered.com/app/2285150/The_Front/) support. +- [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. ### Breaking: Game: diff --git a/GAMES.md b/GAMES.md index 621f860..67f169b 100644 --- a/GAMES.md +++ b/GAMES.md @@ -64,6 +64,7 @@ Beware of the `Notes` column, as it contains information about query port offset | 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. | ## Planned to add support: _ diff --git a/src/games/definitions.rs b/src/games/definitions.rs index 553d1c0..62c4882 100644 --- a/src/games/definitions.rs +++ b/src/games/definitions.rs @@ -58,6 +58,11 @@ pub static GAMES: Map<&'static str, Game> = phf_map! { "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))), diff --git a/src/games/valve.rs b/src/games/valve.rs index 4be1add..acd612c 100644 --- a/src/games/valve.rs +++ b/src/games/valve.rs @@ -34,6 +34,17 @@ game_query_mod!( 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", From adb2109aea12a7913bf8e8d55f6ba264036b3cec Mon Sep 17 00:00:00 2001 From: CosminPerRam Date: Sun, 29 Oct 2023 14:43:35 +0200 Subject: [PATCH 08/13] feat: add valve protocol query example --- CHANGELOG.md | 1 + examples/valve_protocol_query.rs | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 examples/valve_protocol_query.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b5dd6d..6df35d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Who knows what the future holds... - [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. ### Breaking: Game: diff --git a/examples/valve_protocol_query.rs b/examples/valve_protocol_query.rs new file mode 100644 index 0000000..7fd210f --- /dev/null +++ b/examples/valve_protocol_query.rs @@ -0,0 +1,23 @@ +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::time::Duration; +use gamedig::protocols::types::TimeoutSettings; +use gamedig::protocols::valve; +use gamedig::protocols::valve::{Engine, GatheringSettings}; + +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:#?}"); +} From a3740c54240e1be8b388258ccd65142393f15fdc Mon Sep 17 00:00:00 2001 From: CosminPerRam Date: Sun, 29 Oct 2023 14:44:21 +0200 Subject: [PATCH 09/13] fix: remove double similar use on minecraft mod --- src/protocols/minecraft/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/protocols/minecraft/mod.rs b/src/protocols/minecraft/mod.rs index da17668..3d98ab9 100644 --- a/src/protocols/minecraft/mod.rs +++ b/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::*; From 4bbe7e17806c5032d387597cbd58d7e020f0cb72 Mon Sep 17 00:00:00 2001 From: CosminPerRam Date: Sun, 29 Oct 2023 14:49:49 +0200 Subject: [PATCH 10/13] fix: run proper formatting on valve_protocol_query example --- examples/valve_protocol_query.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/valve_protocol_query.rs b/examples/valve_protocol_query.rs index 7fd210f..5e9de3f 100644 --- a/examples/valve_protocol_query.rs +++ b/examples/valve_protocol_query.rs @@ -1,15 +1,15 @@ -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::time::Duration; 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 + 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 }; @@ -18,6 +18,11 @@ fn main() { 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)); + let response = valve::query( + address, + engine, + Some(gather_settings), + Some(timeout_settings), + ); println!("{response:#?}"); } From 5c1568251a5f1df0a42db16c4d1be0c0d831eb9e Mon Sep 17 00:00:00 2001 From: Tom <25043847+Douile@users.noreply.github.com> Date: Mon, 30 Oct 2023 09:30:16 +0000 Subject: [PATCH 11/13] feat: Only compile game impl macros when they are needed (#144) These macros are only required when compiling the code gated behind the games feature, they are unused if that feature is not and are also crate only. --- src/protocols/gamespy/mod.rs | 4 ++++ src/protocols/quake/mod.rs | 4 ++++ src/protocols/valve/mod.rs | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/src/protocols/gamespy/mod.rs b/src/protocols/gamespy/mod.rs index 41d51f8..ef50096 100644 --- a/src/protocols/gamespy/mod.rs +++ b/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/src/protocols/quake/mod.rs b/src/protocols/quake/mod.rs index dd3c213..2368738 100644 --- a/src/protocols/quake/mod.rs +++ b/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/src/protocols/valve/mod.rs b/src/protocols/valve/mod.rs index 9e67fae..1468f14 100644 --- a/src/protocols/valve/mod.rs +++ b/src/protocols/valve/mod.rs @@ -13,6 +13,7 @@ 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, $engine: expr, $default_port: literal) => { crate::protocols::valve::game_query_mod!( @@ -34,6 +35,7 @@ macro_rules! game_query_mod { }; } +#[cfg(feature = "games")] pub(crate) use game_query_mod; // Allow generating doc comments: @@ -47,6 +49,7 @@ pub(crate) use game_query_mod; /// use crate::protocols::valve::game_query_fn; /// game_query_fn!(TEAMFORTRESS2, 27015); /// ``` +#[cfg(feature = "games")] macro_rules! game_query_fn { ($pretty_name: expr, $engine: expr, $default_port: literal, $gathering_settings: expr) => { // TODO: By using $gathering_settings, also add to doc if a game doesnt respond to certain gathering settings @@ -70,4 +73,5 @@ macro_rules! game_query_fn { }; } +#[cfg(feature = "games")] pub(crate) use game_query_fn; From 529abe9d76508d74d7826ae0d84595e6e77f775b Mon Sep 17 00:00:00 2001 From: Tom <25043847+Douile@users.noreply.github.com> Date: Mon, 30 Oct 2023 11:37:15 +0000 Subject: [PATCH 12/13] feat: Add the unreal2 protocol (#124) * WIP: Add unreal2 protocol * Add/Update badge * protocols/unreal2: Update doc comments and TODOs * protocols/unreal2: Don't pre-allocate as many bot players * protocols/unreal2: Use "encoding-rs" for decoding unreal2 strings * Add/Update badge * protocols/unreal2: Add constants for player pre-allocation. Also improve some doc comments and update PACKET_SIZE. * protocols/unreal2: Early break when enough players have been parsed Add a fast-path to avoid waiting for packet timeout when we have parsed as many players as specified in the server info packet. * protocols/unreal2: Use HashSet to store mutators * protocols/unreal2: Handle server sending multiple values for a rule * protocols/unreal2: Add GatheringSettings to control what to query GatheringSettings allows skipping querying rules and/or players which can make the query return much faster. This also required moving each individual query into its own helper. * protocols/unreal2: Add more derives to types * protocols/unreal2: Simplify ServerInfo::parse() Co-Authored-By: CosminPerRam * Docs: Add unreal2 protocol documentation I used a website to generate the markdown RESPONSES table, the save file from this website is included to make updating the table easier in the future. https://www.tablesgenerator.com/markdown_tables * Add/Update badge * protocols/unreal2: Use the correct encoding for UCS2 strings * Docs: Remove unnecessary TGN file --------- Co-authored-by: GitHub Action Co-authored-by: CosminPerRam --- .github/badges/node.svg | 8 +- CHANGELOG.md | 4 + Cargo.toml | 1 + GAMES.md | 6 + PROTOCOLS.md | 1 + RESPONSES.md | 97 +++++----- src/games/definitions.rs | 6 + src/games/mod.rs | 12 ++ src/games/unreal2.rs | 10 + src/protocols/mod.rs | 2 + src/protocols/types.rs | 5 +- src/protocols/unreal2/mod.rs | 54 ++++++ src/protocols/unreal2/protocol.rs | 308 ++++++++++++++++++++++++++++++ src/protocols/unreal2/types.rs | 246 ++++++++++++++++++++++++ 14 files changed, 708 insertions(+), 52 deletions(-) create mode 100644 src/games/unreal2.rs create mode 100644 src/protocols/unreal2/mod.rs create mode 100644 src/protocols/unreal2/protocol.rs create mode 100644 src/protocols/unreal2/types.rs diff --git a/.github/badges/node.svg b/.github/badges/node.svg index c329d50..bff5a5b 100644 --- a/.github/badges/node.svg +++ b/.github/badges/node.svg @@ -1,5 +1,5 @@ - - Node game coverage: 11% + + Node game coverage: 13% @@ -13,8 +13,8 @@ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6df35d8..6d29a3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,15 @@ Who knows what the future holds... # 0.X.Y - DD/MM/YYYY ### Changes: +Games: - [Valheim](https://store.steampowered.com/app/892970/Valheim/) support. - [The Front](https://store.steampowered.com/app/2285150/The_Front/) support. - [Conan Exiles](https://store.steampowered.com/app/440900/Conan_Exiles/) support. - 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: Game: - Changed identifications of the following games as they weren't properly expecting the naming rules: diff --git a/Cargo.toml b/Cargo.toml index 4949cc1..1d9d30a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ byteorder = "1.5" bzip2-rs = "0.1" crc32fast = "1.3" serde_json = "1.0" +encoding_rs = "0.8" serde = { version = "1.0", optional = true } diff --git a/GAMES.md b/GAMES.md index 67f169b..5d7bad6 100644 --- a/GAMES.md +++ b/GAMES.md @@ -65,6 +65,12 @@ 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 | ## 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/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/src/games/definitions.rs b/src/games/definitions.rs index 62c4882..b620ea0 100644 --- a/src/games/definitions.rs +++ b/src/games/definitions.rs @@ -111,4 +111,10 @@ 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), + "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/src/games/mod.rs b/src/games/mod.rs index 35ebbd3..7749d5b 100644 --- a/src/games/mod.rs +++ b/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 @@ -129,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/src/games/unreal2.rs b/src/games/unreal2.rs new file mode 100644 index 0000000..1ce4585 --- /dev/null +++ b/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/src/protocols/mod.rs b/src/protocols/mod.rs index 1b5128f..697d7e8 100644 --- a/src/protocols/mod.rs +++ b/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/src/protocols/types.rs b/src/protocols/types.rs index 2fcd1a1..67e7d13 100644 --- a/src/protocols/types.rs +++ b/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; @@ -24,6 +24,7 @@ pub enum Protocol { Minecraft(Option), Quake(quake::QuakeVersion), 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/src/protocols/unreal2/mod.rs b/src/protocols/unreal2/mod.rs new file mode 100644 index 0000000..c268020 --- /dev/null +++ b/src/protocols/unreal2/mod.rs @@ -0,0 +1,54 @@ +/// 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]. +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); + } + }; +} + +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. +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, + ) + } + }; +} + +pub(crate) use game_query_fn; diff --git a/src/protocols/unreal2/protocol.rs b/src/protocols/unreal2/protocol.rs new file mode 100644 index 0000000..0dde6ca --- /dev/null +++ b/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/src/protocols/unreal2/types.rs b/src/protocols/unreal2/types.rs new file mode 100644 index 0000000..594e76f --- /dev/null +++ b/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 From 1d7cb31bc43409c45d408f07b8a9c39938427b32 Mon Sep 17 00:00:00 2001 From: Douile <25043847+Douile@users.noreply.github.com> Date: Mon, 30 Oct 2023 13:50:17 +0000 Subject: [PATCH 13/13] protocols/unreal2: Only compile game impl macros when needed Unreal2 hadn't been added when #144 was merged so this got missed out. --- src/protocols/unreal2/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/protocols/unreal2/mod.rs b/src/protocols/unreal2/mod.rs index c268020..7cf5e5c 100644 --- a/src/protocols/unreal2/mod.rs +++ b/src/protocols/unreal2/mod.rs @@ -13,6 +13,7 @@ pub use types::*; /// * `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] @@ -22,6 +23,7 @@ macro_rules! game_query_mod { }; } +#[cfg(feature = "games")] pub(crate) use game_query_mod; // Allow generating doc comments: @@ -29,6 +31,7 @@ pub(crate) use game_query_mod; /// 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!( @@ -51,4 +54,5 @@ macro_rules! game_query_fn { }; } +#[cfg(feature = "games")] pub(crate) use game_query_fn;