diff --git a/CHANGELOG.md b/CHANGELOG.md index 529a821..464fe93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ Who knows what the future holds... +# 0.0.5 - ??/??/2022 +Support for GoldSrc split packets and obsolete A2S_INFO response. +Changed the Valve Protocol app parameter to represent the engine responses. +It is now an enum of: +- `Source(Option)` - A Source response with optionally, the id (if the id is present and the response id is not the same, the query fails) +- `GoldSrc(bool)` - A GoldSrc response with the option to enforce the obsolete A2S_INFO response. + +[Counter-Strike: Condition Zero](https://store.steampowered.com/app/80/CounterStrike_Condition_Zero/) implementation. +[Day of Defeat](https://store.steampowered.com/app/30/Day_of_Defeat/) implementation. +Games besides CSGO and TS now have the same response structure. + # 0.0.4 - 23/10/2022 Queries now support DNS resolve. Changed uses a bit, example: from `use gamedig::valve::ValveProtocol::query` to `use gamedig::protocols::valve::query`. diff --git a/GAMES.md b/GAMES.md index a332f47..f8c2657 100644 --- a/GAMES.md +++ b/GAMES.md @@ -13,8 +13,10 @@ | ALIENS | Alien Swarm | Valve Protocol | Not tested. | | ASRD | Alien Swarm: Reactive Drop | Valve Protocol | | | INS | Insurgency | Valve Protocol | | -| INSS | Insurgency: Sandstorm | Valve Protocol | Here you need to use the query port, not the server port. | +| INSS | Insurgency: Sandstorm | Valve Protocol | Use the query port, not the server port. | | INSMIC | Insurgency: Modern Infantry Combat | Valve Protocol | Not tested. | +| CSCZ | Counter-Strike: Condition Zero | Valve Protocol | | +| DOD | Day of Defeat | Valve Protocol | | ## Planned to add support: All Valve titles. diff --git a/PROTOCOLS.md b/PROTOCOLS.md index c83528f..60fee65 100644 --- a/PROTOCOLS.md +++ b/PROTOCOLS.md @@ -1,8 +1,8 @@ # Supported protocols: -| Name | Documentation reference | Used by | Notes | -|----------------|---------------------------------------------------------------------------|---------------------------------------------------------------------------------|----------------------------------------| -| Valve Protocol | [Server Queries](https://developer.valvesoftware.com/wiki/Server_queries) | TF2, CSGO, TS, CSS, DODS, GM, HL2DM, L4D, L4D2, ALIENS, ASRD, INS, INSS, INSMIC | Multi-packet decompression not tested. | +| Name | Documentation reference | Notes | +|----------------|---------------------------------------------------------------------------|----------------------------------------| +| Valve Protocol | [Server Queries](https://developer.valvesoftware.com/wiki/Server_queries) | Multi-packet decompression not tested. | ## Planned to add support: Minecraft protocol diff --git a/examples/master_querant.rs b/examples/master_querant.rs index 72a961d..4270f0c 100644 --- a/examples/master_querant.rs +++ b/examples/master_querant.rs @@ -1,9 +1,10 @@ use std::env; -use gamedig::{aliens, asrd, csgo, css, dods, gm, hl2dm, ins, insmic, inss, l4d, l4d2, tf2, ts}; +use gamedig::{aliens, asrd, cscz, csgo, css, dod, dods, GDResult, gm, hl2dm, ins, insmic, inss, l4d, l4d2, tf2, ts}; use gamedig::protocols::valve; +use gamedig::protocols::valve::App; -fn main() { +fn main() -> GDResult<()> { let args: Vec = env::args().collect(); if args.len() == 1 || args[1] == "help".to_string() { @@ -11,10 +12,10 @@ fn main() { println!(" - any game, example: tf2"); println!(" - an ip, example: 192.168.0.0"); println!(" - an port, optional, example: 27015"); - return; + return Ok(()); } else if args.len() < 3 { println!("Minimum number of arguments: 3, try 'help' to see the details."); - return; + return Ok(()); } let ip = args[2].as_str(); @@ -24,21 +25,27 @@ fn main() { }; match args[1].as_str() { - "aliens" => println!("{:?}", aliens::query(ip, port)), - "asrd" => println!("{:?}", asrd::query(ip, port)), - "csgo" => println!("{:?}", csgo::query(ip, port)), - "css" => println!("{:?}", css::query(ip, port)), - "dods" => println!("{:?}", dods::query(ip, port)), - "gm" => println!("{:?}", gm::query(ip, port)), - "hl2dm" => println!("{:?}", hl2dm::query(ip, port)), - "tf2" => println!("{:?}", tf2::query(ip, port)), - "insmic" => println!("{:?}", insmic::query(ip, port)), - "ins" => println!("{:?}", ins::query(ip, port)), - "inss" => println!("{:?}", inss::query(ip, port)), - "l4d" => println!("{:?}", l4d::query(ip, port)), - "l4d2" => println!("{:?}", l4d2::query(ip, port)), - "ts" => println!("{:?}", ts::query(ip, port)), - "_" => println!("{:?}", valve::query(ip, 27015, None, None)), + "aliens" => println!("{:?}", aliens::query(ip, port)?), + "asrd" => println!("{:?}", asrd::query(ip, port)?), + "csgo" => println!("{:?}", csgo::query(ip, port)?), + "css" => println!("{:?}", css::query(ip, port)?), + "dods" => println!("{:?}", dods::query(ip, port)?), + "gm" => println!("{:?}", gm::query(ip, port)?), + "hl2dm" => println!("{:?}", hl2dm::query(ip, port)?), + "tf2" => println!("{:?}", tf2::query(ip, port)?), + "insmic" => println!("{:?}", insmic::query(ip, port)?), + "ins" => println!("{:?}", ins::query(ip, port)?), + "inss" => println!("{:?}", inss::query(ip, port)?), + "l4d" => println!("{:?}", l4d::query(ip, port)?), + "l4d2" => println!("{:?}", l4d2::query(ip, port)?), + "ts" => println!("{:?}", ts::query(ip, port)?), + "cscz" => println!("{:?}", cscz::query(ip, port)?), + "dod" => println!("{:?}", dod::query(ip, port)?), + "_src" => println!("{:?}", valve::query(ip, 27015, App::Source(None), None)?), + "_gld" => println!("{:?}", valve::query(ip, 27015, App::GoldSrc(false), None)?), + "_gld_f" => println!("{:?}", valve::query(ip, 27015, App::GoldSrc(true), None)?), _ => panic!("Undefined game: {}", args[1]) }; + + Ok(()) } diff --git a/examples/tf2.rs b/examples/tf2.rs index 728f7ef..eb8c70a 100644 --- a/examples/tf2.rs +++ b/examples/tf2.rs @@ -2,7 +2,7 @@ use gamedig::games::tf2; fn main() { - let response = tf2::query("91.216.250.10", None); //or Some(27015), None is the default protocol port (which is 27015) + let response = tf2::query("cosminperram.com", None); //or Some(27015), None is the default protocol port (which is 27015) match response { Err(error) => println!("Couldn't query, error: {error}"), Ok(r) => println!("{:?}", r) diff --git a/src/errors.rs b/src/errors.rs index 6a7049f..68e1528 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,10 +1,13 @@ + +//! The library's possible errors. + use core::fmt; use std::fmt::Formatter; /// Result of Type and GDError. pub type GDResult = Result; -/// GameDigError, every error you can encounter using the library. +/// GameDigError. #[derive(Debug, Clone)] pub enum GDError { /// The received packet was bigger than the buffer size. diff --git a/src/games/aliens.rs b/src/games/aliens.rs index 05bad88..e5ace35 100644 --- a/src/games/aliens.rs +++ b/src/games/aliens.rs @@ -1,12 +1,12 @@ use crate::GDResult; use crate::protocols::valve; -use crate::protocols::valve::{App, game}; +use crate::protocols::valve::{game, SteamID}; pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, Some(App::ALIENS), None)?; + }, SteamID::ALIENS.app(), None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/asrd.rs b/src/games/asrd.rs index 52c87d9..32432c3 100644 --- a/src/games/asrd.rs +++ b/src/games/asrd.rs @@ -1,12 +1,12 @@ use crate::GDResult; use crate::protocols::valve; -use crate::protocols::valve::{App, game}; +use crate::protocols::valve::{game, SteamID}; pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, Some(App::ASRD), None)?; + }, SteamID::ASRD.app(), None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/cscz.rs b/src/games/cscz.rs new file mode 100644 index 0000000..ac11aa9 --- /dev/null +++ b/src/games/cscz.rs @@ -0,0 +1,12 @@ +use crate::GDResult; +use crate::protocols::valve; +use crate::protocols::valve::{game, SteamID}; + +pub fn query(address: &str, port: Option) -> GDResult { + let valve_response = valve::query(address, match port { + None => 27015, + Some(port) => port + }, SteamID::CSCZ.app(), None)?; + + Ok(game::Response::new_from_valve_response(valve_response)) +} diff --git a/src/games/csgo.rs b/src/games/csgo.rs index 4b18678..3f4785e 100644 --- a/src/games/csgo.rs +++ b/src/games/csgo.rs @@ -1,6 +1,6 @@ use crate::GDResult; use crate::protocols::valve; -use crate::protocols::valve::{App, Server, GatheringSettings, get_optional_extracted_data}; +use crate::protocols::valve::{Server, GatheringSettings, get_optional_extracted_data, SteamID}; use crate::protocols::valve::game::Player; #[derive(Debug)] @@ -54,7 +54,7 @@ pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, Some(App::CSGO), Some(GatheringSettings { + }, SteamID::CSGO.app(), Some(GatheringSettings { players: true, rules: false // cause csgo doesnt reply with rules anymore }))?; diff --git a/src/games/css.rs b/src/games/css.rs index 004b870..cc45ebe 100644 --- a/src/games/css.rs +++ b/src/games/css.rs @@ -1,12 +1,12 @@ use crate::GDResult; use crate::protocols::valve; -use crate::protocols::valve::{App, game}; +use crate::protocols::valve::{game, SteamID}; pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, Some(App::CSS), None)?; + }, SteamID::CSS.app(), None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/dod.rs b/src/games/dod.rs new file mode 100644 index 0000000..136541c --- /dev/null +++ b/src/games/dod.rs @@ -0,0 +1,12 @@ +use crate::GDResult; +use crate::protocols::valve; +use crate::protocols::valve::{game, SteamID}; + +pub fn query(address: &str, port: Option) -> GDResult { + let valve_response = valve::query(address, match port { + None => 27015, + Some(port) => port + }, SteamID::DOD.app(), None)?; + + Ok(game::Response::new_from_valve_response(valve_response)) +} diff --git a/src/games/dods.rs b/src/games/dods.rs index 4b3bfc1..fa4c772 100644 --- a/src/games/dods.rs +++ b/src/games/dods.rs @@ -1,12 +1,12 @@ use crate::GDResult; use crate::protocols::valve; -use crate::protocols::valve::{App, game}; +use crate::protocols::valve::{game, SteamID}; pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, Some(App::DODS), None)?; + }, SteamID::DODS.app(), None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/gm.rs b/src/games/gm.rs index ff61d43..6d9a0ee 100644 --- a/src/games/gm.rs +++ b/src/games/gm.rs @@ -1,12 +1,12 @@ use crate::GDResult; use crate::protocols::valve; -use crate::protocols::valve::{App, game}; +use crate::protocols::valve::{game, SteamID}; pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, Some(App::GM), None)?; + }, SteamID::GM.app(), None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/hl2dm.rs b/src/games/hl2dm.rs index f84ab0c..42e7fae 100644 --- a/src/games/hl2dm.rs +++ b/src/games/hl2dm.rs @@ -1,12 +1,12 @@ use crate::GDResult; use crate::protocols::valve; -use crate::protocols::valve::{App, game}; +use crate::protocols::valve::{game, SteamID}; pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, Some(App::HL2DM), None)?; + }, SteamID::HL2DM.app(), None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/ins.rs b/src/games/ins.rs index bd51377..f1091ec 100644 --- a/src/games/ins.rs +++ b/src/games/ins.rs @@ -1,12 +1,12 @@ use crate::GDResult; use crate::protocols::valve; -use crate::protocols::valve::{App, game}; +use crate::protocols::valve::{game, SteamID}; pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, Some(App::INS), None)?; + }, SteamID::INS.app(), None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/insmic.rs b/src/games/insmic.rs index 123e9c9..6e1a869 100644 --- a/src/games/insmic.rs +++ b/src/games/insmic.rs @@ -1,12 +1,12 @@ use crate::GDResult; use crate::protocols::valve; -use crate::protocols::valve::{App, game}; +use crate::protocols::valve::{game, SteamID}; pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, Some(App::INSMIC), None)?; + }, SteamID::INSMIC.app(), None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/inss.rs b/src/games/inss.rs index e696c5d..47ae02d 100644 --- a/src/games/inss.rs +++ b/src/games/inss.rs @@ -1,12 +1,12 @@ use crate::GDResult; use crate::protocols::valve; -use crate::protocols::valve::{App, game}; +use crate::protocols::valve::{game, SteamID}; pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27131, Some(port) => port - }, Some(App::INSS), None)?; + }, SteamID::INSS.app(), None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/l4d.rs b/src/games/l4d.rs index f024be0..3054aaa 100644 --- a/src/games/l4d.rs +++ b/src/games/l4d.rs @@ -1,12 +1,12 @@ use crate::GDResult; use crate::protocols::valve; -use crate::protocols::valve::{App, game}; +use crate::protocols::valve::{game, SteamID}; pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, Some(App::L4D), None)?; + }, SteamID::L4D.app(), None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/l4d2.rs b/src/games/l4d2.rs index 108bd84..32074f4 100644 --- a/src/games/l4d2.rs +++ b/src/games/l4d2.rs @@ -1,12 +1,12 @@ use crate::GDResult; use crate::protocols::valve; -use crate::protocols::valve::{App, game}; +use crate::protocols::valve::{game, SteamID}; pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, Some(App::L4D2), None)?; + }, SteamID::L4D2.app(), None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/mod.rs b/src/games/mod.rs index f9f48c9..6ea01c2 100644 --- a/src/games/mod.rs +++ b/src/games/mod.rs @@ -29,3 +29,7 @@ pub mod ins; pub mod inss; /// Insurgency: Modern Infantry Combat pub mod insmic; +/// Counter Strike: Condition Zero +pub mod cscz; +/// Day of Defeat +pub mod dod; diff --git a/src/games/tf2.rs b/src/games/tf2.rs index a695ec2..256eb4b 100644 --- a/src/games/tf2.rs +++ b/src/games/tf2.rs @@ -1,12 +1,12 @@ use crate::GDResult; use crate::protocols::valve; -use crate::protocols::valve::{App, game}; +use crate::protocols::valve::{game, SteamID}; pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, Some(App::TF2), None)?; + }, SteamID::TF2.app(), None)?; Ok(game::Response::new_from_valve_response(valve_response)) } diff --git a/src/games/ts.rs b/src/games/ts.rs index ad7ee8c..80b0f05 100644 --- a/src/games/ts.rs +++ b/src/games/ts.rs @@ -1,6 +1,6 @@ use crate::GDResult; use crate::protocols::valve; -use crate::protocols::valve::{App, Server, ServerRule, ServerPlayer, get_optional_extracted_data}; +use crate::protocols::valve::{Server, ServerRule, ServerPlayer, get_optional_extracted_data, SteamID}; #[derive(Debug)] pub struct TheShipPlayer { @@ -84,7 +84,7 @@ pub fn query(address: &str, port: Option) -> GDResult { let valve_response = valve::query(address, match port { None => 27015, Some(port) => port - }, Some(App::TS), None)?; + }, SteamID::TS.app(), None)?; Ok(Response::new_from_valve_response(valve_response)) } diff --git a/src/protocols/valve/mod.rs b/src/protocols/valve/mod.rs index ebc8f9f..44527b4 100644 --- a/src/protocols/valve/mod.rs +++ b/src/protocols/valve/mod.rs @@ -1,4 +1,7 @@ + +/// The implementation. pub mod protocol; +/// All types used by the implementation. pub mod types; pub use protocol::*; diff --git a/src/protocols/valve/protocol.rs b/src/protocols/valve/protocol.rs index e5ca9c1..4589848 100644 --- a/src/protocols/valve/protocol.rs +++ b/src/protocols/valve/protocol.rs @@ -1,8 +1,9 @@ use std::net::UdpSocket; use bzip2_rs::decoder::Decoder; use crate::{GDError, GDResult}; -use crate::protocols::valve::types::{App, Environment, ExtraData, GatheringSettings, Request, Response, Server, ServerInfo, ServerPlayer, ServerRule, TheShip}; -use crate::utils::{buffer, complete_address}; +use crate::protocols::valve::{App, ModData, SteamID}; +use crate::protocols::valve::types::{Environment, ExtraData, GatheringSettings, Request, Response, Server, ServerInfo, ServerPlayer, ServerRule, TheShip}; +use crate::utils::{buffer, complete_address, u8_lower_upper}; #[derive(Debug, Clone)] struct Packet { @@ -73,18 +74,27 @@ struct SplitPacket { } impl SplitPacket { - fn new(_appid: u32, buf: &[u8]) -> GDResult { + fn new(app: &App, buf: &[u8]) -> GDResult { let mut pos = 0; let header = buffer::get_u32_le(&buf, &mut pos)?; let id = buffer::get_u32_le(&buf, &mut pos)?; - let total = buffer::get_u8(&buf, &mut pos)?; - let number = buffer::get_u8(&buf, &mut pos)?; - let size = buffer::get_u16_le(&buf, &mut pos)?; //if game is CSS and if protocol is 7, queries with multi-packet responses will crash - let compressed = ((id >> 31) & 1) == 1; - let (decompressed_size, uncompressed_crc32) = match compressed { - false => (None, None), - true => (Some(buffer::get_u32_le(&buf, &mut pos)?), Some(buffer::get_u32_le(&buf, &mut pos)?)) + let (total, number, size, compressed, decompressed_size, uncompressed_crc32) = match app { + App::GoldSrc(_) => { + let (lower, upper) = u8_lower_upper(buffer::get_u8(&buf, &mut pos)?); + (lower, upper, 0, false, None, None) + } + App::Source(_) => { + let total = buffer::get_u8(&buf, &mut pos)?; + let number = buffer::get_u8(&buf, &mut pos)?; + let size = buffer::get_u16_le(&buf, &mut pos)?; //if game is CSS and if protocol is 7, queries with multi-packet responses will crash + let compressed = ((id >> 31) & 1) == 1; + let (decompressed_size, uncompressed_crc32) = match compressed { + false => (None, None), + true => (Some(buffer::get_u32_le(&buf, &mut pos)?), Some(buffer::get_u32_le(&buf, &mut pos)?)) + }; + (total, number, size, compressed, decompressed_size, uncompressed_crc32) + } }; Ok(Self { @@ -100,8 +110,8 @@ impl SplitPacket { }) } - fn decompress(&self) -> GDResult> { - if !self.compressed { + fn get_payload(&self) -> GDResult> { + if self.compressed { let mut decoder = Decoder::new(); decoder.write(&self.payload).map_err(|e| GDError::Decompress(e.to_string()))?; @@ -119,14 +129,6 @@ impl SplitPacket { else { Ok(decompressed_payload) } - } else { //already decompressed - Ok(self.payload.clone()) - } - } - - fn get_payload(&self) -> GDResult> { - if self.compressed { - Ok(self.decompress()?) } else { Ok(self.payload.clone()) } @@ -164,15 +166,15 @@ impl ValveProtocol { Ok(buf[..amt].to_vec()) } - fn receive(&self, appid: u32, buffer_size: usize) -> GDResult { + fn receive(&self, app: &App, buffer_size: usize) -> GDResult { let mut buf = self.receive_raw(buffer_size)?; if buf[0] == 0xFE { //the packet is split - let mut main_packet = SplitPacket::new(appid, &buf)?; + let mut main_packet = SplitPacket::new(&app, &buf)?; for _ in 1..main_packet.total { buf = self.receive_raw(buffer_size)?; - let chunk_packet = SplitPacket::new(appid, &buf)?; + let chunk_packet = SplitPacket::new(&app, &buf)?; main_packet.payload.extend(chunk_packet.payload); } @@ -184,11 +186,11 @@ impl ValveProtocol { } /// Ask for a specific request only. - fn get_request_data(&self, appid: u32, kind: Request) -> GDResult> { + fn get_request_data(&self, app: &App, kind: Request) -> GDResult> { let request_initial_packet = Packet::initial(kind.clone()).to_bytes(); self.send(&request_initial_packet)?; - let packet = self.receive(appid, DEFAULT_PACKET_SIZE)?; + let packet = self.receive(app, DEFAULT_PACKET_SIZE)?; if packet.kind != 0x41 { //'A' return Ok(packet.payload.clone()); @@ -198,12 +200,79 @@ impl ValveProtocol { let challenge_packet = Packet::challenge(kind.clone(), challenge).to_bytes(); self.send(&challenge_packet)?; - Ok(self.receive(appid, DEFAULT_PACKET_SIZE)?.payload) + Ok(self.receive(app, DEFAULT_PACKET_SIZE)?.payload) + } + + fn get_goldsrc_server_info(buf: &[u8]) -> GDResult { + let mut pos = 0; + + buffer::get_u8(&buf, &mut pos)?; //get the header (useless info) + buffer::get_string(&buf, &mut pos)?; //get the server address (useless info) + let name = buffer::get_string(&buf, &mut pos)?; + let map = buffer::get_string(&buf, &mut pos)?; + let folder = buffer::get_string(&buf, &mut pos)?; + let game = buffer::get_string(&buf, &mut pos)?; + let players = buffer::get_u8(&buf, &mut pos)?; + let max_players = buffer::get_u8(&buf, &mut pos)?; + let protocol = buffer::get_u8(&buf, &mut pos)?; + let server_type = match buffer::get_u8(&buf, &mut pos)? { + 68 => Server::Dedicated, //'D' + 76 => Server::NonDedicated, //'L' + 80 => Server::TV, //'P' + _ => Err(GDError::UnknownEnumCast)? + }; + let environment_type = match buffer::get_u8(&buf, &mut pos)? { + 76 => Environment::Linux, //'L' + 87 => Environment::Windows, //'W' + _ => Err(GDError::UnknownEnumCast)? + }; + let has_password = buffer::get_u8(&buf, &mut pos)? == 1; + let is_mod = buffer::get_u8(&buf, &mut pos)? == 1; + let mod_data = match is_mod { + false => None, + true => Some(ModData { + link: buffer::get_string(&buf, &mut pos)?, + download_link: buffer::get_string(&buf, &mut pos)?, + version: buffer::get_u32_le(&buf, &mut pos)?, + size: buffer::get_u32_le(&buf, &mut pos)?, + multiplayer_only: buffer::get_u8(&buf, &mut pos)? == 1, + has_own_dll: buffer::get_u8(&buf, &mut pos)? == 1 + }) + }; + let vac_secured = buffer::get_u8(&buf, &mut pos)? == 1; + let bots = buffer::get_u8(&buf, &mut pos)?; + + Ok(ServerInfo { + protocol, + name, + map, + folder, + game, + appid: 0, //not present in the obsolete response + players, + max_players, + bots, + server_type, + environment_type, + has_password, + vac_secured, + the_ship: None, + version: "".to_string(), //a version field only for the mod + extra_data: None, + is_mod, + mod_data + }) } /// Get the server information's. - fn get_server_info(&self, initial_appid: u32) -> GDResult { - let buf = self.get_request_data(initial_appid, Request::INFO)?; + fn get_server_info(&self, app: &App) -> GDResult { + let buf = self.get_request_data(&app, Request::INFO)?; + if let App::GoldSrc(force) = app { + if *force { + return ValveProtocol::get_goldsrc_server_info(&buf); + } + } + let mut pos = 0; let protocol = buffer::get_u8(&buf, &mut pos)?; @@ -218,7 +287,7 @@ impl ValveProtocol { let server_type = match buffer::get_u8(&buf, &mut pos)? { 100 => Server::Dedicated, //'d' 108 => Server::NonDedicated, //'l' - 112 => Server::SourceTV, //'p' + 112 => Server::TV, //'p' _ => Err(GDError::UnknownEnumCast)? }; let environment_type = match buffer::get_u8(&buf, &mut pos)? { @@ -229,7 +298,7 @@ impl ValveProtocol { }; let has_password = buffer::get_u8(&buf, &mut pos)? == 1; let vac_secured = buffer::get_u8(&buf, &mut pos)? == 1; - let the_ship = match appid == App::TS as u32 { + let the_ship = match *app == SteamID::TS.app() { false => None, true => Some(TheShip { mode: buffer::get_u8(&buf, &mut pos)?, @@ -289,13 +358,15 @@ impl ValveProtocol { vac_secured, the_ship, version, - extra_data + extra_data, + is_mod: false, + mod_data: None }) } /// Get the server player's. - fn get_server_players(&self, appid: u32) -> GDResult> { - let buf = self.get_request_data(appid, Request::PLAYERS)?; + fn get_server_players(&self, app: &App) -> GDResult> { + let buf = self.get_request_data(&app, Request::PLAYERS)?; let mut pos = 0; let count = buffer::get_u8(&buf, &mut pos)?; @@ -307,11 +378,11 @@ impl ValveProtocol { name: buffer::get_string(&buf, &mut pos)?, score: buffer::get_u32_le(&buf, &mut pos)?, duration: buffer::get_f32_le(&buf, &mut pos)?, - deaths: match appid == App::TS as u32 { + deaths: match *app == SteamID::TS.app() { false => None, true => Some(buffer::get_u32_le(&buf, &mut pos)?) }, - money: match appid == App::TS as u32 { + money: match *app == SteamID::TS.app() { false => None, true => Some(buffer::get_u32_le(&buf, &mut pos)?) } @@ -322,12 +393,12 @@ impl ValveProtocol { } /// Get the server rules's. - fn get_server_rules(&self, appid: u32) -> GDResult>> { - if appid == App::CSGO as u32 { //cause csgo wont respond to this since feb 21 2014 update + fn get_server_rules(&self, app: &App) -> GDResult>> { + if *app == SteamID::CSGO.app() { //cause csgo wont respond to this since feb 21 2014 update return Ok(None); } - let buf = self.get_request_data(appid, Request::RULES)?; + let buf = self.get_request_data(&app, Request::RULES)?; let mut pos = 0; let count = buffer::get_u16_le(&buf, &mut pos)?; @@ -344,25 +415,19 @@ impl ValveProtocol { } } -/// Query a server, you need to provide the address, the port and optionally, the app and the -/// gather settings, the app being *None* means to anonymously query the server, and the gather -/// settings being *None* means to get the players and the rules. -pub fn query(address: &str, port: u16, app: Option, gather_settings: Option) -> Result { +/// Query a server by providing the address, the port, the app and the gather settings, the settings +/// being *None* means to also get the players and the rules. +pub fn query(address: &str, port: u16, app: App, gather_settings: Option) -> GDResult { let client = ValveProtocol::new(address, port)?; - let mut query_app_id = match app { - None => 0, - Some(app) => app as u32 - }; + let info = client.get_server_info(&app)?; - let info = client.get_server_info(query_app_id)?; - - if query_app_id != 0 { - if info.appid != query_app_id { - return Err(GDError::BadGame(format!("Expected {}, found {} instead!", query_app_id, info.appid))); + if let App::Source(x) = &app { + if let Some(appid) = x { + if *appid != info.appid { + return Err(GDError::BadGame(format!("Expected {}, found {} instead!", *appid, info.appid))); + } } - } else { - query_app_id = info.appid; } let (gather_players, gather_rules) = match gather_settings.is_some() { @@ -377,11 +442,11 @@ pub fn query(address: &str, port: u16, app: Option, gather_settings: Option info, players: match gather_players { false => None, - true => Some(client.get_server_players(query_app_id)?) + true => Some(client.get_server_players(&app)?) }, rules: match gather_rules { false => None, - true => client.get_server_rules(query_app_id)? + true => client.get_server_rules(&app)? } }) } diff --git a/src/protocols/valve/types.rs b/src/protocols/valve/types.rs index b06c793..6d612d5 100644 --- a/src/protocols/valve/types.rs +++ b/src/protocols/valve/types.rs @@ -1,12 +1,10 @@ -//! All types used by the Valve protocol - /// The type of the server. #[derive(Debug)] pub enum Server { Dedicated, NonDedicated, - SourceTV + TV } /// The Operating System that the server is on. @@ -50,16 +48,20 @@ pub struct ServerInfo { pub server_type: Server, /// The Operating System that the server is on. pub environment_type: Environment, - /// Indicated whether the server requires a password. + /// Indicates whether the server requires a password. pub has_password: bool, - /// Indicated whether the server uses VAC. + /// 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, /// Version of the game installed on the server. pub version: String, /// Some extra data that the server might provide or not. - pub extra_data: Option + 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 } /// A server player. @@ -109,6 +111,16 @@ pub struct ExtraData { pub game_id: Option } +#[derive(Debug)] +pub struct ModData { + pub link: String, + pub download_link: String, + pub version: u32, + pub size: u32, + pub multiplayer_only: bool, + pub has_own_dll: bool +} + pub fn get_optional_extracted_data(data: Option) -> (Option, Option, Option, Option, Option) { match data { None => (None, None, None, None, None), @@ -128,9 +140,13 @@ pub enum Request { RULES = 0x56 } -/// Supported app id's +/// Supported steam apps id's #[derive(PartialEq, Clone)] -pub enum App { +pub enum SteamID { + /// Day of Defeat + DOD = 30, + /// Counter-Strike: Condition Zero + CSCZ = 80, /// Counter-Strike: Source CSS = 240, /// Day of Defeat: Source @@ -161,6 +177,27 @@ pub enum App { ASRD = 563560, } +impl SteamID { + pub fn app(self) -> App { + match self { + SteamID::CSCZ | SteamID::DOD => App::GoldSrc(false), + x => App::Source(Some(x as u32)) + } + } +} + +/// App type +#[derive(PartialEq, Clone)] +pub enum App { + /// A Source game, the argument represents the wanted response steam app id, if its **None**, + /// let the query find it, if its **Some**, the query fails if the response id is not the + /// specified one. + Source(Option), + /// A GoldSrc game, the argument indicates whether to enforce getting the obsolete A2S_INFO + /// goldsrc response or not. + GoldSrc(bool) +} + /// What data to gather, purely used only with the query function. pub struct GatheringSettings { pub players: bool, diff --git a/src/utils.rs b/src/utils.rs index 3cfdf1d..6ca978b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -16,6 +16,10 @@ pub fn complete_address(address: &str, port: u16) -> GDResult { Ok(resolve_dns(address)? + ":" + &*port.to_string()) } +pub fn u8_lower_upper(n: u8) -> (u8, u8) { + (n & 15, n >> 4) +} + pub mod buffer { use super::*;