[Protocol] GameSpy 1 support with the games Unreal Tournament and Battlefield 1942. (#9)

* Initial files + unreal tournament

* Fix master_querant

* Split by delimiter and collect into hashmap

* Furter port to accept more packets

* Improve getting the server's values

* Some initial players parsing

* Players parsing

* Add error handling

* Add some more fields

* Add Battlefield 1942 support.

* Add query_vars and some docs
This commit is contained in:
CosminPerRam 2023-03-03 17:45:18 +02:00 committed by GitHub
parent 5604436553
commit 950c08c18e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 276 additions and 5 deletions

View file

@ -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.

View file

@ -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:
_

View file

@ -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) <br> 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) <br> 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:
_

View file

@ -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<String> = 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])
};

10
src/games/bf1942.rs Normal file
View file

@ -0,0 +1,10 @@
use crate::GDResult;
use crate::protocols::gamespy;
use crate::protocols::gamespy::Response;
pub fn query(address: &str, port: Option<u16>) -> GDResult<Response> {
gamespy::one::query(address, match port {
None => 23000,
Some(port) => port
}, None)
}

View file

@ -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;

10
src/games/ut.rs Normal file
View file

@ -0,0 +1,10 @@
use crate::GDResult;
use crate::protocols::gamespy;
use crate::protocols::gamespy::Response;
pub fn query(address: &str, port: Option<u16>) -> GDResult<Response> {
gamespy::one::query(address, match port {
None => 7778,
Some(port) => port
}, None)
}

View file

@ -0,0 +1,8 @@
/// The implementation.
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use types::*;
pub use protocol::*;

View file

@ -0,0 +1,3 @@
/// GameSpy 1
pub mod one;

View file

@ -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<TimeoutSettings>) -> GDResult<HashMap<String, String>> {
let mut socket = UdpSocket::new(address, port)?;
socket.apply_timeout(timeout_settings)?;
socket.send("\\status\\xserverquery".as_bytes())?;
let mut received_query_id: Option<usize> = None;
let mut parts: Vec<usize> = 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<String> = 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<String, String>, players_maximum: usize) -> GDResult<Vec<Player>> {
let mut players_data: Vec<HashMap<String, String>> = 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<Player> = 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<String, String>) -> GDResult<bool> {
let password_value = server_vars.remove("password").ok_or(GDError::PacketBad)?.to_lowercase();
if let Ok(has) = password_value.parse::<bool>() {
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<TimeoutSettings>) -> GDResult<HashMap<String, String>> {
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<TimeoutSettings>) -> GDResult<Response> {
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
})
}

View file

@ -0,0 +1,36 @@
use std::collections::HashMap;
/// A players 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<u32>,
pub health: Option<u32>,
pub secret: bool
}
/// A query response.
#[derive(Debug)]
pub struct Response {
pub name: String,
pub map: String,
pub map_title: Option<String>,
pub admin_contact: Option<String>,
pub admin_name: Option<String>,
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<Player>,
pub tournament: bool,
pub unused_entries: HashMap<String, String>
}

View file

@ -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;