diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9e197bc..7449aee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: language: system files: '[.]rs$' pass_filenames: false - entry: cargo test + entry: cargo test --bins --lib --examples --all-features - id: format name: Check rustfmt language: system diff --git a/CHANGELOG.md b/CHANGELOG.md index 3753d06..dba38c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ Crate: - Rich errors, capturing backtrace is done on `RUST_BACKTRACE=1`. - Applied some nursery Clippy lints. +Generics: +- Added `ExtraRequestSettings` containing all possible extra request settings. +- Added `query_with_timeout_and_extra_settings()` to allow generic queries with extra settings. + ### Breaking... Crate: - The enum used for errors, `GDError` has been renamed to `GDErrorKind`. @@ -56,6 +60,9 @@ Protocols: - Minecraft Bedrock 1. Renamed `version_protocol` to `protocol_version`. +- Minecraft: +1. Added `request_settings` parameter to `query()` + - The Ship: 1. Renamed `protocol` to `protocol_version`. 2. Renamed `max_players` to `players_maximum` and changed its type from `u64` to `u32`. diff --git a/examples/generic.rs b/examples/generic.rs index 5b9260c..bcad9d8 100644 --- a/examples/generic.rs +++ b/examples/generic.rs @@ -1,6 +1,6 @@ use gamedig::{ - protocols::types::{CommonResponse, TimeoutSettings}, - query_with_timeout, + protocols::types::{CommonResponse, ExtraRequestSettings, TimeoutSettings}, + query_with_timeout_and_extra_settings, GDResult, GAMES, }; @@ -13,6 +13,7 @@ fn generic_query( addr: &IpAddr, port: Option, timeout_settings: Option, + extra_settings: Option, ) -> GDResult> { let game = GAMES .get(game_name) @@ -20,7 +21,7 @@ fn generic_query( println!("Querying {:#?} with game {:#?}.", addr, game); - let response = query_with_timeout(game, addr, port, timeout_settings)?; + let response = query_with_timeout_and_extra_settings(game, addr, port, timeout_settings, extra_settings)?; println!("Response: {:#?}", response.as_json()); let common = response.as_original(); @@ -34,16 +35,22 @@ fn main() { // Handle arguments if let Some(game_name) = args.next() { + let hostname = args.next().expect("Must provide an address"); // Use to_socket_addrs to resolve hostname to IP - let addr: SocketAddr = args - .next() - .map(|s| format!("{}:0", s).to_socket_addrs().unwrap()) - .expect("Must provide address") + let addr: SocketAddr = format!("{}:0", hostname) + .to_socket_addrs() + .unwrap() .next() .expect("Could not lookup host"); let port: Option = args.next().map(|s| s.parse().unwrap()); - generic_query(&game_name, &addr.ip(), port, None).unwrap(); + let extra_settings = ExtraRequestSettings::default() + .set_hostname(hostname.to_string()) + .set_gather_rules(true) + .set_gather_players(true) + .set_check_app_id(false); + + generic_query(&game_name, &addr.ip(), port, None, Some(extra_settings)).unwrap(); } else { // Without arguments print a list of games @@ -68,7 +75,7 @@ mod test { fn test_game(game_name: &str) { let timeout_settings = Some(TimeoutSettings::new(Some(Duration::from_nanos(1)), Some(Duration::from_nanos(1))).unwrap()); - assert!(generic_query(game_name, &ADDR, None, timeout_settings).is_err()); + assert!(generic_query(game_name, &ADDR, None, timeout_settings, None).is_err()); } #[test] diff --git a/src/games/mod.rs b/src/games/mod.rs index 16a4c67..9a87049 100644 --- a/src/games/mod.rs +++ b/src/games/mod.rs @@ -118,7 +118,7 @@ pub mod warsow; use crate::protocols::gamespy::GameSpyVersion; use crate::protocols::quake::QuakeVersion; -use crate::protocols::types::{CommonResponse, ProprietaryProtocol, TimeoutSettings}; +use crate::protocols::types::{CommonResponse, ExtraRequestSettings, ProprietaryProtocol, TimeoutSettings}; use crate::protocols::{self, Protocol}; use crate::GDResult; use std::net::{IpAddr, SocketAddr}; @@ -142,26 +142,50 @@ mod definitions; pub use definitions::GAMES; /// Make a query given a game definition +#[inline] pub fn query(game: &Game, address: &IpAddr, port: Option) -> GDResult> { - query_with_timeout(game, address, port, None) + query_with_timeout_and_extra_settings(game, address, port, None, None) } /// Make a query given a game definition and timeout settings +#[inline] pub fn query_with_timeout( game: &Game, address: &IpAddr, port: Option, timeout_settings: Option, +) -> GDResult> { + query_with_timeout_and_extra_settings(game, address, port, timeout_settings, None) +} + +/// Make a query given a game definition, timeout settings, and extra settings +pub fn query_with_timeout_and_extra_settings( + game: &Game, + address: &IpAddr, + port: Option, + timeout_settings: Option, + extra_settings: 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, timeout_settings).map(Box::new)? + protocols::valve::query( + &socket_addr, + steam_app.as_engine(), + extra_settings.map(ExtraRequestSettings::into), + timeout_settings, + ) + .map(Box::new)? } Protocol::Minecraft(version) => { match version { Some(protocols::minecraft::Server::Java) => { - protocols::minecraft::query_java(&socket_addr, timeout_settings, None).map(Box::new)? + protocols::minecraft::query_java( + &socket_addr, + timeout_settings, + extra_settings.map(ExtraRequestSettings::into), + ) + .map(Box::new)? } Some(protocols::minecraft::Server::Bedrock) => { protocols::minecraft::query_bedrock(&socket_addr, timeout_settings).map(Box::new)? @@ -169,7 +193,14 @@ pub fn query_with_timeout( Some(protocols::minecraft::Server::Legacy(group)) => { protocols::minecraft::query_legacy_specific(*group, &socket_addr, timeout_settings).map(Box::new)? } - None => protocols::minecraft::query(&socket_addr, timeout_settings).map(Box::new)?, + None => { + protocols::minecraft::query( + &socket_addr, + timeout_settings, + extra_settings.map(ExtraRequestSettings::into), + ) + .map(Box::new)? + } } } Protocol::Gamespy(version) => { diff --git a/src/protocols/minecraft/protocol/mod.rs b/src/protocols/minecraft/protocol/mod.rs index 5b79e07..00e9eba 100644 --- a/src/protocols/minecraft/protocol/mod.rs +++ b/src/protocols/minecraft/protocol/mod.rs @@ -26,8 +26,12 @@ mod legacy_v1_6; /// Queries a Minecraft server with all the protocol variants one by one (Java /// -> Bedrock -> Legacy (1.6 -> 1.4 -> Beta 1.8)). -pub fn query(address: &SocketAddr, timeout_settings: Option) -> GDResult { - if let Ok(response) = query_java(address, timeout_settings.clone(), None) { +pub fn query( + address: &SocketAddr, + timeout_settings: Option, + request_settings: Option, +) -> GDResult { + if let Ok(response) = query_java(address, timeout_settings.clone(), request_settings) { return Ok(response); } diff --git a/src/protocols/minecraft/types.rs b/src/protocols/minecraft/types.rs index 1ddfb0f..0a9b49d 100644 --- a/src/protocols/minecraft/types.rs +++ b/src/protocols/minecraft/types.rs @@ -5,7 +5,7 @@ use crate::{ buffer::Buffer, protocols::{ - types::{CommonPlayer, CommonResponse, GenericPlayer}, + types::{CommonPlayer, CommonResponse, ExtraRequestSettings, GenericPlayer}, GenericResponse, }, GDErrorKind::{InvalidInput, PacketBad, UnknownEnumCast}, @@ -110,6 +110,16 @@ impl Default for RequestSettings { } } +impl From for RequestSettings { + fn from(value: ExtraRequestSettings) -> Self { + let default = Self::default(); + Self { + hostname: value.hostname.unwrap_or(default.hostname), + protocol_version: value.protocol_version.unwrap_or(default.protocol_version), + } + } +} + impl CommonResponse for JavaResponse { fn as_original(&self) -> GenericResponse { GenericResponse::Minecraft(VersionedResponse::Java(self)) } diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs index efb820a..1b5128f 100644 --- a/src/protocols/mod.rs +++ b/src/protocols/mod.rs @@ -15,4 +15,4 @@ pub mod types; /// Reference: [Server Query](https://developer.valvesoftware.com/wiki/Server_queries) pub mod valve; -pub use types::{GenericResponse, Protocol}; +pub use types::{ExtraRequestSettings, GenericResponse, Protocol}; diff --git a/src/protocols/types.rs b/src/protocols/types.rs index ed8a8de..adacdcc 100644 --- a/src/protocols/types.rs +++ b/src/protocols/types.rs @@ -185,6 +185,82 @@ impl Default for TimeoutSettings { } } +/// Generic extra request settings +/// +/// Fields of this struct may not be used depending on which protocol is +/// selected, the individual fields link to the specific places they will be +/// used with additional documentation. +/// +/// ## Examples +/// Create minecraft settings with builder: +/// ``` +/// use gamedig::protocols::{minecraft, ExtraRequestSettings}; +/// let mc_settings: minecraft::RequestSettings = ExtraRequestSettings::default().set_hostname("mc.hypixel.net".to_string()).into(); +/// ``` +/// +/// Create valve settings with builder: +/// ``` +/// use gamedig::protocols::{valve, ExtraRequestSettings}; +/// let valve_settings: valve::GatheringSettings = ExtraRequestSettings::default().set_check_app_id(false).into(); +/// ``` +#[derive(Clone, Debug, Default)] +pub struct ExtraRequestSettings { + /// The server's hostname. + /// + /// Used by: + /// - [minecraft::RequestSettings#structfield.hostname] + pub hostname: Option, + /// The protocol version to use. + /// + /// Used by: + /// - [minecraft::RequestSettings#structfield.protocol_version] + pub protocol_version: Option, + /// Whether to gather player information + /// + /// Used by: + /// - [valve::GatheringSettings#structfield.players] + pub gather_players: Option, + /// Whether to gather rule information. + /// + /// Used by: + /// - [valve::GatheringSettings#structfield.rules] + pub gather_rules: Option, + /// Whether to check if the App ID is valid. + /// + /// Used by: + /// - [valve::GatheringSettings#structfield.check_app_id] + pub check_app_id: Option, +} + +impl ExtraRequestSettings { + /// [Sets hostname](ExtraRequestSettings#structfield.hostname) + pub fn set_hostname(mut self, hostname: String) -> Self { + self.hostname = Some(hostname); + self + } + /// [Sets protocol + /// version](ExtraRequestSettings#structfield.protocol_version) + pub fn set_protocol_version(mut self, protocol_version: i32) -> Self { + self.protocol_version = Some(protocol_version); + self + } + /// [Sets gather players](ExtraRequestSettings#structfield.gather_players) + pub fn set_gather_players(mut self, gather_players: bool) -> Self { + self.gather_players = Some(gather_players); + self + } + /// [Sets gather rules](ExtraRequestSettings#structfield.gather_rules) + pub fn set_gather_rules(mut self, gather_rules: bool) -> Self { + self.gather_rules = Some(gather_rules); + self + } + /// [Sets check app ID](ExtraRequestSettings#structfield.check_app_id) + pub fn set_check_app_id(mut self, check_app_id: bool) -> Self { + self.check_app_id = Some(check_app_id); + self + } +} + #[cfg(test)] mod tests { use super::*; @@ -235,4 +311,13 @@ mod tests { assert_eq!(default_settings.get_read(), Some(Duration::from_secs(4))); assert_eq!(default_settings.get_write(), Some(Duration::from_secs(4))); } + + // Test that extra request settings can be converted + #[test] + fn test_extra_request_settings() { + let settings = ExtraRequestSettings::default(); + + let _: minecraft::RequestSettings = settings.clone().into(); + let _: valve::GatheringSettings = settings.into(); + } } diff --git a/src/protocols/valve/types.rs b/src/protocols/valve/types.rs index 500aa07..1f754b8 100644 --- a/src/protocols/valve/types.rs +++ b/src/protocols/valve/types.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer}; +use crate::protocols::types::{CommonPlayer, CommonResponse, ExtraRequestSettings, GenericPlayer}; use crate::GDErrorKind::UnknownEnumCast; use crate::GDResult; use crate::{buffer::Buffer, protocols::GenericResponse}; @@ -430,6 +430,17 @@ impl Default for GatheringSettings { } } +impl From for GatheringSettings { + fn from(value: ExtraRequestSettings) -> Self { + let default = Self::default(); + Self { + players: value.gather_players.unwrap_or(default.players), + rules: value.gather_rules.unwrap_or(default.rules), + check_app_id: value.check_app_id.unwrap_or(default.check_app_id), + } + } +} + /// Generic response types that are used by many games, they are the protocol /// ones, but without the unnecessary bits (example: the **The Ship**-only /// fields).