diff --git a/Cargo.toml b/Cargo.toml index 4affa63..ec1a6ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,10 @@ keywords = ["server", "query", "game", "check", "status"] rust-version = "1.60.0" [features] -default = [] +default = ["game_defs"] no_games = [] no_services = [] +game_defs = ["dep:phf"] serde = ["dep:serde", "serde/derive"] [dependencies] @@ -29,3 +30,6 @@ crc32fast = "1.3.2" serde_json = "1.0.91" serde = { version = "1.0.155", optional = true } + +phf = { version = "0.11", optional = true, features = ["macros"] } + diff --git a/RESPONSES.md b/RESPONSES.md new file mode 100644 index 0000000..6f60413 --- /dev/null +++ b/RESPONSES.md @@ -0,0 +1,54 @@ +Every protocol has its own response type(s), below is a listing of the overlapping fields on these responses. + +If a cell is blank it doesn't exist, otherwise it contains the type of that data in the current column's response type. +In the case that a field that performs the same function exists in the current column's response type that name is annotated in brackets. + +# Response table + +| Field | Generic | GameSpy(1) | GameSpy(3) | Minecraft(Java) | Minecraft(Bedrock) | Valve | Quake | FFOW | TheShip | +| :--------------- | -------------- | ---------- | ---------- | --------------- | ------------------ | ------ | ------ | ------ | ------- | +| name | Option | String | String | | String | String | String | String | String | +| description | Option | | | String | | | | String | | +| game | Option | String (game_type) | String (game_type) | | Option (game_mode) | String | | String (game_mode) | String | +| game_version | Option | String | String | String (version_name) | | String (version) | String (version) | String (version) | String (version) | +| map | Option | String | String | | Option | String | String | String | String | +| players_maxmimum | u64 | usize | usize | u32 | u32 | u8 | u8 | u8 | u8 (max_players) | +| players_online | u64 | usize | usize | u32 | u32 | u8 | u8 | u8 | u8 (players) | +| players_bots | Option | | | | | u8 | | | u8 (bots) | +| has_password | Option | bool | bool | | | bool | | bool | bool | +| map_title | | Option | | | | | | | | +| admin_contact | | Option | | | | | | | | +| admin_name | | Option | | | | | | | | +| players_minimum | | Option | Option | | | | | | | +| players | | Vec | Vec | | | Option> | Vec

| | Vec (player_details) | +| tournament | | bool | bool | | | | | | | +| unused_entries | | Hashmap | HashMap | | | Option (extra_data) | HashMap | | | +| teams | | | Vec | | | | | | | +| version_protocol | | | | i32 | String | u8 (protocol) | | u8 (protocol) | u8 (protocol) | +| players_sample | | | | Option> | | | | | | +| favicon | | | | Option | | | | | | +| previews_chat | | | | Option | | | | | | +| enforces_secure_chat | | | | Option | | | | | | +| server_type | | | | Server | Server | Server | | | Server | +| edition | | | | | String | | | | | +| id | | | | | String | | | | | +| rules | | | | | | Option> | | | HashMap | +| folder | | | | | | String | | | | +| appid | | | | | | u32 | | | | +| environment_type | | | | | | Environment | | Environment | | +| vac_secured | | | | | | bool | | bool | bool | +| the_ship | | | | | | Option | | | | +| is_mod | | | | | | bool | | | | +| mod_data | | | | | | Option | | | | +| 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 | diff --git a/examples/generic.rs b/examples/generic.rs new file mode 100644 index 0000000..7a42aa3 --- /dev/null +++ b/examples/generic.rs @@ -0,0 +1,61 @@ +use gamedig::{protocols::GenericResponse, query, GDResult, GAMES}; + +use std::net::IpAddr; + +fn generic_query(game_name: &str, addr: &IpAddr, port: Option) -> GDResult { + let game = GAMES.get(game_name).expect("Game doesn't exist"); + + println!("Querying {:?} with {:?}", addr, game); + + let response = query(game, addr, port)?; + + println!("{:?}", response); + + Ok(response) +} + +fn main() { + let mut args = std::env::args().skip(1); + + let game_name = args.next().expect("Must provide a game name"); + let addr: IpAddr = args + .next() + .map(|s| s.parse().unwrap()) + .expect("Must provide address"); + let port: Option = args.next().map(|s| s.parse().unwrap()); + + generic_query(&game_name, &addr, port).unwrap(); +} + +#[cfg(test)] +mod test { + use gamedig::GAMES; + use std::net::{IpAddr, Ipv4Addr}; + + use super::generic_query; + + const ADDR: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST); + + fn test_game(game_name: &str) { + assert!(generic_query(game_name, &ADDR, None).is_err()); + } + + #[test] + fn battlefield() { test_game("bf1942"); } + + #[test] + fn minecraft() { test_game("mc"); } + + #[test] + fn tf2() { test_game("tf2"); } + + #[test] + fn quake() { test_game("quake3a"); } + + #[test] + fn all_games() { + for game_name in GAMES.keys() { + test_game(game_name); + } + } +} diff --git a/src/games/definitions.rs b/src/games/definitions.rs new file mode 100644 index 0000000..1c199f1 --- /dev/null +++ b/src/games/definitions.rs @@ -0,0 +1,83 @@ +//! Static definitions of currently supported games + +use crate::protocols::{ + gamespy::GameSpyVersion, + minecraft::{LegacyGroup, Server}, + quake::QuakeVersion, + valve::SteamApp, + Protocol, +}; +use crate::Game; + +use phf::{phf_map, Map}; + +macro_rules! game { + ($name: literal, $default_port: literal, $protocol: expr) => { + Game { + name: $name, + default_port: $default_port, + protocol: $protocol, + } + }; +} + +/// Map of all currently supported games +pub static GAMES: Map<&'static str, Game> = phf_map! { + "mc" => game!("Minecraft", 25565, Protocol::Minecraft(None)), + "mc-java" => game!("Minecraft (java)", 25565, Protocol::Minecraft(Some(Server::Java))), + "mc-bedrock" => game!("Minecraft (bedrock)", 19132, Protocol::Minecraft(Some(Server::Bedrock))), + "mc-legacy-1.6" => game!("Minecraft (legacy v1.6)", 25565, Protocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_6)))), + "mc-legacy-1.4" => game!("Minecraft (legacy v1.4-1.5)", 25565, Protocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_4)))), + "mc-legacy-b1.8" => game!("Minecraft (legacy vB1.8-1.3)", 25565, Protocol::Minecraft(Some(Server::Legacy(LegacyGroup::VB1_8)))), + "aliens" => game!("Alien Swarm", 27015, Protocol::Valve(SteamApp::ALIENS)), + "aoc" => game!("Age of Chivalry", 27015, Protocol::Valve(SteamApp::AOC)), + "arma2oa" => game!("ARMA 2: Operation Arrowhead", 2304, Protocol::Valve(SteamApp::ARMA2OA)), + "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)), + "bat1944" => game!("Battalion 1944", 7780, Protocol::Valve(SteamApp::BAT1944)), + "bb2" => game!("BrainBread 2", 27015, Protocol::Valve(SteamApp::BB2)), + "bf1942" => game!("Battlefield 1942", 23000, Protocol::Gamespy(GameSpyVersion::One)), + "bm" => game!("Black Mesa", 27015, Protocol::Valve(SteamApp::BM)), + "bo" => game!("Ballistic Overkill", 27016, Protocol::Valve(SteamApp::BO)), + "ccure" => game!("Codename CURE", 27015, Protocol::Valve(SteamApp::CCURE)), + "cosu" => game!("Colony Survival", 27004, Protocol::Valve(SteamApp::COSU)), + "cs" => game!("Counter-Strike", 27015, Protocol::Valve(SteamApp::CS)), + "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)), + "cw" => 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)), + "ffow" => game!("Frontlines: Fuel of War", 5478, Protocol::FFOW), + "gm" => game!("Garry's Mod", 27016, Protocol::Valve(SteamApp::GM)), + "hl2dm" => game!("Half-Life 2 Deathmatch", 27015, Protocol::Valve(SteamApp::HL2DM)), + "hldms" => game!("Half-Life Deathmatch: Source", 27015, Protocol::Valve(SteamApp::HLDMS)), + "hll" => game!("Hell Let Loose", 26420, Protocol::Valve(SteamApp::HLL)), + "ins" => game!("Insurgency", 27015, Protocol::Valve(SteamApp::INS)), + "insmic" => game!("Insurgency: Modern Infantry Combat", 27015, Protocol::Valve(SteamApp::INSMIC)), + "inss" => game!("Insurgency: Sandstorm", 27131, Protocol::Valve(SteamApp::INSS)), + "l4d" => game!("Left 4 Dead", 27015, Protocol::Valve(SteamApp::L4D)), + "l4d2" => game!("Left 4 Dead 2", 27015, Protocol::Valve(SteamApp::L4D2)), + "ohd" => game!("Operation: Harsh Doorstop", 27005, Protocol::Valve(SteamApp::OHD)), + "onset" => game!("Onset", 7776, Protocol::Valve(SteamApp::ONSET)), + "pz" => game!("Project Zomboid", 16261, Protocol::Valve(SteamApp::PZ)), + "quake1" => game!("Quake 1", 27500, Protocol::Quake(QuakeVersion::One)), + "quake2" => game!("Quake 2", 27910, Protocol::Quake(QuakeVersion::Two)), + "quake3a" => 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)), + "sc" => game!("Sven Co-op", 27015, Protocol::Valve(SteamApp::SC)), + "sdtd" => game!("7 Days To Die", 26900, Protocol::Valve(SteamApp::SDTD)), + "sof2" => game!("Soldier of Fortune 2", 20100, Protocol::Quake(QuakeVersion::Three)), + "ss" => game!("Serious Sam", 25601, Protocol::Gamespy(GameSpyVersion::One)), + "tf" => game!("The Forest", 27016, Protocol::Valve(SteamApp::TF)), + "tf2" => game!("Team Fortress 2", 27015, Protocol::Valve(SteamApp::TF2)), + "tfc" => game!("Team Fortress Classic", 27015, Protocol::Valve(SteamApp::TFC)), + "ts" => game!("The Ship", 27015, Protocol::TheShip), + "unturned" => game!("Unturned", 27015, Protocol::Valve(SteamApp::UNTURNED)), + "ut" => game!("Unreal Tournament", 7778, Protocol::Gamespy(GameSpyVersion::One)), + "vr" => game!("V Rising", 27016, Protocol::Valve(SteamApp::VR)), +}; diff --git a/src/games/ffow.rs b/src/games/ffow.rs index 06239cd..e8d6af9 100644 --- a/src/games/ffow.rs +++ b/src/games/ffow.rs @@ -1,5 +1,6 @@ -use crate::protocols::types::TimeoutSettings; +use crate::protocols::types::{SpecificResponse, TimeoutSettings}; use crate::protocols::valve::{Engine, Environment, Server, ValveProtocol}; +use crate::protocols::GenericResponse; use crate::GDResult; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -43,6 +44,53 @@ pub struct Response { pub time_left: u16, } +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct ExtraResponse { + /// Protocol used by the server. + pub protocol: u8, + /// Map name. + pub active_mod: String, + /// Dedicated, NonDedicated or SourceTV + pub server_type: Server, + /// The Operating System that the server is on. + pub environment_type: Environment, + /// Indicates whether the server uses VAC. + pub vac_secured: bool, + /// Current round index. + pub round: u8, + /// Maximum amount of rounds. + pub rounds_maximum: u8, + /// Time left for the current round in seconds. + pub time_left: u16, +} + +impl From for GenericResponse { + fn from(r: Response) -> Self { + Self { + name: Some(r.name), + description: Some(r.description), + game: Some(r.game_mode), + game_version: Some(r.version), + map: Some(r.map), + players_maximum: r.players_maximum.into(), + players_online: r.players_online.into(), + players_bots: None, + has_password: Some(r.has_password), + inner: SpecificResponse::FFOW(ExtraResponse { + protocol: r.protocol, + active_mod: r.active_mod, + server_type: r.server_type, + environment_type: r.environment_type, + vac_secured: r.vac_secured, + round: r.round, + rounds_maximum: r.rounds_maximum, + time_left: r.time_left, + }), + } + } +} + pub fn query(address: &IpAddr, port: Option) -> GDResult { query_with_timeout(address, port, TimeoutSettings::default()) } diff --git a/src/games/mod.rs b/src/games/mod.rs index 068492c..fa68e5e 100644 --- a/src/games/mod.rs +++ b/src/games/mod.rs @@ -1,5 +1,8 @@ //! Currently supported games. +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + /// Alien Swarm pub mod aliens; /// Age of Chivalry @@ -106,3 +109,61 @@ pub mod unturned; pub mod ut; /// V Rising pub mod vr; + +use crate::protocols::gamespy::GameSpyVersion; +use crate::protocols::quake::QuakeVersion; +use crate::protocols::{self, Protocol}; +use crate::GDResult; +use std::net::{IpAddr, SocketAddr}; + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq)] +pub struct Game { + pub name: &'static str, + pub default_port: u16, + pub protocol: Protocol, +} + +#[cfg(feature = "game_defs")] +mod definitions; + +#[cfg(feature = "game_defs")] +pub use definitions::GAMES; + +pub fn query(game: &Game, address: &IpAddr, port: Option) -> GDResult { + let socket_addr = SocketAddr::new(*address, port.unwrap_or(game.default_port)); + Ok(match &game.protocol { + Protocol::Valve(steam_app) => { + protocols::valve::query(&socket_addr, steam_app.as_engine(), None, None).map(|r| r.into())? + } + Protocol::Minecraft(version) => { + match version { + Some(protocols::minecraft::Server::Java) => { + protocols::minecraft::query_java(&socket_addr, None).map(|r| r.into())? + } + Some(protocols::minecraft::Server::Bedrock) => { + protocols::minecraft::query_bedrock(&socket_addr, None).map(|r| r.into())? + } + Some(protocols::minecraft::Server::Legacy(group)) => { + protocols::minecraft::query_legacy_specific(*group, &socket_addr, None).map(|r| r.into())? + } + None => protocols::minecraft::query(&socket_addr, None).map(|r| r.into())?, + } + } + Protocol::Gamespy(version) => { + match version { + GameSpyVersion::One => protocols::gamespy::one::query(&socket_addr, None).map(|r| r.into())?, + GameSpyVersion::Three => protocols::gamespy::three::query(&socket_addr, None).map(|r| r.into())?, + } + } + Protocol::Quake(version) => { + match version { + QuakeVersion::One => protocols::quake::one::query(&socket_addr, None).map(|r| r.into())?, + QuakeVersion::Two => protocols::quake::two::query(&socket_addr, None).map(|r| r.into())?, + QuakeVersion::Three => protocols::quake::three::query(&socket_addr, None).map(|r| r.into())?, + } + } + Protocol::TheShip => ts::query(address, port).map(|r| r.into())?, + Protocol::FFOW => ffow::query(address, port).map(|r| r.into())?, + }) +} diff --git a/src/games/ts.rs b/src/games/ts.rs index 089c0a2..d69f1a1 100644 --- a/src/games/ts.rs +++ b/src/games/ts.rs @@ -1,5 +1,9 @@ use crate::{ - protocols::valve::{self, get_optional_extracted_data, Server, ServerPlayer, SteamApp}, + protocols::{ + types::SpecificResponse, + valve::{self, get_optional_extracted_data, Server, ServerPlayer, SteamApp}, + GenericResponse, + }, GDResult, }; use std::net::{IpAddr, SocketAddr}; @@ -57,6 +61,55 @@ pub struct Response { pub duration: u8, } +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq)] +pub struct ExtraResponse { + pub protocol: u8, + pub player_details: Vec, + pub server_type: Server, + pub vac_secured: bool, + pub port: Option, + pub steam_id: Option, + pub tv_port: Option, + pub tv_name: Option, + pub keywords: Option, + pub rules: HashMap, + pub mode: u8, + pub witnesses: u8, + pub duration: u8, +} + +impl From for GenericResponse { + fn from(r: Response) -> Self { + Self { + name: Some(r.name), + description: None, + game: Some(r.game), + game_version: Some(r.version), + map: Some(r.map), + players_maximum: r.max_players.into(), + players_online: r.players.into(), + players_bots: Some(r.bots.into()), + has_password: Some(r.has_password), + inner: SpecificResponse::TheShip(ExtraResponse { + protocol: r.protocol, + player_details: r.players_details, + server_type: r.server_type, + vac_secured: r.vac_secured, + steam_id: r.steam_id, + port: r.port, + tv_port: r.tv_port, + tv_name: r.tv_name, + keywords: r.keywords, + rules: r.rules, + mode: r.mode, + witnesses: r.witnesses, + duration: r.duration, + }), + } + } +} + impl Response { pub fn new_from_valve_response(response: valve::Response) -> Self { let (port, steam_id, tv_port, tv_name, keywords) = get_optional_extracted_data(response.info.extra_data); diff --git a/src/protocols/gamespy/mod.rs b/src/protocols/gamespy/mod.rs index 1f5150f..be8e952 100644 --- a/src/protocols/gamespy/mod.rs +++ b/src/protocols/gamespy/mod.rs @@ -1,5 +1,22 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + mod common; /// The implementations. pub mod protocols; pub use protocols::*; + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq)] +pub enum GameSpyVersion { + One, + Three, +} + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq)] +pub enum VersionedExtraResponse { + One(protocols::one::ExtraResponse), + Three(protocols::three::ExtraResponse), +} diff --git a/src/protocols/gamespy/protocols/one/types.rs b/src/protocols/gamespy/protocols/one/types.rs index 7f328dd..5909490 100644 --- a/src/protocols/gamespy/protocols/one/types.rs +++ b/src/protocols/gamespy/protocols/one/types.rs @@ -3,6 +3,9 @@ use std::collections::HashMap; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use crate::protocols::gamespy::VersionedExtraResponse; +use crate::protocols::{types::SpecificResponse, GenericResponse}; + /// A player’s details. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -39,3 +42,41 @@ pub struct Response { pub tournament: bool, pub unused_entries: HashMap, } + +/// Non-generic query response +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExtraResponse { + pub map_title: Option, + pub admin_contact: Option, + pub admin_name: Option, + pub players_minimum: Option, + pub tournament: bool, + pub unused_entries: HashMap, + pub players: Vec, +} + +impl From for GenericResponse { + fn from(r: Response) -> Self { + Self { + name: Some(r.name), + description: None, + game: Some(r.game_type), + game_version: Some(r.game_version), + map: Some(r.map), + players_maximum: r.players_maximum.try_into().unwrap(), // FIXME: usize to u64 may fail + players_online: r.players_online.try_into().unwrap(), + players_bots: None, + has_password: Some(r.has_password), + inner: SpecificResponse::Gamespy(VersionedExtraResponse::One(ExtraResponse { + map_title: r.map_title, + admin_contact: r.admin_contact, + admin_name: r.admin_name, + players_minimum: r.players_minimum, + tournament: r.tournament, + unused_entries: r.unused_entries, + players: r.players, + })), + } + } +} diff --git a/src/protocols/gamespy/protocols/three/types.rs b/src/protocols/gamespy/protocols/three/types.rs index 164b19a..dd4adf1 100644 --- a/src/protocols/gamespy/protocols/three/types.rs +++ b/src/protocols/gamespy/protocols/three/types.rs @@ -1,3 +1,5 @@ +use crate::protocols::gamespy::VersionedExtraResponse; +use crate::protocols::{types::SpecificResponse, GenericResponse}; use std::collections::HashMap; #[cfg(feature = "serde")] @@ -40,3 +42,37 @@ pub struct Response { pub tournament: bool, pub unused_entries: HashMap, } + +/// Non-generic query response +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExtraResponse { + pub players_minimum: Option, + pub teams: Vec, + pub tournament: bool, + pub unused_entries: HashMap, + pub players: Vec, +} + +impl From for GenericResponse { + fn from(r: Response) -> Self { + Self { + name: Some(r.name), + description: None, + game: Some(r.game_type), + game_version: Some(r.game_version), + map: Some(r.map), + players_maximum: r.players_maximum.try_into().unwrap(), // FIXME: usize to u64 may fail + players_online: r.players_online.try_into().unwrap(), + players_bots: None, + has_password: Some(r.has_password), + inner: SpecificResponse::Gamespy(VersionedExtraResponse::Three(ExtraResponse { + players_minimum: r.players_minimum, + teams: r.teams, + tournament: r.tournament, + unused_entries: r.unused_entries, + players: r.players, + })), + } + } +} diff --git a/src/protocols/minecraft/types.rs b/src/protocols/minecraft/types.rs index 8b22581..7bff98c 100644 --- a/src/protocols/minecraft/types.rs +++ b/src/protocols/minecraft/types.rs @@ -4,6 +4,7 @@ use crate::{ bufferer::Bufferer, + protocols::{types::SpecificResponse, GenericResponse}, GDError::{PacketBad, UnknownEnumCast}, GDResult, }; @@ -43,6 +44,13 @@ pub struct Player { pub id: String, } +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum VersionedExtraResponse { + Bedrock(BedrockExtraResponse), + Java(JavaExtraResponse), +} + /// A Java query response. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -70,6 +78,48 @@ pub struct JavaResponse { pub server_type: Server, } +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct JavaExtraResponse { + /// Version protocol, example: 760 (for 1.19.2). Note that for versions + /// below 1.6 this field is always -1. + pub version_protocol: i32, + /// Some online players (can be missing). + pub players_sample: Option>, + /// The favicon (can be missing). + pub favicon: Option, + /// Tells if the chat preview is enabled (can be missing). + pub previews_chat: Option, + /// Tells if secure chat is enforced (can be missing). + pub enforces_secure_chat: Option, + /// Tell's the server type. + pub server_type: Server, +} + +impl From for GenericResponse { + fn from(r: JavaResponse) -> Self { + Self { + name: None, + description: Some(r.description), + game: Some(String::from("Minecraft")), + game_version: Some(r.version_name), + map: None, + players_maximum: r.players_maximum.into(), + players_online: r.players_online.into(), + players_bots: None, + has_password: None, + inner: SpecificResponse::Minecraft(VersionedExtraResponse::Java(JavaExtraResponse { + version_protocol: r.version_protocol, + players_sample: r.players_sample, + favicon: r.favicon, + previews_chat: r.previews_chat, + enforces_secure_chat: r.enforces_secure_chat, + server_type: r.server_type, + })), + } + } +} + /// A Bedrock Edition query response. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -96,6 +146,44 @@ pub struct BedrockResponse { pub server_type: Server, } +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct BedrockExtraResponse { + /// Server's edition. + pub edition: String, + /// Version protocol, example: 760 (for 1.19.2). + pub version_protocol: String, + /// Server id. + pub id: Option, + /// Current game mode. + pub game_mode: Option, + /// Tells the server type. + pub server_type: Server, +} + +impl From for GenericResponse { + fn from(r: BedrockResponse) -> Self { + Self { + name: Some(r.name), + description: None, + game: None, + game_version: Some(r.version_name), + map: r.map, + players_maximum: r.players_maximum.into(), + players_online: r.players_online.into(), + players_bots: None, + has_password: None, + inner: SpecificResponse::Minecraft(VersionedExtraResponse::Bedrock(BedrockExtraResponse { + edition: r.edition, + version_protocol: r.version_protocol, + id: r.id, + game_mode: r.game_mode, + server_type: r.server_type, + })), + } + } +} + impl JavaResponse { pub fn from_bedrock_response(response: BedrockResponse) -> Self { Self { diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs index 6e3f288..efb820a 100644 --- a/src/protocols/mod.rs +++ b/src/protocols/mod.rs @@ -14,3 +14,5 @@ pub mod quake; pub mod types; /// Reference: [Server Query](https://developer.valvesoftware.com/wiki/Server_queries) pub mod valve; + +pub use types::{GenericResponse, Protocol}; diff --git a/src/protocols/quake/mod.rs b/src/protocols/quake/mod.rs index e20afcc..a893cf7 100644 --- a/src/protocols/quake/mod.rs +++ b/src/protocols/quake/mod.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + pub mod one; pub mod three; pub mod two; @@ -7,3 +10,11 @@ pub mod types; pub use types::*; mod client; + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq)] +pub enum QuakeVersion { + One, + Two, + Three, +} diff --git a/src/protocols/quake/types.rs b/src/protocols/quake/types.rs index 2f8c145..15f47c7 100644 --- a/src/protocols/quake/types.rs +++ b/src/protocols/quake/types.rs @@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use crate::protocols::{types::SpecificResponse, GenericResponse}; + /// General server information's. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq)] @@ -21,3 +23,30 @@ pub struct Response

{ /// Other server entries that weren't used. pub unused_entries: HashMap, } + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExtraResponse { + /// Other server entries that weren't used. + pub unused_entries: HashMap, +} + +impl From> for GenericResponse { + fn from(r: Response) -> Self { + Self { + name: Some(r.name), + description: None, + game: None, + game_version: Some(r.version), + map: Some(r.map), + players_maximum: r.players_maximum.into(), + players_online: r.players_online.into(), + players_bots: None, + has_password: None, + inner: SpecificResponse::Quake(ExtraResponse { + // TODO: Players + unused_entries: r.unused_entries, + }), + } + } +} diff --git a/src/protocols/types.rs b/src/protocols/types.rs index 0f98f46..580be63 100644 --- a/src/protocols/types.rs +++ b/src/protocols/types.rs @@ -1,98 +1,154 @@ -use crate::{GDError::InvalidInput, GDResult}; - -use std::time::Duration; - -/// Timeout settings for socket operations -#[derive(Clone, Debug)] -pub struct TimeoutSettings { - read: Option, - write: Option, -} - -impl TimeoutSettings { - /// Construct new settings, passing None will block indefinitely. Passing - /// zero Duration throws GDError::[InvalidInput](InvalidInput). - pub fn new(read: Option, write: Option) -> GDResult { - if let Some(read_duration) = read { - if read_duration == Duration::new(0, 0) { - return Err(InvalidInput); - } - } - - if let Some(write_duration) = write { - if write_duration == Duration::new(0, 0) { - return Err(InvalidInput); - } - } - - Ok(Self { read, write }) - } - - /// Get the read timeout. - pub fn get_read(&self) -> Option { self.read } - - /// Get the write timeout. - pub fn get_write(&self) -> Option { self.write } -} - -impl Default for TimeoutSettings { - /// Default values are 4 seconds for both read and write. - fn default() -> Self { - Self { - read: Some(Duration::from_secs(4)), - write: Some(Duration::from_secs(4)), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::time::Duration; - - // Test creating new TimeoutSettings with valid durations - #[test] - fn test_new_with_valid_durations() -> GDResult<()> { - // Define valid read and write durations - let read_duration = Duration::from_secs(1); - let write_duration = Duration::from_secs(2); - - // Create new TimeoutSettings with the valid durations - let timeout_settings = TimeoutSettings::new(Some(read_duration), Some(write_duration))?; - - // Verify that the get_read and get_write methods return the expected values - assert_eq!(timeout_settings.get_read(), Some(read_duration)); - assert_eq!(timeout_settings.get_write(), Some(write_duration)); - - Ok(()) - } - - // Test creating new TimeoutSettings with a zero duration - #[test] - fn test_new_with_zero_duration() { - // Define a zero read duration and a valid write duration - let read_duration = Duration::new(0, 0); - let write_duration = Duration::from_secs(2); - - // Try to create new TimeoutSettings with the zero read duration (this should - // fail) - let result = TimeoutSettings::new(Some(read_duration), Some(write_duration)); - - // Verify that the function returned an error and that the error type is - // InvalidInput - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), InvalidInput); - } - - // Test that the default TimeoutSettings values are correct - #[test] - fn test_default_values() { - // Get the default TimeoutSettings values - let default_settings = TimeoutSettings::default(); - - // Verify that the get_read and get_write methods return the expected default - // values - assert_eq!(default_settings.get_read(), Some(Duration::from_secs(4))); - assert_eq!(default_settings.get_write(), Some(Duration::from_secs(4))); - } -} +use crate::protocols::{gamespy, minecraft, quake, valve}; +use crate::{GDError::InvalidInput, GDResult}; + +use std::time::Duration; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Enumeration of all valid protocol types +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq)] +pub enum Protocol { + Gamespy(gamespy::GameSpyVersion), + Minecraft(Option), + Quake(quake::QuakeVersion), + Valve(valve::SteamApp), + TheShip, + FFOW, +} + +/// A generic version of a response +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq)] +pub struct GenericResponse { + /// The name of the server + pub name: Option, + /// Description of the server + pub description: Option, + /// Name of the current game or game mode + pub game: Option, + /// Version of the game being run on the server + pub game_version: Option, + /// The current map name + pub map: Option, + /// Maximum number of players allowed to connect + pub players_maximum: u64, + /// Number of players currently connected + pub players_online: u64, + /// Number of bots currently connected + pub players_bots: Option, + /// Whether the server requires a password to join + pub has_password: Option, + /// Data specific to non-generic responses + pub inner: SpecificResponse, +} + +/// A specific response containing extra data that isn't generic +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq)] +pub enum SpecificResponse { + Gamespy(gamespy::VersionedExtraResponse), + Minecraft(minecraft::VersionedExtraResponse), + Quake(quake::ExtraResponse), + Valve(valve::ExtraResponse), + #[cfg(not(feature = "no_games"))] + TheShip(crate::games::ts::ExtraResponse), + #[cfg(not(feature = "no_games"))] + FFOW(crate::games::ffow::ExtraResponse), +} + +/// Timeout settings for socket operations +#[derive(Clone, Debug)] +pub struct TimeoutSettings { + read: Option, + write: Option, +} + +impl TimeoutSettings { + /// Construct new settings, passing None will block indefinitely. Passing + /// zero Duration throws GDError::[InvalidInput](InvalidInput). + pub fn new(read: Option, write: Option) -> GDResult { + if let Some(read_duration) = read { + if read_duration == Duration::new(0, 0) { + return Err(InvalidInput); + } + } + + if let Some(write_duration) = write { + if write_duration == Duration::new(0, 0) { + return Err(InvalidInput); + } + } + + Ok(Self { read, write }) + } + + /// Get the read timeout. + pub fn get_read(&self) -> Option { self.read } + + /// Get the write timeout. + pub fn get_write(&self) -> Option { self.write } +} + +impl Default for TimeoutSettings { + /// Default values are 4 seconds for both read and write. + fn default() -> Self { + Self { + read: Some(Duration::from_secs(4)), + write: Some(Duration::from_secs(4)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Duration; + + // Test creating new TimeoutSettings with valid durations + #[test] + fn test_new_with_valid_durations() -> GDResult<()> { + // Define valid read and write durations + let read_duration = Duration::from_secs(1); + let write_duration = Duration::from_secs(2); + + // Create new TimeoutSettings with the valid durations + let timeout_settings = TimeoutSettings::new(Some(read_duration), Some(write_duration))?; + + // Verify that the get_read and get_write methods return the expected values + assert_eq!(timeout_settings.get_read(), Some(read_duration)); + assert_eq!(timeout_settings.get_write(), Some(write_duration)); + + Ok(()) + } + + // Test creating new TimeoutSettings with a zero duration + #[test] + fn test_new_with_zero_duration() { + // Define a zero read duration and a valid write duration + let read_duration = Duration::new(0, 0); + let write_duration = Duration::from_secs(2); + + // Try to create new TimeoutSettings with the zero read duration (this should + // fail) + let result = TimeoutSettings::new(Some(read_duration), Some(write_duration)); + + // Verify that the function returned an error and that the error type is + // InvalidInput + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), InvalidInput); + } + + // Test that the default TimeoutSettings values are correct + #[test] + fn test_default_values() { + // Get the default TimeoutSettings values + let default_settings = TimeoutSettings::default(); + + // Verify that the get_read and get_write methods return the expected default + // values + assert_eq!(default_settings.get_read(), Some(Duration::from_secs(4))); + assert_eq!(default_settings.get_write(), Some(Duration::from_secs(4))); + } +} diff --git a/src/protocols/valve/types.rs b/src/protocols/valve/types.rs index 4d18937..1392c3f 100644 --- a/src/protocols/valve/types.rs +++ b/src/protocols/valve/types.rs @@ -1,8 +1,11 @@ use std::collections::HashMap; -use crate::bufferer::Bufferer; use crate::GDError::UnknownEnumCast; use crate::GDResult; +use crate::{ + bufferer::Bufferer, + protocols::{types::SpecificResponse, GenericResponse}, +}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -55,6 +58,63 @@ pub struct Response { pub rules: Option>, } +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq)] +pub struct ExtraResponse { + pub players: Option>, + pub rules: Option>, + /// Protocol used by the server. + pub protocol: u8, + /// Name of the folder containing the game files. + pub folder: String, + /// [Steam Application ID](https://developer.valvesoftware.com/wiki/Steam_Application_ID) of game. + pub appid: u32, + /// Dedicated, NonDedicated or SourceTV + pub server_type: Server, + /// The Operating System that the server is on. + pub environment_type: Environment, + /// Indicates whether the server uses VAC. + pub vac_secured: bool, + /// [The ship](https://developer.valvesoftware.com/wiki/The_Ship) extra data + pub the_ship: Option, + /// Some extra data that the server might provide or not. + pub extra_data: Option, + /// GoldSrc only: Indicates whether the hosted game is a mod. + pub is_mod: bool, + /// GoldSrc only: If the game is a mod, provide additional data. + pub mod_data: Option, +} + +impl From for GenericResponse { + fn from(r: Response) -> Self { + GenericResponse { + name: Some(r.info.name), + description: None, + game: Some(r.info.game), + game_version: Some(r.info.version), + map: Some(r.info.map), + players_maximum: r.info.players_maximum.into(), + players_online: r.info.players_online.into(), + players_bots: Some(r.info.players_bots.into()), + has_password: Some(r.info.has_password), + inner: SpecificResponse::Valve(ExtraResponse { + players: r.players, + rules: r.rules, + protocol: r.info.protocol, + folder: r.info.folder, + appid: r.info.appid, + server_type: r.info.server_type, + environment_type: r.info.environment_type, + vac_secured: r.info.vac_secured, + the_ship: r.info.the_ship, + extra_data: r.info.extra_data, + is_mod: r.info.is_mod, + mod_data: r.info.mod_data, + }), + } + } +} + /// General server information's. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]