mirror of
https://github.com/tribufu/rust-gamedig
synced 2026-05-06 15:27:28 +00:00
[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:
parent
5604436553
commit
950c08c18e
12 changed files with 276 additions and 5 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
2
GAMES.md
2
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:
|
||||
_
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
_
|
||||
|
|
|
|||
|
|
@ -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
10
src/games/bf1942.rs
Normal 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)
|
||||
}
|
||||
|
|
@ -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
10
src/games/ut.rs
Normal 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)
|
||||
}
|
||||
8
src/protocols/gamespy/mod.rs
Normal file
8
src/protocols/gamespy/mod.rs
Normal 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::*;
|
||||
3
src/protocols/gamespy/protocol/mod.rs
Normal file
3
src/protocols/gamespy/protocol/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
/// GameSpy 1
|
||||
pub mod one;
|
||||
186
src/protocols/gamespy/protocol/one.rs
Normal file
186
src/protocols/gamespy/protocol/one.rs
Normal 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
|
||||
})
|
||||
}
|
||||
36
src/protocols/gamespy/types.rs
Normal file
36
src/protocols/gamespy/types.rs
Normal file
|
|
@ -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<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>
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue