diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a8d49e..7acb431 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Protocols: Games: - [Serious Sam](https://www.gog.com/game/serious_sam_the_first_encounter) support. +- [Frontlines: Fuel of War](https://store.steampowered.com/app/9460/Frontlines_Fuel_of_War/) support. ### Breaking: Protocols: diff --git a/GAMES.md b/GAMES.md index 5b0105c..c0f6769 100644 --- a/GAMES.md +++ b/GAMES.md @@ -2,52 +2,53 @@ A supported game is defined as a game that has been successfully tested, other g Beware of the `Notes` column, as it contains information about query port offsets or other query requirements. # Supported games: -| Game | Use name | Protocol | Notes | -|------------------------------------|----------|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Team Fortress 2 | TF2 | Valve Protocol | | -| The Ship | TS | Valve Protocol (*Altered) | | -| Counter-Strike: Global Offensive | CSGO | Valve Protocol | The server must have the cvar `host_players_show` set to `2` to get the full player list. | -| Counter-Strike: Source | CSS | Valve Protocol | | -| Day of Defeat: Source | DODS | Valve Protocol | | -| Left 4 Dead | L4D | Valve Protocol | | -| Left 4 Dead 2 | L4D2 | Valve Protocol | | -| Half-Life 2 Deathmatch | HL2DM | Valve Protocol | | -| Alien Swarm | ALIENS | Valve Protocol | | -| Alien Swarm: Reactive Drop | ASRD | Valve Protocol | | -| Insurgency | INS | Valve Protocol | | -| Insurgency: Sandstorm | INSS | Valve Protocol | Query port offset: 1. | -| Insurgency: Modern Infantry Combat | INSMIC | Valve Protocol | | -| Counter-Strike: Condition Zero | CSCZ | Valve Protocol (GoldSrc) | | -| Day of Defeat | DOD | Valve Protocol (GoldSrc) | | -| Minecraft | MC | Proprietary | Bedrock edition provides a different response compared to the Java edition, query specifically for bedrock to get them, otherwise, only matching fields will be provided. | -| 7 Days To Die | SDTD | Valve Protocol | | -| ARK: Survival Evolved | ASE | Valve Protocol | | -| Unturned | UNTURNED | Valve Protocol | | -| The Forest | TF | Valve Protocol (GoldSrc) | Query port offset: 1. | -| Team Fortress Classic | TFC | Valve Protocol | | -| Sven Co-op | SC | Valve Protocol (GoldSrc) | | -| Rust | RUST | Valve Protocol | | -| Counter-Strike | CS | Valve Protocol (GoldSrc) | | -| Arma 2: Operation Arrowhead | ARMA2OA | Valve Protocol | Query port offset: 1. | -| Day of Infamy | DOI | Valve Protocol | | -| Half-Life Deathmatch: Source | HLDMS | Valve Protocol | | -| Risk of Rain 2 | ROR2 | Valve Protocol | Query port offset: 1. | -| Battalion 1944 | BAT1944 | Valve Protocol | Query port offset: 3. It is strongly recommended to also query the rules, as it sends basic server info in them. | -| Black Mesa | BM | Valve Protocol | | -| Project Zomboid | PZ | Valve Protocol | | -| Age of Chivalry | AOC | Valve Protocol | | -| Don't Starve Together | DST | Valve Protocol | Query port is 27016. | -| Colony Survival | COLU | Valve Protocol | | -| Onset | ONSET | Valve Protocol | Query port is 7776. | -| Codename CURE | CCURE | Valve Protocol | | -| Ballistic Overkill | BO | Valve Protocol | Query port is 27016. | -| BrainBread 2 | BB2 | Valve Protocol | | -| Avorion | AVORION | Valve Protocol | Query port is 27020. | -| Operation: Harsh Doorstop | OHD | Valve Protocol | Query port is 27005. | -| V Rising | VR | Valve Protocol | Query port is 27016. | -| Unreal Tournament | UT | GameSpy 1 | Query Port offset: 1. | -| Battlefield 1942 | BF1942 | GameSpy 1 | Query port is 23000. | -| Serious Sam | SS | GameSpy 1 | Query Port offset: 1 | +| Game | Use name | Protocol | Notes | +|------------------------------------|----------|------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Team Fortress 2 | TF2 | Valve Protocol | | +| The Ship | TS | Valve Protocol (*Altered) | | +| Counter-Strike: Global Offensive | CSGO | Valve Protocol | The server must have the cvar `host_players_show` set to `2` to get the full player list. | +| Counter-Strike: Source | CSS | Valve Protocol | | +| Day of Defeat: Source | DODS | Valve Protocol | | +| Left 4 Dead | L4D | Valve Protocol | | +| Left 4 Dead 2 | L4D2 | Valve Protocol | | +| Half-Life 2 Deathmatch | HL2DM | Valve Protocol | | +| Alien Swarm | ALIENS | Valve Protocol | | +| Alien Swarm: Reactive Drop | ASRD | Valve Protocol | | +| Insurgency | INS | Valve Protocol | | +| Insurgency: Sandstorm | INSS | Valve Protocol | Query port offset: 1. | +| Insurgency: Modern Infantry Combat | INSMIC | Valve Protocol | | +| Counter-Strike: Condition Zero | CSCZ | Valve Protocol (GoldSrc) | | +| Day of Defeat | DOD | Valve Protocol (GoldSrc) | | +| Minecraft | MC | Proprietary | Bedrock edition provides a different response compared to the Java edition, query specifically for bedrock to get them, otherwise, only matching fields will be provided. | +| 7 Days To Die | SDTD | Valve Protocol | | +| ARK: Survival Evolved | ASE | Valve Protocol | | +| Unturned | UNTURNED | Valve Protocol | | +| The Forest | TF | Valve Protocol (GoldSrc) | Query port offset: 1. | +| Team Fortress Classic | TFC | Valve Protocol | | +| Sven Co-op | SC | Valve Protocol (GoldSrc) | | +| Rust | RUST | Valve Protocol | | +| Counter-Strike | CS | Valve Protocol (GoldSrc) | | +| Arma 2: Operation Arrowhead | ARMA2OA | Valve Protocol | Query port offset: 1. | +| Day of Infamy | DOI | Valve Protocol | | +| Half-Life Deathmatch: Source | HLDMS | Valve Protocol | | +| Risk of Rain 2 | ROR2 | Valve Protocol | Query port offset: 1. | +| Battalion 1944 | BAT1944 | Valve Protocol | Query port offset: 3. It is strongly recommended to also query the rules, as it sends basic server info in them. | +| Black Mesa | BM | Valve Protocol | | +| Project Zomboid | PZ | Valve Protocol | | +| Age of Chivalry | AOC | Valve Protocol | | +| Don't Starve Together | DST | Valve Protocol | Query port is 27016. | +| Colony Survival | COLU | Valve Protocol | | +| Onset | ONSET | Valve Protocol | Query port is 7776. | +| Codename CURE | CCURE | Valve Protocol | | +| Ballistic Overkill | BO | Valve Protocol | Query port is 27016. | +| BrainBread 2 | BB2 | Valve Protocol | | +| Avorion | AVORION | Valve Protocol | Query port is 27020. | +| Operation: Harsh Doorstop | OHD | Valve Protocol | Query port is 27005. | +| V Rising | VR | Valve Protocol | Query port is 27016. | +| Unreal Tournament | UT | GameSpy 1 | Query Port offset: 1. | +| Battlefield 1942 | BF1942 | GameSpy 1 | Query port is 23000. | +| Serious Sam | SS | GameSpy 1 | Query Port offset: 1. | +| Frontlines: Fuel of War | FFOW | Valve Protocol (Proprietary) | Query Port offset: 2. | ## Planned to add support: _ diff --git a/examples/master_querant.rs b/examples/master_querant.rs index 94ee720..1bbb0a9 100644 --- a/examples/master_querant.rs +++ b/examples/master_querant.rs @@ -24,6 +24,7 @@ use gamedig::{ dods, doi, dst, + ffow, gm, hl2dm, hldms, @@ -167,6 +168,7 @@ fn main() -> GDResult<()> { "ss" => println!("{:#?}", ss::query(ip, port)), "_gamespy3" => println!("{:#?}", gamespy::three::query(ip, port.unwrap(), None)), "_gamespy3_vars" => println!("{:#?}", gamespy::three::query_vars(ip, port.unwrap(), None)), + "ffow" => println!("{:#?}", ffow::query(ip, port)), _ => panic!("Undefined game: {}", args[1]), }; diff --git a/src/games/ffow.rs b/src/games/ffow.rs new file mode 100644 index 0000000..3f7c1c2 --- /dev/null +++ b/src/games/ffow.rs @@ -0,0 +1,95 @@ +use crate::protocols::types::TimeoutSettings; +use crate::protocols::valve::{Engine, Environment, Server, ValveProtocol}; +use crate::GDResult; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// The query response. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Response { + /// Protocol used by the server. + pub protocol: u8, + /// Name of the server. + pub name: String, + /// Map name. + pub active_mod: String, + /// Running game mode. + pub game_mode: String, + /// Description of the server. + pub description: String, + /// The version that the server is running on. + pub version: String, + /// Current map. + pub map: String, + /// Number of players on the server. + pub players_online: u8, + /// Maximum number of players the server reports it can hold. + pub players_maximum: u8, + /// Dedicated, NonDedicated or SourceTV + pub server_type: Server, + /// The Operating System that the server is on. + pub environment_type: Environment, + /// Indicates whether the server requires a password. + pub has_password: bool, + /// 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, +} + +pub fn query(address: &str, port: Option) -> GDResult { + query_with_timeout(address, port, TimeoutSettings::default()) +} + +pub fn query_with_timeout(address: &str, port: Option, timeout_settings: TimeoutSettings) -> GDResult { + let mut client = ValveProtocol::new(address, port.unwrap_or(5478), Some(timeout_settings))?; + let mut buffer = client.get_request_data( + &Engine::GoldSrc(true), + 0, + 0x46, + String::from("LSQ").into_bytes(), + )?; + + let protocol = buffer.get_u8()?; + let name = buffer.get_string_utf8()?; + let map = buffer.get_string_utf8()?; + let active_mod = buffer.get_string_utf8()?; + let game_mode = buffer.get_string_utf8()?; + let description = buffer.get_string_utf8()?; + let version = buffer.get_string_utf8()?; + buffer.move_position_ahead(2); + let players_online = buffer.get_u8()?; + let players_maximum = buffer.get_u8()?; + let server_type = Server::from_gldsrc(buffer.get_u8()?)?; + let environment_type = Environment::from_gldsrc(buffer.get_u8()?)?; + let has_password = buffer.get_u8()? == 1; + let vac_secured = buffer.get_u8()? == 1; + buffer.move_position_ahead(1); //average fps + let round = buffer.get_u8()?; + let rounds_maximum = buffer.get_u8()?; + let time_left = buffer.get_u16()?; + + Ok(Response { + protocol, + name, + active_mod, + game_mode, + description, + version, + map, + players_online, + players_maximum, + server_type, + environment_type, + has_password, + vac_secured, + round, + rounds_maximum, + time_left, + }) +} diff --git a/src/games/mod.rs b/src/games/mod.rs index a5f76a4..338e89f 100644 --- a/src/games/mod.rs +++ b/src/games/mod.rs @@ -42,6 +42,8 @@ pub mod dods; pub mod doi; /// Don't Starve Together pub mod dst; +/// Frontlines: Fuel of War +pub mod ffow; /// Garry's Mod pub mod gm; /// Half-Life 2 Deathmatch diff --git a/src/protocols/valve/protocol.rs b/src/protocols/valve/protocol.rs index 9e4944b..87847ed 100644 --- a/src/protocols/valve/protocol.rs +++ b/src/protocols/valve/protocol.rs @@ -28,61 +28,9 @@ use crate::{ use bzip2_rs::decoder::Decoder; +use crate::protocols::valve::Packet; use std::collections::HashMap; -#[derive(Debug, Clone)] -struct Packet { - pub header: u32, - pub kind: u8, - pub payload: Vec, -} - -impl Packet { - fn new(buffer: &mut Bufferer) -> GDResult { - Ok(Self { - header: buffer.get_u32()?, - kind: buffer.get_u8()?, - payload: buffer.remaining_data_vec(), - }) - } - - fn challenge(kind: Request, challenge: Vec) -> Self { - let mut initial = Packet::initial(kind); - - Self { - header: initial.header, - kind: initial.kind, - payload: match kind { - Request::Info => { - initial.payload.extend(challenge); - initial.payload - } - _ => challenge, - }, - } - } - - fn initial(kind: Request) -> Self { - Self { - header: 4294967295, // FF FF FF FF - kind: kind as u8, - payload: match kind { - Request::Info => String::from("Source Engine Query\0").into_bytes(), - _ => vec![0xFF, 0xFF, 0xFF, 0xFF], - }, - } - } - - fn to_bytes(&self) -> Vec { - let mut buf = Vec::from(self.header.to_be_bytes()); - - buf.push(self.kind); - buf.extend(&self.payload); - - buf - } -} - #[derive(Debug)] #[allow(dead_code)] //remove this later on struct SplitPacket { @@ -169,14 +117,14 @@ impl SplitPacket { } } -struct ValveProtocol { +pub(crate) struct ValveProtocol { socket: UdpSocket, } static PACKET_SIZE: usize = 6144; impl ValveProtocol { - fn new(address: &str, port: u16, timeout_settings: Option) -> GDResult { + pub fn new(address: &str, port: u16, timeout_settings: Option) -> GDResult { let socket = UdpSocket::new(address, port)?; socket.apply_timeout(timeout_settings)?; @@ -208,22 +156,41 @@ impl ValveProtocol { } let mut new_packet_buffer = Bufferer::new_with_data(Endianess::Little, &main_packet.get_payload()?); - Ok(Packet::new(&mut new_packet_buffer)?) + Ok(Packet::new_from_bufferer(&mut new_packet_buffer)?) } else { - Packet::new(&mut buffer) + Packet::new_from_bufferer(&mut buffer) } } + pub fn get_kind_request_data(&mut self, engine: &Engine, protocol: u8, kind: Request) -> GDResult { + self.get_request_data(engine, protocol, kind as u8, kind.get_default_payload()) + } + /// Ask for a specific request only. - fn get_request_data(&mut self, engine: &Engine, protocol: u8, kind: Request) -> GDResult { - let request_initial_packet = Packet::initial(kind).to_bytes(); + pub fn get_request_data( + &mut self, + engine: &Engine, + protocol: u8, + kind: u8, + payload: Vec, + ) -> GDResult { + let request_initial_packet = Packet::new(kind, payload).to_bytes(); self.socket.send(&request_initial_packet)?; let mut packet = self.receive(engine, protocol, PACKET_SIZE)?; while packet.kind == 0x41 { // 'A' - let challenge = packet.payload.clone(); - let challenge_packet = Packet::challenge(kind, challenge).to_bytes(); + let challenge = packet.payload; + + const INFO: u8 = Request::Info as u8; // hmm, this could be unwanted and problematic + let challenge_packet = Packet::new( + kind, + match kind { + INFO => [Request::Info.get_default_payload(), challenge].concat(), + _ => challenge, + }, + ) + .to_bytes(); self.socket.send(&challenge_packet)?; @@ -297,7 +264,7 @@ impl ValveProtocol { /// Get the server information's. fn get_server_info(&mut self, engine: &Engine) -> GDResult { - let mut buffer = self.get_request_data(engine, 0, Request::Info)?; + let mut buffer = self.get_kind_request_data(engine, 0, Request::Info)?; if let Engine::GoldSrc(force) = engine { if *force { @@ -314,18 +281,8 @@ impl ValveProtocol { let players = buffer.get_u8()?; let max_players = buffer.get_u8()?; let bots = buffer.get_u8()?; - let server_type = match buffer.get_u8()? { - 100 => Server::Dedicated, //'d' - 108 => Server::NonDedicated, //'l' - 112 => Server::TV, //'p' - _ => Err(UnknownEnumCast)?, - }; - let environment_type = match buffer.get_u8()? { - 108 => Environment::Linux, //'l' - 119 => Environment::Windows, //'w' - 109 | 111 => Environment::Mac, //'m' or 'o' - _ => Err(UnknownEnumCast)?, - }; + let server_type = Server::from_gldsrc(buffer.get_u8()?)?; + let environment_type = Environment::from_gldsrc(buffer.get_u8()?)?; let has_password = buffer.get_u8()? == 1; let vac_secured = buffer.get_u8()? == 1; let the_ship = match *engine == SteamApp::TS.as_engine() { @@ -400,7 +357,7 @@ impl ValveProtocol { /// Get the server player's. fn get_server_players(&mut self, engine: &Engine, protocol: u8) -> GDResult> { - let mut buffer = self.get_request_data(engine, protocol, Request::Players)?; + let mut buffer = self.get_kind_request_data(engine, protocol, Request::Players)?; let count = buffer.get_u8()? as usize; let mut players: Vec = Vec::with_capacity(count); @@ -428,7 +385,7 @@ impl ValveProtocol { /// Get the server's rules. fn get_server_rules(&mut self, engine: &Engine, protocol: u8) -> GDResult> { - let mut buffer = self.get_request_data(engine, protocol, Request::Rules)?; + let mut buffer = self.get_kind_request_data(engine, protocol, Request::Rules)?; let count = buffer.get_u16()? as usize; let mut rules: HashMap = HashMap::with_capacity(count); diff --git a/src/protocols/valve/types.rs b/src/protocols/valve/types.rs index 5515ca6..82edeb2 100644 --- a/src/protocols/valve/types.rs +++ b/src/protocols/valve/types.rs @@ -1,5 +1,8 @@ use std::collections::HashMap; +use crate::bufferer::Bufferer; +use crate::GDError::UnknownEnumCast; +use crate::GDResult; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -12,6 +15,17 @@ pub enum Server { TV, } +impl Server { + pub(crate) fn from_gldsrc(value: u8) -> GDResult { + Ok(match value { + 100 => Server::Dedicated, //'d' + 108 => Server::NonDedicated, //'l' + 112 => Server::TV, //'p' + _ => Err(UnknownEnumCast)?, + }) + } +} + /// The Operating System that the server is on. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -21,6 +35,17 @@ pub enum Environment { Mac, } +impl Environment { + pub(crate) fn from_gldsrc(value: u8) -> GDResult { + Ok(match value { + 108 => Environment::Linux, //'l' + 119 => Environment::Windows, //'w' + 109 | 111 => Environment::Mac, //'m' or 'o' + _ => Err(UnknownEnumCast)?, + }) + } +} + /// A query response. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq)] @@ -142,6 +167,40 @@ pub(crate) fn get_optional_extracted_data(data: Option) -> ExtractedD } } +#[derive(Debug, Clone)] +pub(crate) struct Packet { + pub header: u32, + pub kind: u8, + pub payload: Vec, +} + +impl Packet { + pub fn new(kind: u8, payload: Vec) -> Self { + Self { + header: 4294967295, // FF FF FF FF + kind, + payload, + } + } + + pub fn new_from_bufferer(buffer: &mut Bufferer) -> GDResult { + Ok(Self { + header: buffer.get_u32()?, + kind: buffer.get_u8()?, + payload: buffer.remaining_data_vec(), + }) + } + + pub fn to_bytes(&self) -> Vec { + let mut buf = Vec::from(self.header.to_be_bytes()); + + buf.push(self.kind); + buf.extend(&self.payload); + + buf + } +} + /// The type of the request, see the [protocol](https://developer.valvesoftware.com/wiki/Server_queries). #[derive(Eq, PartialEq, Copy, Clone)] #[repr(u8)] @@ -154,6 +213,15 @@ pub(crate) enum Request { Rules = 0x56, } +impl Request { + pub fn get_default_payload(&self) -> Vec { + match self { + Request::Info => String::from("Source Engine Query\0").into_bytes(), + _ => vec![0xFF, 0xFF, 0xFF, 0xFF], + } + } +} + /// Supported steam apps #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]