From af8e1e9b1a8765fd095e1613a61e82185aa61546 Mon Sep 17 00:00:00 2001 From: CosminPerRam Date: Sun, 10 Dec 2023 19:39:26 +0200 Subject: [PATCH] feat: add savage 2 support (#169) * feat: savage 2 support * fix: add savage 2 to definitions * chore: run rustfmt * fix: config serde use * fix: remove needless borrow * docs: add savage 2 to protocols.md --- CHANGELOG.md | 1 + GAMES.md | 1 + PROTOCOLS.md | 15 ++-- RESPONSES.md | 104 ++++++++++++----------- crates/lib/src/games/definitions.rs | 1 + crates/lib/src/games/mod.rs | 5 ++ crates/lib/src/games/savage2/mod.rs | 8 ++ crates/lib/src/games/savage2/protocol.rs | 37 ++++++++ crates/lib/src/games/savage2/types.rs | 30 +++++++ crates/lib/src/protocols/types.rs | 3 + 10 files changed, 148 insertions(+), 57 deletions(-) create mode 100644 crates/lib/src/games/savage2/mod.rs create mode 100644 crates/lib/src/games/savage2/protocol.rs create mode 100644 crates/lib/src/games/savage2/types.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b4a1a4..82d736d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Games: - [Conan Exiles](https://store.steampowered.com/app/440900/Conan_Exiles/) support. - [Post Scriptum](https://store.steampowered.com/app/736220/Post_Scriptum/) support. - [Squad](https://store.steampowered.com/app/393380/Squad/) support. +- [Savage 2](https://savage2.net/) support. - Added a valve protocol query example. Protocols: diff --git a/GAMES.md b/GAMES.md index e8333f8..fe88259 100644 --- a/GAMES.md +++ b/GAMES.md @@ -73,6 +73,7 @@ Beware of the `Notes` column, as it contains information about query port offset | Unreal Tournament 2004 | UT2004 | Unreal2 | Query port offset: 1 | | Post Scriptum | POSTSCRIPTUM | Valve | | | Squad | SQUAD | Valve | | +| Savage 2 | SAVAGE2 | Proprietary | | ## Planned to add support: _ diff --git a/PROTOCOLS.md b/PROTOCOLS.md index 6b1ef0c..78c49ba 100644 --- a/PROTOCOLS.md +++ b/PROTOCOLS.md @@ -1,13 +1,14 @@ A protocol is defined as proprietary if it is being used only for a single scope (or series, like Minecraft). # Supported protocols: -| Name | For | Proprietary? | Documentation reference | Notes | -|----------------|-------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Valve Protocol | Games | No | [Server Queries](https://developer.valvesoftware.com/wiki/Server_queries) | In some cases, the players details query might contain some 0-length named players. Multi-packet decompression not tested. | -| Minecraft | Games | Yes | Java: [List Server Protocol](https://wiki.vg/Server_List_Ping)
Bedrock: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/minecraftbedrock.js) | | -| GameSpy | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy1.js) Two: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy2.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy3.js) | These protocols are not really standardized, gamedig tries to get the most common fields amongst its supported games, if there are parsing problems, use the `query_vars` function. | -| Quake | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake1.js) Two: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake2.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake3.js) | | -| Unreal2 | Games | Yes | [Node-GameDig Source](https://github.com/gamedig/node-gamedig) | Sometimes servers send strings that node-gamedig would treat as latin1 that are UTF-8 encoded, when this happens the remove color code breaks because latin1 decodes the colour sequences differently. Some games provide additional info at the end of the server info packet, this is not currently handled (see the node implementation). Some games use a bot player to denote the team names, this is not currently handled. | +| Name | For | Proprietary? | Documentation reference | Notes | +|----------------|-------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Valve Protocol | Games | No | [Server Queries](https://developer.valvesoftware.com/wiki/Server_queries) | In some cases, the players details query might contain some 0-length named players. Multi-packet decompression not tested. | +| Minecraft | Games | Yes | Java: [List Server Protocol](https://wiki.vg/Server_List_Ping)
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. | +| Savage 2 | Games | Yes | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/savage2.js) | | ## Planned to add support: _ diff --git a/RESPONSES.md b/RESPONSES.md index 14415fe..a9c3174 100644 --- a/RESPONSES.md +++ b/RESPONSES.md @@ -5,53 +5,57 @@ In the case that a field that performs the same function exists in the current c # Response table -| Field | Generic | GameSpy(1) | GameSpy(2) | GameSpy(3) | Minecraft(Java) | Minecraft(Bedrock) | Valve | Quake | Unreal2 | Proprietary: FFOW | Proprietary: TheShip | Proprietary: JC2MP | -|----------------------|------------------|---------------------------|---------------|---------------------------|-----------------------|--------------------|-----------------------------------|---------------------------|--------------------------------|-------------------|---------------------------|--------------------| -| name | `Option` | `String` | `String` | `String` | | `String` | `String` | `String` | `String` | `String` | `String` | `String` | -| 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` | | | | +| Field | Generic | GameSpy(1) | GameSpy(2) | GameSpy(3) | Minecraft(Java) | Minecraft(Bedrock) | Valve | Quake | Unreal2 | Proprietary: FFOW | Proprietary: TheShip | Proprietary: JC2MP | Proprietary: Savage 2 | +|----------------------|----------|------------|------------|------------|-----------------|--------------------|---------------|-----------|------------|-------------------|----------------------|--------------------|-----------------------| +| name | `Option` | `String` | `String` | `String` | | `String` | `String` | `String` | `String` | `String` | `String` | `String` | `String` | +| description | `Option` | | | | `String` | | | | | `String` | | `String` | | +| game_mode | `Option` | `String` | | `String` | | `Option` | `String` | | `String` | `String` | `String` | | `String` | +| game_version | `Option` | `String` | | `String` | `String` | | `String` | `String` | | `String` | `String` | `String` | | +| map | `Option` | `String` | `String` | `String` | | `Option` | `String` | `String` | `String` | `String` | `String` | | `String` | +| players_maxmimum | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u32` | `u8` | `u8` | `u32` | `u8` | +| players_online | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u32` | `u8` | `u8` | `u32` | `u8` | +| players_bots | `Option` | | | | | | `u8` | | | | `u8` | | | +| has_password | `Option` | `bool` | `bool` | `bool` | | | `bool` | | | `bool` | `bool` | `bool` | | +| players_minimum | | `Option` | `Option` | `Option` | | | | | | | | | `u8` | +| players | | `Vec` | `Vec` | `Vec` | `Option>` | | `Option>` | `Vec ` | `Vec` | | `Vec` | `Vec` | | +| tournament | | `bool` | | `bool` | | | | | | | | | | +| unused_entries | | `Hashmap` | | `HashMap` | | | | `HashMap` | | | | | | +| teams | | | `Vec` | `Vec` | | | | | | | | | | +| protocol_version | | | | | `i32` | `String` | `u8` | | | `u8` | `u8` | | `String` | +| server_type | | | | | `Server` | `Server` | `Server` | | | | `Server` | | | +| rules | | | | | | | `Option>` | | `HashMap>` | | `HashMap` | | | +| environment_type | | | | | | | `Environment` | | | `Environment` | | | | +| vac_secured | | | | | | | `bool` | | | `bool` | `bool` | | | +| map_title | | `Option` | | | | | | | | | | | | +| admin_contact | | `Option` | | | | | | | | | | | | +| admin_name | | `Option` | | | | | | | | | | | | +| favicon | | | | | `Option` | | | | | | | | | +| previews_chat | | | | | `Option` | | | | | | | | | +| enforces_secure_chat | | | | | `Option` | | | | | | | | | +| edition | | | | | | `String` | | | | | | | | +| id | | | | | | `String` | | | `String` | | | | | +| the_ship | | | | | | | `Option` | | | | | | | +| is_mod | | | | | | | `bool` | | | | | | | +| extra_data | | | | | | | `Option` | | | | | | | +| mod_data | | | | | | | `Option` | | | | | | | +| folder | | | | | | | `String` | | | | | | | +| appid | | | | | | | `u32` | | | | | | | +| active_mod | | | | | | | | | | `String` | | | | +| round | | | | | | | | | | `u8` | | | | +| rounds_maximum | | | | | | | | | | `u8` | | | | +| time_left | | | | | | | | | | `u16` | | | | +| port | | | | | | | | | `u32` | | `Option` | | | +| steam_id | | | | | | | | | | | `Option` | | | +| tv_port | | | | | | | | | | | `Option` | | | +| tv_name | | | | | | | | | | | `Option` | | | +| keywords | | | | | | | | | | | `Option` | | | +| mode | | | | | | | | | | | `u8` | | | +| witnesses | | | | | | | | | | | `u8` | | | +| duration | | | | | | | | | | | `u8` | | | +| query_port | | | | | | | | | `u32` | | | | | +| ip | | | | | | | | | `String` | | | | | +| mutators | | | | | | | | | `HashSet` | | | | | +| next_map | | | | | | | | | | | | | `String` | +| location | | | | | | | | | | | | | `String` | +| level_minimum | | | | | | | | | | | | | `String` | +| time | | | | | | | | | | | | | `String` | diff --git a/crates/lib/src/games/definitions.rs b/crates/lib/src/games/definitions.rs index 18e033b..e157f99 100644 --- a/crates/lib/src/games/definitions.rs +++ b/crates/lib/src/games/definitions.rs @@ -88,6 +88,7 @@ pub static GAMES: Map<&'static str, Game> = phf_map! { "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))), + "savage2" => game!("Savage 2", 11235, Protocol::PROPRIETARY(ProprietaryProtocol::Savage2)), "sco" => game!("Sven Co-op", 27015, Protocol::Valve(Engine::new_gold_src(false))), "sdtd" => game!("7 Days to Die", 26900, Protocol::Valve(Engine::new(251_570))), "sof2" => game!("Soldier of Fortune 2", 20100, Protocol::Quake(QuakeVersion::Three)), diff --git a/crates/lib/src/games/mod.rs b/crates/lib/src/games/mod.rs index e8e5641..add1f19 100644 --- a/crates/lib/src/games/mod.rs +++ b/crates/lib/src/games/mod.rs @@ -21,6 +21,8 @@ pub mod ffow; pub mod jc2m; /// Minecraft pub mod minecraft; +/// Savage 2 +pub mod savage2; /// The Ship pub mod theship; @@ -117,6 +119,9 @@ pub fn query_with_timeout_and_extra_settings( } Protocol::PROPRIETARY(protocol) => { match protocol { + ProprietaryProtocol::Savage2 => { + savage2::query_with_timeout(address, port, timeout_settings).map(Box::new)? + } ProprietaryProtocol::TheShip => { theship::query_with_timeout(address, port, timeout_settings).map(Box::new)? } diff --git a/crates/lib/src/games/savage2/mod.rs b/crates/lib/src/games/savage2/mod.rs new file mode 100644 index 0000000..88d57f2 --- /dev/null +++ b/crates/lib/src/games/savage2/mod.rs @@ -0,0 +1,8 @@ +/// The implementation. +/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/savage2.js) +pub mod protocol; +/// All types used by the implementation. +pub mod types; + +pub use protocol::*; +pub use types::*; diff --git a/crates/lib/src/games/savage2/protocol.rs b/crates/lib/src/games/savage2/protocol.rs new file mode 100644 index 0000000..dbb1eb8 --- /dev/null +++ b/crates/lib/src/games/savage2/protocol.rs @@ -0,0 +1,37 @@ +use crate::buffer::{Buffer, Utf8Decoder}; +use crate::games::savage2::types::Response; +use crate::protocols::types::TimeoutSettings; +use crate::socket::{Socket, UdpSocket}; +use crate::GDResult; +use byteorder::LittleEndian; +use std::net::{IpAddr, SocketAddr}; + +pub fn query(address: &IpAddr, port: Option) -> GDResult { query_with_timeout(address, port, None) } + +pub fn query_with_timeout( + address: &IpAddr, + port: Option, + timeout_settings: Option, +) -> GDResult { + let addr = &SocketAddr::new(*address, port.unwrap_or(11235)); + let mut socket = UdpSocket::new(addr, &timeout_settings)?; + socket.send(&[0x01])?; + let data = socket.receive(None)?; + let mut buffer = Buffer::::new(&data); + + buffer.move_cursor(12)?; + + Ok(Response { + name: buffer.read_string::(None)?, + players_online: buffer.read::()?, + players_maximum: buffer.read::()?, + time: buffer.read_string::(None)?, + map: buffer.read_string::(None)?, + next_map: buffer.read_string::(None)?, + location: buffer.read_string::(None)?, + players_minimum: buffer.read::()?, + game_mode: buffer.read_string::(None)?, + protocol_version: buffer.read_string::(None)?, + level_minimum: buffer.read::()?, + }) +} diff --git a/crates/lib/src/games/savage2/types.rs b/crates/lib/src/games/savage2/types.rs new file mode 100644 index 0000000..d72068b --- /dev/null +++ b/crates/lib/src/games/savage2/types.rs @@ -0,0 +1,30 @@ +use crate::protocols::types::CommonResponse; +use crate::protocols::GenericResponse; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq)] +pub struct Response { + pub name: String, + pub players_online: u8, + pub players_maximum: u8, + pub players_minimum: u8, + pub time: String, + pub map: String, + pub next_map: String, + pub location: String, + pub game_mode: String, + pub protocol_version: String, + pub level_minimum: u8, +} + +impl CommonResponse for Response { + fn as_original(&self) -> GenericResponse { GenericResponse::Savage2(self) } + + fn name(&self) -> Option<&str> { Some(&self.name) } + fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) } + fn map(&self) -> Option<&str> { Some(&self.map) } + fn players_maximum(&self) -> u32 { self.players_maximum.into() } + fn players_online(&self) -> u32 { self.players_online.into() } +} diff --git a/crates/lib/src/protocols/types.rs b/crates/lib/src/protocols/types.rs index 4249d5c..9bd4b53 100644 --- a/crates/lib/src/protocols/types.rs +++ b/crates/lib/src/protocols/types.rs @@ -18,6 +18,7 @@ pub enum ProprietaryProtocol { Minecraft(Option), FFOW, JC2M, + Savage2, } /// Enumeration of all valid protocol types @@ -48,6 +49,8 @@ pub enum GenericResponse<'a> { FFOW(&'a crate::games::ffow::Response), #[cfg(feature = "games")] JC2M(&'a crate::games::jc2m::Response), + #[cfg(feature = "games")] + Savage2(&'a crate::games::savage2::Response), } /// All player types