From b368877031b1f55b75142fe2f070b6ddb36da180 Mon Sep 17 00:00:00 2001 From: Tom <25043847+Douile@users.noreply.github.com> Date: Sun, 25 Jun 2023 13:31:23 +0000 Subject: [PATCH] [Protocol] Implement generic response with dyn (#56) * Implement generic response as enum * First draft of implementing into_common() * Make common response type generic * Use macros and generics to reduce repetition * [Games] Add dynamically dispatched CommonResponse trait This adds two traits: "CommonResponse", and "CommonPlayer", when the generic game query function returns a response it returns a pointer to its original response type that implements "CommonResponse". Both common traits require that "as_original()" be implemented, this returns an enum containing a pointer to the original type. Both traits have a concrete method "as_json()" that returns a struct containing data fetched from all of its methods as. This struct implements serde and can hence be serialized as required. The traits require a few other methods be implemented, those being the fields that are common across all types. All other methods have a default None implementation so that each response type only needs to implement methods for fields that it has. * [Game] Implement common traits for JCMP2 response * [Fmt] Run cargo fmt * Fix doctest failing * Run cargo fmt --- examples/generic.rs | 10 +- src/games/ffow.rs | 56 ++----- src/games/jc2mp.rs | 43 ++++-- src/games/mod.rs | 31 ++-- src/games/ts.rs | 71 ++++----- src/lib.rs | 2 +- src/protocols/gamespy/mod.rs | 19 ++- src/protocols/gamespy/protocols/one/types.rs | 65 ++++---- .../gamespy/protocols/three/types.rs | 61 ++++---- src/protocols/gamespy/protocols/two/types.rs | 55 ++++--- src/protocols/minecraft/types.rs | 114 ++++---------- src/protocols/quake/one.rs | 15 +- src/protocols/quake/two.rs | 17 ++- src/protocols/quake/types.rs | 53 ++++--- src/protocols/types.rs | 141 +++++++++++++----- src/protocols/valve/protocol.rs | 1 - src/protocols/valve/types.rs | 81 +++------- 17 files changed, 405 insertions(+), 430 deletions(-) diff --git a/examples/generic.rs b/examples/generic.rs index c85c708..0a84f6e 100644 --- a/examples/generic.rs +++ b/examples/generic.rs @@ -1,15 +1,17 @@ -use gamedig::{protocols::GenericResponse, query, GDResult, GAMES}; +use gamedig::{protocols::types::CommonResponse, query, GDResult, GAMES}; use std::net::IpAddr; -fn generic_query(game_name: &str, addr: &IpAddr, port: Option) -> GDResult { +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 game {:#?}.", addr, game.name); + println!("Querying {:#?} with game {:#?}.", addr, game); let response = query(game, addr, port)?; + println!("Response: {:#?}", response.as_json()); - println!("{:#?}", response); + let common = response.as_original(); + println!("Common response: {:#?}", common); Ok(response) } diff --git a/src/games/ffow.rs b/src/games/ffow.rs index e8d6af9..7d8718b 100644 --- a/src/games/ffow.rs +++ b/src/games/ffow.rs @@ -1,4 +1,4 @@ -use crate::protocols::types::{SpecificResponse, TimeoutSettings}; +use crate::protocols::types::{CommonResponse, TimeoutSettings}; use crate::protocols::valve::{Engine, Environment, Server, ValveProtocol}; use crate::protocols::GenericResponse; use crate::GDResult; @@ -44,51 +44,17 @@ 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 CommonResponse for Response { + fn as_original(&self) -> GenericResponse { GenericResponse::FFOW(self) } -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, - }), - } - } + fn name(&self) -> Option<&str> { Some(&self.name) } + fn game(&self) -> Option<&str> { Some(&self.game_mode) } + fn description(&self) -> Option<&str> { Some(&self.description) } + fn game_version(&self) -> Option<&str> { Some(&self.version) } + fn map(&self) -> Option<&str> { Some(&self.map) } + fn has_password(&self) -> Option { Some(self.has_password) } + fn players_maximum(&self) -> u64 { self.players_maximum.into() } + fn players_online(&self) -> u64 { self.players_online.into() } } pub fn query(address: &IpAddr, port: Option) -> GDResult { diff --git a/src/games/jc2mp.rs b/src/games/jc2mp.rs index b96c025..f6b4e0c 100644 --- a/src/games/jc2mp.rs +++ b/src/games/jc2mp.rs @@ -1,7 +1,7 @@ use crate::bufferer::{Bufferer, Endianess}; use crate::protocols::gamespy::common::has_password; use crate::protocols::gamespy::three::{data_to_map, GameSpy3}; -use crate::protocols::types::SpecificResponse; +use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer}; use crate::protocols::GenericResponse; use crate::{GDError, GDResult}; #[cfg(feature = "serde")] @@ -16,6 +16,12 @@ pub struct Player { ping: u16, } +impl CommonPlayer for Player { + fn as_original(&self) -> GenericPlayer { GenericPlayer::JCMP2(self) } + + fn name(&self) -> &str { &self.name } +} + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct Response { @@ -28,20 +34,27 @@ pub struct Response { players_online: usize, } -impl From for GenericResponse { - fn from(r: Response) -> Self { - Self { - name: Some(r.name), - description: Some(r.description), - game: None, - game_version: Some(r.version), - map: None, - players_maximum: r.players_maximum as u64, - players_online: r.players_online as u64, - players_bots: None, - has_password: Some(r.has_password), - inner: SpecificResponse::JC2MP, - } +impl CommonResponse for Response { + fn as_original(&self) -> GenericResponse { GenericResponse::JC2MP(self) } + + fn game_version(&self) -> Option<&str> { Some(&self.version) } + fn description(&self) -> Option<&str> { Some(&self.description) } + fn name(&self) -> Option<&str> { Some(&self.name) } + fn has_password(&self) -> Option { Some(self.has_password) } + fn players_maximum(&self) -> u64 { + // If usize doesn't fit in u64 silently return 0 as this is extremely unlikely + // for a player count + self.players_maximum.try_into().unwrap_or(0) + } + fn players_online(&self) -> u64 { self.players_online.try_into().unwrap_or(0) } + + fn players(&self) -> Option> { + Some( + self.players + .iter() + .map(|p| p as &dyn CommonPlayer) + .collect(), + ) } } diff --git a/src/games/mod.rs b/src/games/mod.rs index 50e1809..944d971 100644 --- a/src/games/mod.rs +++ b/src/games/mod.rs @@ -114,6 +114,7 @@ pub mod vr; use crate::protocols::gamespy::GameSpyVersion; use crate::protocols::quake::QuakeVersion; +use crate::protocols::types::CommonResponse; use crate::protocols::{self, Protocol}; use crate::GDResult; use std::net::{IpAddr, SocketAddr}; @@ -137,42 +138,42 @@ mod definitions; pub use definitions::GAMES; /// Make a query given a game definition -pub fn query(game: &Game, address: &IpAddr, port: Option) -> GDResult { +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())? + protocols::valve::query(&socket_addr, steam_app.as_engine(), None, None).map(Box::new)? } Protocol::Minecraft(version) => { match version { Some(protocols::minecraft::Server::Java) => { - protocols::minecraft::query_java(&socket_addr, None).map(|r| r.into())? + protocols::minecraft::query_java(&socket_addr, None).map(Box::new)? } Some(protocols::minecraft::Server::Bedrock) => { - protocols::minecraft::query_bedrock(&socket_addr, None).map(|r| r.into())? + protocols::minecraft::query_bedrock(&socket_addr, None).map(Box::new)? } Some(protocols::minecraft::Server::Legacy(group)) => { - protocols::minecraft::query_legacy_specific(*group, &socket_addr, None).map(|r| r.into())? + protocols::minecraft::query_legacy_specific(*group, &socket_addr, None).map(Box::new)? } - None => protocols::minecraft::query(&socket_addr, None).map(|r| r.into())?, + None => protocols::minecraft::query(&socket_addr, None).map(Box::new)?, } } Protocol::Gamespy(version) => { match version { - GameSpyVersion::One => protocols::gamespy::one::query(&socket_addr, None).map(|r| r.into())?, - GameSpyVersion::Two => protocols::gamespy::two::query(&socket_addr, None).map(|r| r.into())?, - GameSpyVersion::Three => protocols::gamespy::three::query(&socket_addr, None).map(|r| r.into())?, + GameSpyVersion::One => protocols::gamespy::one::query(&socket_addr, None).map(Box::new)?, + GameSpyVersion::Two => protocols::gamespy::two::query(&socket_addr, None).map(Box::new)?, + GameSpyVersion::Three => protocols::gamespy::three::query(&socket_addr, None).map(Box::new)?, } } 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())?, + QuakeVersion::One => protocols::quake::one::query(&socket_addr, None).map(Box::new)?, + QuakeVersion::Two => protocols::quake::two::query(&socket_addr, None).map(Box::new)?, + QuakeVersion::Three => protocols::quake::three::query(&socket_addr, None).map(Box::new)?, } } - Protocol::TheShip => ts::query(address, port).map(|r| r.into())?, - Protocol::FFOW => ffow::query(address, port).map(|r| r.into())?, - Protocol::JC2MP => jc2mp::query(address, port).map(|r| r.into())?, + Protocol::TheShip => ts::query(address, port).map(Box::new)?, + Protocol::FFOW => ffow::query(address, port).map(Box::new)?, + Protocol::JC2MP => jc2mp::query(address, port).map(Box::new)?, }) } diff --git a/src/games/ts.rs b/src/games/ts.rs index d69f1a1..ecd8734 100644 --- a/src/games/ts.rs +++ b/src/games/ts.rs @@ -1,6 +1,6 @@ use crate::{ protocols::{ - types::SpecificResponse, + types::{CommonPlayer, CommonResponse, GenericPlayer}, valve::{self, get_optional_extracted_data, Server, ServerPlayer, SteamApp}, GenericResponse, }, @@ -35,6 +35,13 @@ impl TheShipPlayer { } } +impl CommonPlayer for TheShipPlayer { + fn as_original(&self) -> GenericPlayer { GenericPlayer::TheShip(self) } + + fn name(&self) -> &str { &self.name } + fn score(&self) -> Option { Some(self.score) } +} + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq)] pub struct Response { @@ -61,52 +68,24 @@ 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 CommonResponse for Response { + fn as_original(&self) -> GenericResponse { GenericResponse::TheShip(self) } -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, - }), - } + fn name(&self) -> Option<&str> { Some(&self.name) } + fn map(&self) -> Option<&str> { Some(&self.map) } + fn game(&self) -> Option<&str> { Some(&self.game) } + fn players_maximum(&self) -> u64 { self.max_players.into() } + fn players_online(&self) -> u64 { self.players.into() } + fn players_bots(&self) -> Option { Some(self.bots.into()) } + fn has_password(&self) -> Option { Some(self.has_password) } + + fn players(&self) -> Option> { + Some( + self.players_details + .iter() + .map(|p| p as &dyn CommonPlayer) + .collect(), + ) } } diff --git a/src/lib.rs b/src/lib.rs index 1298415..bb19572 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,7 @@ //! let response = query(game, &"127.0.0.1".parse().unwrap(), None); // None will use the default port //! match response { //! Err(error) => println!("Couldn't query, error: {}", error), -//! Ok(r) => println!("{:#?}", r), +//! Ok(r) => println!("{:#?}", r.as_json()), //! } //! ``` //! diff --git a/src/protocols/gamespy/mod.rs b/src/protocols/gamespy/mod.rs index 464ec8c..3118d98 100644 --- a/src/protocols/gamespy/mod.rs +++ b/src/protocols/gamespy/mod.rs @@ -16,11 +16,18 @@ pub enum GameSpyVersion { Three, } -/// Enum of versions and their ExtraResponse data -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +/// Versioned response type #[derive(Debug, Clone, PartialEq)] -pub enum VersionedExtraResponse { - One(one::ExtraResponse), - Two(two::ExtraResponse), - Three(three::ExtraResponse), +pub enum VersionedResponse<'a> { + One(&'a one::Response), + Two(&'a two::Response), + Three(&'a three::Response), +} + +/// Versioned player type +#[derive(Debug, Clone, PartialEq)] +pub enum VersionedPlayer<'a> { + One(&'a one::Player), + Two(&'a two::Player), + Three(&'a three::Player), } diff --git a/src/protocols/gamespy/protocols/one/types.rs b/src/protocols/gamespy/protocols/one/types.rs index 5909490..3cee4b0 100644 --- a/src/protocols/gamespy/protocols/one/types.rs +++ b/src/protocols/gamespy/protocols/one/types.rs @@ -3,8 +3,9 @@ use std::collections::HashMap; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use crate::protocols::gamespy::VersionedExtraResponse; -use crate::protocols::{types::SpecificResponse, GenericResponse}; +use crate::protocols::gamespy::{VersionedPlayer, VersionedResponse}; +use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer}; +use crate::protocols::GenericResponse; /// A player’s details. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -23,6 +24,15 @@ pub struct Player { pub secret: bool, } +impl CommonPlayer for Player { + fn as_original(&self) -> crate::protocols::types::GenericPlayer { + GenericPlayer::Gamespy(VersionedPlayer::One(self)) + } + + fn name(&self) -> &str { &self.name } + // TODO: Maybe frags is score? +} + /// A query response. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq)] @@ -43,40 +53,23 @@ pub struct Response { 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 CommonResponse for Response { + fn as_original(&self) -> GenericResponse { GenericResponse::GameSpy(VersionedResponse::One(self)) } -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, - })), - } + fn name(&self) -> Option<&str> { Some(&self.name) } + fn map(&self) -> Option<&str> { Some(&self.map) } + fn has_password(&self) -> Option { Some(self.has_password) } + fn game(&self) -> Option<&str> { Some(&self.game_type) } + fn game_version(&self) -> Option<&str> { Some(&self.game_version) } + fn players_maximum(&self) -> u64 { self.players_maximum.try_into().unwrap_or(0) } + fn players_online(&self) -> u64 { self.players_online.try_into().unwrap_or(0) } + + fn players(&self) -> Option> { + Some( + self.players + .iter() + .map(|p| p as &dyn CommonPlayer) + .collect(), + ) } } diff --git a/src/protocols/gamespy/protocols/three/types.rs b/src/protocols/gamespy/protocols/three/types.rs index dd4adf1..7eb40ad 100644 --- a/src/protocols/gamespy/protocols/three/types.rs +++ b/src/protocols/gamespy/protocols/three/types.rs @@ -1,5 +1,6 @@ -use crate::protocols::gamespy::VersionedExtraResponse; -use crate::protocols::{types::SpecificResponse, GenericResponse}; +use crate::protocols::gamespy::{VersionedPlayer, VersionedResponse}; +use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer}; +use crate::protocols::GenericResponse; use std::collections::HashMap; #[cfg(feature = "serde")] @@ -17,6 +18,15 @@ pub struct Player { pub skill: u32, } +impl CommonPlayer for Player { + fn as_original(&self) -> crate::protocols::types::GenericPlayer { + GenericPlayer::Gamespy(VersionedPlayer::Three(self)) + } + + fn name(&self) -> &str { &self.name } + fn score(&self) -> Option { Some(self.score.try_into().unwrap_or(0)) } +} + /// A team's details #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -43,36 +53,23 @@ pub struct Response { 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 CommonResponse for Response { + fn as_original(&self) -> GenericResponse { GenericResponse::GameSpy(VersionedResponse::Three(self)) } -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, - })), - } + fn name(&self) -> Option<&str> { Some(&self.name) } + fn map(&self) -> Option<&str> { Some(&self.map) } + fn has_password(&self) -> Option { Some(self.has_password) } + fn game(&self) -> Option<&str> { Some(&self.game_type) } + fn game_version(&self) -> Option<&str> { Some(&self.game_version) } + fn players_maximum(&self) -> u64 { self.players_maximum.try_into().unwrap_or(0) } + fn players_online(&self) -> u64 { self.players_online.try_into().unwrap_or(0) } + + fn players(&self) -> Option> { + Some( + self.players + .iter() + .map(|p| p as &dyn CommonPlayer) + .collect(), + ) } } diff --git a/src/protocols/gamespy/protocols/two/types.rs b/src/protocols/gamespy/protocols/two/types.rs index e488bcb..9149688 100644 --- a/src/protocols/gamespy/protocols/two/types.rs +++ b/src/protocols/gamespy/protocols/two/types.rs @@ -1,8 +1,9 @@ use std::collections::HashMap; -use crate::protocols::gamespy::VersionedExtraResponse; -use crate::protocols::types::SpecificResponse; +use crate::protocols::gamespy::{VersionedPlayer, VersionedResponse}; +use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer}; use crate::protocols::GenericResponse; + #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -22,6 +23,13 @@ pub struct Player { pub team_index: u16, } +impl CommonPlayer for Player { + fn as_original(&self) -> GenericPlayer { GenericPlayer::Gamespy(VersionedPlayer::Two(self)) } + + fn name(&self) -> &str { &self.name } + fn score(&self) -> Option { Some(self.score.into()) } +} + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct Response { @@ -36,34 +44,21 @@ pub struct Response { pub unused_entries: HashMap, } -/// Non-generic query response -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ExtraResponse { - pub teams: Vec, - pub players_minimum: Option, - pub unused_entries: HashMap, - pub players: Vec, -} +impl CommonResponse for Response { + fn as_original(&self) -> GenericResponse { GenericResponse::GameSpy(VersionedResponse::Two(self)) } -impl From for GenericResponse { - fn from(r: Response) -> Self { - Self { - name: Some(r.name), - description: None, - game: None, - game_version: None, - 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::Two(ExtraResponse { - teams: r.teams, - players_minimum: r.players_minimum, - unused_entries: r.unused_entries, - players: r.players, - })), - } + fn name(&self) -> Option<&str> { Some(&self.name) } + fn map(&self) -> Option<&str> { Some(&self.map) } + fn has_password(&self) -> Option { Some(self.has_password) } + fn players_maximum(&self) -> u64 { self.players_maximum.try_into().unwrap_or(0) } + fn players_online(&self) -> u64 { self.players_online.try_into().unwrap_or(0) } + + fn players(&self) -> Option> { + Some( + self.players + .iter() + .map(|p| p as &dyn CommonPlayer) + .collect(), + ) } } diff --git a/src/protocols/minecraft/types.rs b/src/protocols/minecraft/types.rs index 7600a99..b8ccaff 100644 --- a/src/protocols/minecraft/types.rs +++ b/src/protocols/minecraft/types.rs @@ -4,7 +4,10 @@ use crate::{ bufferer::Bufferer, - protocols::{types::SpecificResponse, GenericResponse}, + protocols::{ + types::{CommonPlayer, CommonResponse, GenericPlayer}, + GenericResponse, + }, GDError::{PacketBad, UnknownEnumCast}, GDResult, }; @@ -44,11 +47,17 @@ 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), +impl CommonPlayer for Player { + fn as_original(&self) -> GenericPlayer { GenericPlayer::Minecraft(self) } + + fn name(&self) -> &str { &self.name } +} + +/// Versioned response type +#[derive(Debug, Clone, PartialEq)] +pub enum VersionedResponse<'a> { + Bedrock(&'a BedrockResponse), + Java(&'a JavaResponse), } /// A Java query response. @@ -78,46 +87,18 @@ pub struct JavaResponse { pub server_type: Server, } -/// Non-generic java response -#[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 CommonResponse for JavaResponse { + fn as_original(&self) -> GenericResponse { GenericResponse::Minecraft(VersionedResponse::Java(self)) } -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, - })), - } + fn description(&self) -> Option<&str> { Some(&self.description) } + fn players_maximum(&self) -> u64 { self.players_maximum.into() } + fn players_online(&self) -> u64 { self.players_online.into() } + fn game_version(&self) -> Option<&str> { Some(&self.version_name) } + + fn players(&self) -> Option> { + self.players_sample + .as_ref() + .map(|players| players.iter().map(|p| p as &dyn CommonPlayer).collect()) } } @@ -147,43 +128,14 @@ pub struct BedrockResponse { pub server_type: Server, } -/// Non-generic bedrock response -#[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 CommonResponse for BedrockResponse { + fn as_original(&self) -> GenericResponse { GenericResponse::Minecraft(VersionedResponse::Bedrock(self)) } -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, - })), - } - } + fn name(&self) -> Option<&str> { Some(&self.name) } + fn map(&self) -> Option<&str> { self.map.as_deref() } + fn game_version(&self) -> Option<&str> { Some(&self.version_name) } + fn players_maximum(&self) -> u64 { self.players_maximum.into() } + fn players_online(&self) -> u64 { self.players_online.into() } } impl JavaResponse { diff --git a/src/protocols/quake/one.rs b/src/protocols/quake/one.rs index a542bbf..7078227 100644 --- a/src/protocols/quake/one.rs +++ b/src/protocols/quake/one.rs @@ -1,12 +1,14 @@ use crate::protocols::quake::client::{client_query, remove_wrapping_quotes, QuakeClient}; use crate::protocols::quake::Response; -use crate::protocols::types::TimeoutSettings; +use crate::protocols::types::{CommonPlayer, GenericPlayer, TimeoutSettings}; use crate::{GDError, GDResult}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use std::net::SocketAddr; use std::slice::Iter; +use super::QuakePlayerType; + /// Quake 1 player data. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -22,6 +24,17 @@ pub struct Player { pub color_secondary: u8, } +impl QuakePlayerType for Player { + fn version(response: &Response) -> super::VersionedResponse { super::VersionedResponse::One(response) } +} + +impl CommonPlayer for Player { + fn as_original(&self) -> GenericPlayer { GenericPlayer::QuakeOne(self) } + + fn name(&self) -> &str { &self.name } + fn score(&self) -> Option { Some(self.score.into()) } +} + pub(crate) struct QuakeOne; impl QuakeClient for QuakeOne { type Player = Player; diff --git a/src/protocols/quake/two.rs b/src/protocols/quake/two.rs index be75e12..fc2576f 100644 --- a/src/protocols/quake/two.rs +++ b/src/protocols/quake/two.rs @@ -1,13 +1,15 @@ use crate::protocols::quake::client::{client_query, remove_wrapping_quotes, QuakeClient}; use crate::protocols::quake::one::QuakeOne; use crate::protocols::quake::Response; -use crate::protocols::types::TimeoutSettings; +use crate::protocols::types::{CommonPlayer, GenericPlayer, TimeoutSettings}; use crate::{GDError, GDResult}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use std::net::SocketAddr; use std::slice::Iter; +use super::QuakePlayerType; + /// Quake 2 player data. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -17,6 +19,19 @@ pub struct Player { pub name: String, } +impl QuakePlayerType for Player { + fn version(response: &Response) -> super::VersionedResponse { + super::VersionedResponse::TwoAndThree(response) + } +} + +impl CommonPlayer for Player { + fn as_original(&self) -> GenericPlayer { GenericPlayer::QuakeTwo(self) } + + fn name(&self) -> &str { &self.name } + // TODO: Maybe frags is score? +} + pub(crate) struct QuakeTwo; impl QuakeClient for QuakeTwo { type Player = Player; diff --git a/src/protocols/quake/types.rs b/src/protocols/quake/types.rs index 3833704..9bb23b7 100644 --- a/src/protocols/quake/types.rs +++ b/src/protocols/quake/types.rs @@ -2,7 +2,10 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use crate::protocols::{types::SpecificResponse, GenericResponse}; +use crate::protocols::{ + types::{CommonPlayer, CommonResponse}, + GenericResponse, +}; /// General server information's. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -24,30 +27,32 @@ pub struct Response

{ pub unused_entries: HashMap, } -/// Non-generic quake response -#[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, +pub trait QuakePlayerType: Sized + CommonPlayer { + fn version(response: &Response) -> VersionedResponse; } -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, - }), - } +impl CommonResponse for Response

{ + fn as_original(&self) -> GenericResponse { GenericResponse::Quake(P::version(self)) } + + fn name(&self) -> Option<&str> { Some(&self.name) } + fn map(&self) -> Option<&str> { Some(&self.map) } + fn players_maximum(&self) -> u64 { self.players_maximum.into() } + fn players_online(&self) -> u64 { self.players_online.into() } + fn game_version(&self) -> Option<&str> { Some(&self.version) } + + fn players(&self) -> Option> { + Some( + self.players + .iter() + .map(|p| p as &dyn CommonPlayer) + .collect(), + ) } } + +/// Versioned response type +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum VersionedResponse<'a> { + One(&'a Response), + TwoAndThree(&'a Response), +} diff --git a/src/protocols/types.rs b/src/protocols/types.rs index cf417c4..b761453 100644 --- a/src/protocols/types.rs +++ b/src/protocols/types.rs @@ -14,51 +14,124 @@ pub enum Protocol { Minecraft(Option), Quake(quake::QuakeVersion), Valve(valve::SteamApp), + #[cfg(not(feature = "no_games"))] TheShip, + #[cfg(not(feature = "no_games"))] FFOW, JC2MP, } -/// A generic version of a response -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +/// All response types #[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, +pub enum GenericResponse<'a> { + GameSpy(gamespy::VersionedResponse<'a>), + Minecraft(minecraft::VersionedResponse<'a>), + Quake(quake::VersionedResponse<'a>), + Valve(&'a valve::Response), + #[cfg(not(feature = "no_games"))] + TheShip(&'a crate::games::ts::Response), + #[cfg(not(feature = "no_games"))] + FFOW(&'a crate::games::ffow::Response), + #[cfg(not(feature = "no_games"))] + JC2MP(&'a crate::games::jc2mp::Response), } -/// A specific response containing extra data that isn't generic -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +/// All player types #[derive(Debug, Clone, PartialEq)] -pub enum SpecificResponse { - Gamespy(gamespy::VersionedExtraResponse), - Minecraft(minecraft::VersionedExtraResponse), - Quake(quake::ExtraResponse), - Valve(valve::ExtraResponse), +pub enum GenericPlayer<'a> { + Valve(&'a valve::ServerPlayer), + QuakeOne(&'a quake::one::Player), + QuakeTwo(&'a quake::two::Player), + Minecraft(&'a minecraft::Player), + Gamespy(gamespy::VersionedPlayer<'a>), #[cfg(not(feature = "no_games"))] - TheShip(crate::games::ts::ExtraResponse), + TheShip(&'a crate::games::ts::TheShipPlayer), #[cfg(not(feature = "no_games"))] - FFOW(crate::games::ffow::ExtraResponse), - #[cfg(not(feature = "no_games"))] - JC2MP, + JCMP2(&'a crate::games::jc2mp::Player), +} + +pub trait CommonResponse { + /// Get the original response type + fn as_original(&self) -> GenericResponse; + /// Get a struct that can be stored as JSON (you don't need to override + /// this) + fn as_json(&self) -> CommonResponseJson { + CommonResponseJson { + name: self.name(), + description: self.description(), + game: self.game(), + game_version: self.game_version(), + has_password: self.has_password(), + map: self.map(), + players_maximum: self.players_maximum(), + players_online: self.players_online(), + players_bots: self.players_bots(), + players: self + .players() + .map(|players| players.iter().map(|p| p.as_json()).collect()), + } + } + + /// The name of the server + fn name(&self) -> Option<&str> { None } + /// Description of the server + fn description(&self) -> Option<&str> { None } + /// Name of the current game or game mode + fn game(&self) -> Option<&str> { None } + /// Version of the game being run on the server + fn game_version(&self) -> Option<&str> { None } + /// The current map name + fn map(&self) -> Option<&str> { None } + /// Maximum number of players allowed to connect + fn players_maximum(&self) -> u64; + /// Number of players currently connected + fn players_online(&self) -> u64; + /// Number of bots currently connected + fn players_bots(&self) -> Option { None } + /// Whether the server requires a password to join + fn has_password(&self) -> Option { None } + /// Currently connected players + fn players(&self) -> Option> { None } +} + +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[derive(Debug, Clone, PartialEq)] +pub struct CommonResponseJson<'a> { + pub name: Option<&'a str>, + pub description: Option<&'a str>, + pub game: Option<&'a str>, + pub game_version: Option<&'a str>, + pub map: Option<&'a str>, + pub players_maximum: u64, + pub players_online: u64, + pub players_bots: Option, + pub has_password: Option, + pub players: Option>>, +} + +pub trait CommonPlayer { + /// Get the original player type + fn as_original(&self) -> GenericPlayer; + /// Get a struct that can be stored as JSON (you don't need to override + /// this) + fn as_json(&self) -> CommonPlayerJson { + CommonPlayerJson { + name: self.name(), + score: self.score(), + } + } + + /// Player name + fn name(&self) -> &str; + /// Player score + fn score(&self) -> Option { None } +} + +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[derive(Debug, Clone, PartialEq)] +pub struct CommonPlayerJson<'a> { + pub name: &'a str, + pub score: Option, } /// Timeout settings for socket operations diff --git a/src/protocols/valve/protocol.rs b/src/protocols/valve/protocol.rs index 05dc737..1593e32 100644 --- a/src/protocols/valve/protocol.rs +++ b/src/protocols/valve/protocol.rs @@ -19,7 +19,6 @@ use crate::{ SteamApp, }, }, - socket::{Socket, UdpSocket}, utils::u8_lower_upper, GDError::{BadGame, Decompress, UnknownEnumCast}, diff --git a/src/protocols/valve/types.rs b/src/protocols/valve/types.rs index f3f7069..5ce3933 100644 --- a/src/protocols/valve/types.rs +++ b/src/protocols/valve/types.rs @@ -1,11 +1,9 @@ use std::collections::HashMap; +use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer}; use crate::GDError::UnknownEnumCast; use crate::GDResult; -use crate::{ - bufferer::Bufferer, - protocols::{types::SpecificResponse, GenericResponse}, -}; +use crate::{bufferer::Bufferer, protocols::GenericResponse}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -58,61 +56,22 @@ pub struct Response { pub rules: Option>, } -/// Non-generic valve response -#[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 CommonResponse for Response { + fn as_original(&self) -> GenericResponse { GenericResponse::Valve(self) } -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, - }), - } + fn name(&self) -> Option<&str> { Some(&self.info.name) } + fn game(&self) -> Option<&str> { Some(&self.info.game) } + fn game_version(&self) -> Option<&str> { Some(&self.info.version) } + fn map(&self) -> Option<&str> { Some(&self.info.map) } + fn players_maximum(&self) -> u64 { self.info.players_maximum.into() } + fn players_online(&self) -> u64 { self.info.players_online.into() } + fn players_bots(&self) -> Option { Some(self.info.players_bots.into()) } + fn has_password(&self) -> Option { Some(self.info.has_password) } + + fn players(&self) -> Option> { + self.players + .as_ref() + .map(|p| p.iter().map(|p| p as &dyn CommonPlayer).collect()) } } @@ -174,6 +133,12 @@ pub struct ServerPlayer { pub money: Option, // the_ship } +impl CommonPlayer for ServerPlayer { + fn as_original(&self) -> GenericPlayer { GenericPlayer::Valve(self) } + fn name(&self) -> &str { &self.name } + fn score(&self) -> Option { Some(self.score) } +} + /// Only present for [the ship](https://developer.valvesoftware.com/wiki/The_Ship). #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]