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;