diff --git a/CHANGELOG.md b/CHANGELOG.md index f65d885..d0832d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,16 @@ the protocols/services, also saves storage space). Games: - [V Rising](https://store.steampowered.com/app/1604030/V_Rising/) support. +- [Unreal Tournament](https://en.wikipedia.org/wiki/Unreal_Tournament) support. +- [Battlefield 1942](https://www.ea.com/games/battlefield/battlefield-1942) support. Protocols: - Valve: 1. Reversed (from `0.1.0`) "Players with no name are no more added to the `players_details` field.", also added a note in the [protocols](PROTOCOLS.md) file regarding this. 2. Fixed querying while multiple challenge responses might happen. +- GameSpy 1 support. + ### Breaking: None. diff --git a/GAMES.md b/GAMES.md index d6e4629..f0e9723 100644 --- a/GAMES.md +++ b/GAMES.md @@ -45,6 +45,8 @@ Beware of the `Notes` column, as it contains information about query port offset | 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. | ## Planned to add support: _ diff --git a/PROTOCOLS.md b/PROTOCOLS.md index 35b8a0f..53f5482 100644 --- a/PROTOCOLS.md +++ b/PROTOCOLS.md @@ -1,10 +1,11 @@ A protocol is defined as proprietary if it is being used only for a single scope (or series, like Minecraft). # Supported protocols: -| Name | For | Proprietary? | Documentation reference | Notes | -|----------------|-------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------| -| Valve Protocol | Games | No | [Server Queries](https://developer.valvesoftware.com/wiki/Server_queries) | In some cases, the players details query might contain some 0-length named players. Multi-packet decompression not tested. | -| Minecraft | Games | Yes | Java: [List Server Protocol](https://wiki.vg/Server_List_Ping)
Bedrock: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/minecraftbedrock.js) | | +| Name | For | Proprietary? | Documentation reference | Notes | +|----------------|-------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Valve Protocol | Games | No | [Server Queries](https://developer.valvesoftware.com/wiki/Server_queries) | In some cases, the players details query might contain some 0-length named players. Multi-packet decompression not tested. | +| Minecraft | Games | Yes | Java: [List Server Protocol](https://wiki.vg/Server_List_Ping)
Bedrock: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/minecraftbedrock.js) | | +| GameSpy | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy1.js) | These protocols are not really standardized, gamedig tries to get the most common fields amongst its supported games, if there are parsing problems, use the `query_vars` function. | ## Planned to add support: _ diff --git a/examples/master_querant.rs b/examples/master_querant.rs index 2311564..9e85693 100644 --- a/examples/master_querant.rs +++ b/examples/master_querant.rs @@ -1,9 +1,10 @@ use std::env; -use gamedig::{aliens, aoc, arma2oa, ase, asrd, avorion, bat1944, bb2, bm, bo, ccure, cosu, cs, cscz, csgo, css, dod, dods, doi, dst, GDResult, gm, hl2dm, hldms, ins, insmic, inss, l4d, l4d2, mc, ohd, onset, pz, ror2, rust, sc, sdtd, tf, tf2, tfc, ts, unturned, vr}; +use gamedig::{aliens, aoc, arma2oa, ase, asrd, avorion, bat1944, bb2, bf1942, bm, bo, ccure, cosu, cs, cscz, csgo, css, dod, dods, doi, dst, GDResult, gm, hl2dm, hldms, ins, insmic, inss, l4d, l4d2, mc, ohd, onset, pz, ror2, rust, sc, sdtd, tf, tf2, tfc, ts, unturned, ut, vr}; use gamedig::protocols::minecraft::LegacyGroup; use gamedig::protocols::valve; use gamedig::protocols::valve::Engine; +use gamedig::protocols::gamespy; fn main() -> GDResult<()> { let args: Vec = env::args().collect(); @@ -83,6 +84,10 @@ fn main() -> GDResult<()> { "avorion" => println!("{:#?}", avorion::query(ip, port)?), "ohd" => println!("{:#?}", ohd::query(ip, port)?), "vr" => println!("{:#?}", vr::query(ip, port)?), + "_gamespy1" => println!("{:#?}", gamespy::one::query(ip, port.unwrap(), None)), + "_gamespy1_vars" => println!("{:#?}", gamespy::one::query_vars(ip, port.unwrap(), None)), + "ut" => println!("{:#?}", ut::query(ip, port)), + "bf1942" => println!("{:#?}", bf1942::query(ip, port)), _ => panic!("Undefined game: {}", args[1]) }; diff --git a/src/games/bf1942.rs b/src/games/bf1942.rs new file mode 100644 index 0000000..f61a9d9 --- /dev/null +++ b/src/games/bf1942.rs @@ -0,0 +1,10 @@ +use crate::GDResult; +use crate::protocols::gamespy; +use crate::protocols::gamespy::Response; + +pub fn query(address: &str, port: Option) -> GDResult { + gamespy::one::query(address, match port { + None => 23000, + Some(port) => port + }, None) +} diff --git a/src/games/mod.rs b/src/games/mod.rs index 893fbc7..225cfab 100644 --- a/src/games/mod.rs +++ b/src/games/mod.rs @@ -85,3 +85,7 @@ pub mod avorion; pub mod ohd; /// V Rising pub mod vr; +/// Unreal Tournament +pub mod ut; +/// Battlefield 1942 +pub mod bf1942; diff --git a/src/games/ut.rs b/src/games/ut.rs new file mode 100644 index 0000000..486b590 --- /dev/null +++ b/src/games/ut.rs @@ -0,0 +1,10 @@ +use crate::GDResult; +use crate::protocols::gamespy; +use crate::protocols::gamespy::Response; + +pub fn query(address: &str, port: Option) -> GDResult { + gamespy::one::query(address, match port { + None => 7778, + Some(port) => port + }, None) +} diff --git a/src/protocols/gamespy/mod.rs b/src/protocols/gamespy/mod.rs new file mode 100644 index 0000000..8ed7a9b --- /dev/null +++ b/src/protocols/gamespy/mod.rs @@ -0,0 +1,8 @@ + +/// The implementation. +pub mod protocol; +/// All types used by the implementation. +pub mod types; + +pub use types::*; +pub use protocol::*; diff --git a/src/protocols/gamespy/protocol/mod.rs b/src/protocols/gamespy/protocol/mod.rs new file mode 100644 index 0000000..afcffc5 --- /dev/null +++ b/src/protocols/gamespy/protocol/mod.rs @@ -0,0 +1,3 @@ + +/// GameSpy 1 +pub mod one; diff --git a/src/protocols/gamespy/protocol/one.rs b/src/protocols/gamespy/protocol/one.rs new file mode 100644 index 0000000..97d42f0 --- /dev/null +++ b/src/protocols/gamespy/protocol/one.rs @@ -0,0 +1,186 @@ +use std::collections::HashMap; +use crate::bufferer::{Bufferer, Endianess}; +use crate::{GDError, GDResult}; +use crate::protocols::gamespy::{Player, Response}; +use crate::protocols::types::TimeoutSettings; +use crate::socket::{Socket, UdpSocket}; + +fn get_server_values(address: &str, port: u16, timeout_settings: Option) -> GDResult> { + let mut socket = UdpSocket::new(address, port)?; + socket.apply_timeout(timeout_settings)?; + + socket.send("\\status\\xserverquery".as_bytes())?; + + let mut received_query_id: Option = None; + let mut parts: Vec = Vec::new(); + let mut is_finished = false; + + let mut server_values = HashMap::new(); + + while !is_finished { + let data = socket.receive(None)?; + let mut bufferer = Bufferer::new_with_data(Endianess::Little, &data); + + let mut as_string = bufferer.get_string_utf8_unended()?; + as_string.remove(0); + + let splited: Vec = as_string.split('\\').map(str::to_string).collect(); + + for i in 0..splited.len() / 2 { + let position = i * 2; + let key = splited[position].clone(); + let value = match splited.get(position + 1) { + None => "".to_string(), + Some(v) => v.clone() + }; + + server_values.insert(key, value); + } + + is_finished = server_values.contains_key("final"); + server_values.remove("final"); + + let query_data = server_values.get("queryid"); + + let mut part = parts.len(); //if the part number isn't provided, it's value is the parts length + let mut query_id = None; + if let Some(qid) = query_data { + let split: Vec<&str> = qid.split('.').collect(); + + query_id = Some(split[0].parse().map_err(|_| GDError::TypeParse)?); + match split.len() { + 1 => (), + 2 => part = split[1].parse().map_err(|_| GDError::TypeParse)?, + _ => Err(GDError::PacketBad)? //the queryid can't be splitted in more than 2 elements + }; + } + + server_values.remove("queryid"); + + if received_query_id.is_some() && received_query_id != query_id { + return Err(GDError::PacketBad); //wrong query id! + } + else { + received_query_id = query_id; + } + + match parts.contains(&part) { + true => Err(GDError::PacketBad)?, + false => parts.push(part) + } + } + + Ok(server_values) +} + +fn extract_players(server_vars: &mut HashMap, players_maximum: usize) -> GDResult> { + let mut players_data: Vec> = Vec::with_capacity(players_maximum); + + server_vars.retain(|key, value| { + let split: Vec<&str> = key.split('_').collect(); + + if split.len() != 2 { + return true; + } + + let kind = split[0]; + let id: usize = match split[1].parse() { + Ok(v) => v, + Err(_) => return true + }; + + let early_return = match kind { + "team" | "player" | "ping" | "face" | "skin" | "mesh" | "frags" | "ngsecret" | "deaths" | "health" => false, + _x => { + //println!("UNKNOWN {id} {x} {value}"); + true + } + }; + + if early_return { + return true; + } + + if id >= players_data.len() { + let others = vec![HashMap::new(); id - players_data.len() + 1]; + players_data.extend_from_slice(&others); + } + players_data[id].insert(kind.to_string(), value.to_string()); + + false + }); + + let mut players: Vec = Vec::with_capacity(players_data.len()); + + for player_data in players_data { + let new_player = Player { + name: match player_data.get("player") { + Some(v) => v.clone(), + None => player_data.get("playername").ok_or(GDError::PacketBad)?.clone() + }, + team: player_data.get("team").ok_or(GDError::PacketBad)?.trim().parse().map_err(|_| GDError::TypeParse)?, + ping: player_data.get("ping").ok_or(GDError::PacketBad)?.trim().parse().map_err(|_| GDError::TypeParse)?, + face: player_data.get("face").ok_or(GDError::PacketBad)?.clone(), + skin: player_data.get("skin").ok_or(GDError::PacketBad)?.clone(), + mesh: player_data.get("mesh").ok_or(GDError::PacketBad)?.clone(), + frags: player_data.get("frags").ok_or(GDError::PacketBad)?.trim().parse().map_err(|_| GDError::TypeParse)?, + deaths: match player_data.get("deaths") { + Some(v) => Some(v.trim().parse().map_err(|_| GDError::TypeParse)?), + None => None + }, + health: match player_data.get("health") { + Some(v) => Some(v.trim().parse().map_err(|_| GDError::TypeParse)?), + None => None + }, + secret: player_data.get("ngsecret").ok_or(GDError::PacketBad)?.to_lowercase().parse().map_err(|_| GDError::TypeParse)?, + }; + + players.push(new_player); + } + + Ok(players) +} + +fn has_password(server_vars: &mut HashMap) -> GDResult { + let password_value = server_vars.remove("password").ok_or(GDError::PacketBad)?.to_lowercase(); + + if let Ok(has) = password_value.parse::() { + return Ok(has); + } + + let as_numeral: u8 = password_value.parse().map_err(|_| GDError::TypeParse)?; + + Ok(as_numeral != 0) +} + +/// If there are parsing problems using the `query` function, you can directly get the server's values using this function. +pub fn query_vars(address: &str, port: u16, timeout_settings: Option) -> GDResult> { + get_server_values(address, port, timeout_settings) +} + +/// Query a server by providing the address, the port and timeout settings. +/// Providing None to the timeout settings results in using the default values. (TimeoutSettings::[default](TimeoutSettings::default)). +pub fn query(address: &str, port: u16, timeout_settings: Option) -> GDResult { + let mut server_vars = query_vars(address, port, timeout_settings)?; + + let players_maximum = server_vars.remove("maxplayers").ok_or(GDError::PacketBad)?.parse().map_err(|_| GDError::TypeParse)?; + + let players = extract_players(&mut server_vars, players_maximum)?; + + Ok(Response { + name: server_vars.remove("hostname").ok_or(GDError::PacketBad)?, + map: server_vars.remove("mapname").ok_or(GDError::PacketBad)?, + map_title: server_vars.remove("maptitle"), + admin_contact: server_vars.remove("AdminEMail"), + admin_name: server_vars.remove("AdminName"), + has_password: has_password(&mut server_vars)?, + game_type: server_vars.remove("gametype").ok_or(GDError::PacketBad)?, + game_version: server_vars.remove("gamever").ok_or(GDError::PacketBad)?, + players_maximum, + players_online: players.len(), + players_minimum: server_vars.remove("minplayers").unwrap_or("0".to_string()).parse().map_err(|_| GDError::TypeParse)?, + players, + tournament: server_vars.remove("tournament").unwrap_or("true".to_string()).to_lowercase().parse().map_err(|_| GDError::TypeParse)?, + unused_entries: server_vars + }) +} diff --git a/src/protocols/gamespy/types.rs b/src/protocols/gamespy/types.rs new file mode 100644 index 0000000..db4ed3e --- /dev/null +++ b/src/protocols/gamespy/types.rs @@ -0,0 +1,36 @@ +use std::collections::HashMap; + +/// A player’s details. +#[derive(Debug)] +pub struct Player { + pub name: String, + pub team: u8, + /// The ping from the server's perspective. + pub ping: u16, + pub face: String, + pub skin: String, + pub mesh: String, + pub frags: u32, + pub deaths: Option, + pub health: Option, + pub secret: bool +} + +/// A query response. +#[derive(Debug)] +pub struct Response { + pub name: String, + pub map: String, + pub map_title: Option, + pub admin_contact: Option, + pub admin_name: Option, + pub has_password: bool, + pub game_type: String, + pub game_version: String, + pub players_maximum: usize, + pub players_online: usize, + pub players_minimum: u8, + pub players: Vec, + pub tournament: bool, + pub unused_entries: HashMap +} diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs index 08f3ec2..e6e4fac 100644 --- a/src/protocols/mod.rs +++ b/src/protocols/mod.rs @@ -10,3 +10,5 @@ pub mod types; pub mod valve; /// Reference: [Server List Ping](https://wiki.vg/Server_List_Ping) pub mod minecraft; +/// Reference: [node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy1.js) +pub mod gamespy;