diff --git a/GAMES.md b/GAMES.md index 63e1d14..e5ce055 100644 --- a/GAMES.md +++ b/GAMES.md @@ -1,15 +1,17 @@ # Supported games: -| ID | Name | Protocol | Notes | -|-------|----------------------------------|----------------|------------------------------------------------------------------------------| -| TF2 | Team Fortress 2 | Valve Protocol | | -| TS | The Ship | Valve Protocol | | -| CSGO | Counter-Strike: Global Offensive | Valve Protocol | The server wouldn't respond the to Rules query since the 21 Feb 2014 update. | -| CSS | Counter-Strike: Source | Valve Protocol | If protocol is 7, queries with multi-packet responses will crash. | -| DODS | Day of Defeat: Source | Valve Protocol | | -| L4D | Left 4 Dead | Valve Protocol | | -| L4D2 | Left 4 Dead 2 | Valve Protocol | | -| HL2DM | Half-Life 2 Deathmatch | Valve Protocol | | +| ID | Name | Protocol | Notes | +|--------|----------------------------------|----------------|------------------------------------------------------------------------------| +| TF2 | Team Fortress 2 | Valve Protocol | | +| TS | The Ship | Valve Protocol | | +| CSGO | Counter-Strike: Global Offensive | Valve Protocol | The server wouldn't respond the to Rules query since the 21 Feb 2014 update. | +| CSS | Counter-Strike: Source | Valve Protocol | If protocol is 7, queries with multi-packet responses will crash. | +| DODS | Day of Defeat: Source | Valve Protocol | | +| L4D | Left 4 Dead | Valve Protocol | | +| L4D2 | Left 4 Dead 2 | Valve Protocol | | +| HL2DM | Half-Life 2 Deathmatch | Valve Protocol | | +| ALIENS | Alien Swarm | Valve Protocol | Not tested. | +| ASRD | Alien Swarm: Reactive Drop | Valve Protocol | | ## Planned to add support: All Valve titles. diff --git a/PROTOCOLS.md b/PROTOCOLS.md index ddb2a63..a729dab 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 | Multi-packet decompression not tested. | +| 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 | Multi-packet decompression not tested. | ## Planned to add support: Minecraft protocol diff --git a/examples/asrd.rs b/examples/asrd.rs new file mode 100644 index 0000000..d0256a3 --- /dev/null +++ b/examples/asrd.rs @@ -0,0 +1,10 @@ + +use gamedig::games::asrd; + +fn main() { + let response = asrd::query("5.199.135.237", Some(30000)); + match response { + Err(error) => println!("Couldn't query, error: {error}"), + Ok(r) => println!("{:?}", r) + } +} diff --git a/src/games/aliens.rs b/src/games/aliens.rs new file mode 100644 index 0000000..17838cc --- /dev/null +++ b/src/games/aliens.rs @@ -0,0 +1,83 @@ +use crate::{GDResult, valve}; +use crate::valve::{ValveProtocol, App, GatheringSettings, Server, ServerRule, ServerPlayer}; + +#[derive(Debug)] +pub struct Player { + pub name: String, + pub score: u32, + pub duration: f32 +} + +impl Player { + fn from_valve_response(player: &ServerPlayer) -> Self { + Self { + name: player.name.clone(), + score: player.score, + duration: player.duration + } + } +} + +#[derive(Debug)] +pub struct Response { + pub protocol: u8, + pub name: String, + pub map: String, + pub game: String, + pub players: u8, + pub players_details: Vec, + pub max_players: u8, + pub bots: u8, + pub server_type: Server, + pub has_password: bool, + pub vac_secured: bool, + pub version: String, + pub port: Option, + pub steam_id: Option, + pub tv_port: Option, + pub tv_name: Option, + pub keywords: Option, + pub rules: Vec +} + +impl Response { + pub fn new_from_valve_response(response: valve::Response) -> Self { + let (port, steam_id, tv_port, tv_name, keywords) = match response.info.extra_data { + None => (None, None, None, None, None), + Some(ed) => (ed.port, ed.steam_id, ed.tv_port, ed.tv_name, ed.keywords) + }; + + Self { + protocol: response.info.protocol, + name: response.info.name, + map: response.info.map, + game: response.info.game, + players: response.info.players, + players_details: response.players.unwrap().iter().map(|p| Player::from_valve_response(p)).collect(), + max_players: response.info.max_players, + bots: response.info.bots, + server_type: response.info.server_type, + has_password: response.info.has_password, + vac_secured: response.info.vac_secured, + version: response.info.version, + port, + steam_id, + tv_port, + tv_name, + keywords, + rules: response.rules.unwrap() + } + } +} + +pub fn query(address: &str, port: Option) -> GDResult { + let valve_response = ValveProtocol::query(App::ALIENS, address, match port { + None => 27015, + Some(port) => port + }, GatheringSettings { + players: true, + rules: true + })?; + + Ok(Response::new_from_valve_response(valve_response)) +} diff --git a/src/games/asrd.rs b/src/games/asrd.rs new file mode 100644 index 0000000..a3d3963 --- /dev/null +++ b/src/games/asrd.rs @@ -0,0 +1,83 @@ +use crate::{GDResult, valve}; +use crate::valve::{ValveProtocol, App, GatheringSettings, Server, ServerRule, ServerPlayer}; + +#[derive(Debug)] +pub struct Player { + pub name: String, + pub score: u32, + pub duration: f32 +} + +impl Player { + fn from_valve_response(player: &ServerPlayer) -> Self { + Self { + name: player.name.clone(), + score: player.score, + duration: player.duration + } + } +} + +#[derive(Debug)] +pub struct Response { + pub protocol: u8, + pub name: String, + pub map: String, + pub game: String, + pub players: u8, + pub players_details: Vec, + pub max_players: u8, + pub bots: u8, + pub server_type: Server, + pub has_password: bool, + pub vac_secured: bool, + pub version: String, + pub port: Option, + pub steam_id: Option, + pub tv_port: Option, + pub tv_name: Option, + pub keywords: Option, + pub rules: Vec +} + +impl Response { + pub fn new_from_valve_response(response: valve::Response) -> Self { + let (port, steam_id, tv_port, tv_name, keywords) = match response.info.extra_data { + None => (None, None, None, None, None), + Some(ed) => (ed.port, ed.steam_id, ed.tv_port, ed.tv_name, ed.keywords) + }; + + Self { + protocol: response.info.protocol, + name: response.info.name, + map: response.info.map, + game: response.info.game, + players: response.info.players, + players_details: response.players.unwrap().iter().map(|p| Player::from_valve_response(p)).collect(), + max_players: response.info.max_players, + bots: response.info.bots, + server_type: response.info.server_type, + has_password: response.info.has_password, + vac_secured: response.info.vac_secured, + version: response.info.version, + port, + steam_id, + tv_port, + tv_name, + keywords, + rules: response.rules.unwrap() + } + } +} + +pub fn query(address: &str, port: Option) -> GDResult { + let valve_response = ValveProtocol::query(App::ASRD, address, match port { + None => 27015, + Some(port) => port + }, GatheringSettings { + players: true, + rules: true + })?; + + Ok(Response::new_from_valve_response(valve_response)) +} diff --git a/src/games/mod.rs b/src/games/mod.rs index d5d8e73..82c677c 100644 --- a/src/games/mod.rs +++ b/src/games/mod.rs @@ -1,12 +1,25 @@ //! Currently supported games. +/// Team Fortress 2 pub mod tf2; +/// The Ship pub mod ts; +/// Counter-Strike: Global Offensive pub mod csgo; +/// Counter-Strike: Source pub mod css; +/// Day of Defeat: Source pub mod dods; +/// Garry's Mod pub mod gm; +/// Left 4 Dead pub mod l4d; +/// Left 4 Dead 2 pub mod l4d2; +/// Half-Life 2 Deathmatch pub mod hl2dm; +/// Alien Swarm +pub mod aliens; +/// Alien Swarm: Reactive Drop +pub mod asrd; diff --git a/src/protocols/valve.rs b/src/protocols/valve.rs index 143dc84..fd2b98d 100644 --- a/src/protocols/valve.rs +++ b/src/protocols/valve.rs @@ -41,7 +41,7 @@ pub struct ServerInfo { /// Full name of the game. pub game: String, /// [Steam Application ID](https://developer.valvesoftware.com/wiki/Steam_Application_ID) of game. - pub id: u16, + pub appid: u32, /// Number of players on the server. pub players: u8, /// Maximum number of players the server reports it can hold. @@ -124,7 +124,7 @@ pub enum Request { } /// Supported app id's -#[derive(PartialEq)] +#[derive(PartialEq, Clone)] pub enum App { /// Counter-Strike: Source CSS = 240, @@ -138,31 +138,16 @@ pub enum App { L4D = 500, /// Left 4 Dead L4D2 = 550, + /// Alien Swarm + ALIENS = 630, /// Counter-Strike: Global Offensive CSGO = 730, /// The Ship TS = 2400, /// Garry's Mod GM = 4000, -} - -impl TryFrom for App { - type Error = GDError; - - fn try_from(value: u16) -> GDResult { - match value { - x if x == App::CSS as u16 => Ok(App::CSS), - x if x == App::HL2DM as u16 => Ok(App::HL2DM), - x if x == App::DODS as u16 => Ok(App::DODS), - x if x == App::TF2 as u16 => Ok(App::TF2), - x if x == App::L4D as u16 => Ok(App::L4D), - x if x == App::L4D2 as u16 => Ok(App::L4D2), - x if x == App::CSGO as u16 => Ok(App::CSGO), - x if x == App::TS as u16 => Ok(App::TS), - x if x == App::GM as u16 => Ok(App::GM), - _ => Err(GDError::UnknownEnumCast), - } - } + /// Alien Swarm: Reactive Drop + ASRD = 563560, } /// What data to gather, purely used only with the query function. @@ -373,68 +358,90 @@ impl ValveProtocol { let buf = self.get_request_data(app, Request::INFO)?; let mut pos = 0; - Ok(ServerInfo { - protocol: buffer::get_u8(&buf, &mut pos)?, - name: buffer::get_string(&buf, &mut pos)?, - map: buffer::get_string(&buf, &mut pos)?, - folder: buffer::get_string(&buf, &mut pos)?, - game: buffer::get_string(&buf, &mut pos)?, - id: buffer::get_u16_le(&buf, &mut pos)?, - players: buffer::get_u8(&buf, &mut pos)?, - max_players: buffer::get_u8(&buf, &mut pos)?, - bots: buffer::get_u8(&buf, &mut pos)?, - server_type: match buffer::get_u8(&buf, &mut pos)? { - 100 => Server::Dedicated, //'d' - 108 => Server::NonDedicated, //'l' - 112 => Server::SourceTV, //'p' - _ => Err(GDError::UnknownEnumCast)? - }, - environment_type: match buffer::get_u8(&buf, &mut pos)? { - 108 => Environment::Linux, //'l' - 119 => Environment::Windows, //'w' - 109 | 111 => Environment::Mac, //'m' or 'o' - _ => Err(GDError::UnknownEnumCast)? - }, - has_password: buffer::get_u8(&buf, &mut pos)? == 1, - vac_secured: buffer::get_u8(&buf, &mut pos)? == 1, - the_ship: match *app == App::TS { - false => None, - true => Some(TheShip { - mode: buffer::get_u8(&buf, &mut pos)?, - witnesses: buffer::get_u8(&buf, &mut pos)?, - duration: buffer::get_u8(&buf, &mut pos)? - }) - }, - version: buffer::get_string(&buf, &mut pos)?, - extra_data: match buffer::get_u8(&buf, &mut pos) { - Err(_) => None, - Ok(value) => Some(ExtraData { - port: match (value & 0x80) > 0 { - false => None, - true => Some(buffer::get_u16_le(&buf, &mut pos)?) - }, - steam_id: match (value & 0x10) > 0 { - false => None, - true => Some(buffer::get_u64_le(&buf, &mut pos)?) - }, - tv_port: match (value & 0x40) > 0 { - false => None, - true => Some(buffer::get_u16_le(&buf, &mut pos)?) - }, - tv_name: match (value & 0x40) > 0 { - false => None, - true => Some(buffer::get_string(&buf, &mut pos)?) - }, - keywords: match (value & 0x20) > 0 { - false => None, - true => Some(buffer::get_string(&buf, &mut pos)?) - }, - game_id: match (value & 0x01) > 0 { - false => None, - true => Some(buffer::get_u64_le(&buf, &mut pos)?) + let protocol = buffer::get_u8(&buf, &mut pos)?; + 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 mut appid = buffer::get_u16_le(&buf, &mut pos)? as u32; + let players = buffer::get_u8(&buf, &mut pos)?; + let max_players = buffer::get_u8(&buf, &mut pos)?; + let bots = buffer::get_u8(&buf, &mut pos)?; + let server_type = match buffer::get_u8(&buf, &mut pos)? { + 100 => Server::Dedicated, //'d' + 108 => Server::NonDedicated, //'l' + 112 => Server::SourceTV, //'p' + _ => Err(GDError::UnknownEnumCast)? + }; + let environment_type = match buffer::get_u8(&buf, &mut pos)? { + 108 => Environment::Linux, //'l' + 119 => Environment::Windows, //'w' + 109 | 111 => Environment::Mac, //'m' or 'o' + _ => Err(GDError::UnknownEnumCast)? + }; + let has_password = buffer::get_u8(&buf, &mut pos)? == 1; + let vac_secured = buffer::get_u8(&buf, &mut pos)? == 1; + let the_ship = match *app == App::TS { + false => None, + true => Some(TheShip { + mode: buffer::get_u8(&buf, &mut pos)?, + witnesses: buffer::get_u8(&buf, &mut pos)?, + duration: buffer::get_u8(&buf, &mut pos)? + }) + }; + let version = buffer::get_string(&buf, &mut pos)?; + let extra_data = match buffer::get_u8(&buf, &mut pos) { + Err(_) => None, + Ok(value) => Some(ExtraData { + port: match (value & 0x80) > 0 { + false => None, + true => Some(buffer::get_u16_le(&buf, &mut pos)?) + }, + steam_id: match (value & 0x10) > 0 { + false => None, + true => Some(buffer::get_u64_le(&buf, &mut pos)?) + }, + tv_port: match (value & 0x40) > 0 { + false => None, + true => Some(buffer::get_u16_le(&buf, &mut pos)?) + }, + tv_name: match (value & 0x40) > 0 { + false => None, + true => Some(buffer::get_string(&buf, &mut pos)?) + }, + keywords: match (value & 0x20) > 0 { + false => None, + true => Some(buffer::get_string(&buf, &mut pos)?) + }, + game_id: match (value & 0x01) > 0 { + false => None, + true => { + let gid = buffer::get_u64_le(&buf, &mut pos)?; + appid = (gid & ((1 << 24) - 1)) as u32; + + Some(gid) } - }) - } + } + }) + }; + + Ok(ServerInfo { + protocol, + name, + map, + folder, + game, + appid, + players, + max_players, + bots, + server_type, + environment_type, + has_password, + vac_secured, + the_ship, + version, + extra_data }) } @@ -493,7 +500,10 @@ impl ValveProtocol { let info = client.get_server_info(&app)?; - App::try_from(info.id).map_err(|_| GDError::BadGame(format!("Found {} instead!", info.id)))?; + let query_app_id = app.clone() as u32; + if info.appid != query_app_id { + return Err(GDError::BadGame(format!("Expected {}, found {} instead!", query_app_id, info.appid))); + } Ok(Response { info,