mirror of
https://github.com/tribufu/rust-gamedig
synced 2026-06-01 09:42:41 +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:
|
Games:
|
||||||
- [V Rising](https://store.steampowered.com/app/1604030/V_Rising/) support.
|
- [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:
|
Protocols:
|
||||||
- Valve:
|
- 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.
|
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.
|
2. Fixed querying while multiple challenge responses might happen.
|
||||||
|
|
||||||
|
- GameSpy 1 support.
|
||||||
|
|
||||||
### Breaking:
|
### Breaking:
|
||||||
None.
|
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. |
|
| Avorion | AVORION | Valve Protocol | Query port is 27020. |
|
||||||
| Operation: Harsh Doorstop | OHD | Valve Protocol | Query port is 27005. |
|
| Operation: Harsh Doorstop | OHD | Valve Protocol | Query port is 27005. |
|
||||||
| V Rising | VR | Valve Protocol | Query port is 27016. |
|
| 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:
|
## Planned to add support:
|
||||||
_
|
_
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@ A protocol is defined as proprietary if it is being used only for a single scope
|
||||||
|
|
||||||
# Supported protocols:
|
# Supported protocols:
|
||||||
| Name | For | Proprietary? | Documentation reference | Notes |
|
| 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. |
|
| 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) | |
|
| 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:
|
## Planned to add support:
|
||||||
_
|
_
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
|
|
||||||
use std::env;
|
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::minecraft::LegacyGroup;
|
||||||
use gamedig::protocols::valve;
|
use gamedig::protocols::valve;
|
||||||
use gamedig::protocols::valve::Engine;
|
use gamedig::protocols::valve::Engine;
|
||||||
|
use gamedig::protocols::gamespy;
|
||||||
|
|
||||||
fn main() -> GDResult<()> {
|
fn main() -> GDResult<()> {
|
||||||
let args: Vec<String> = env::args().collect();
|
let args: Vec<String> = env::args().collect();
|
||||||
|
|
@ -83,6 +84,10 @@ fn main() -> GDResult<()> {
|
||||||
"avorion" => println!("{:#?}", avorion::query(ip, port)?),
|
"avorion" => println!("{:#?}", avorion::query(ip, port)?),
|
||||||
"ohd" => println!("{:#?}", ohd::query(ip, port)?),
|
"ohd" => println!("{:#?}", ohd::query(ip, port)?),
|
||||||
"vr" => println!("{:#?}", vr::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])
|
_ => 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;
|
pub mod ohd;
|
||||||
/// V Rising
|
/// V Rising
|
||||||
pub mod vr;
|
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;
|
pub mod valve;
|
||||||
/// Reference: [Server List Ping](https://wiki.vg/Server_List_Ping)
|
/// Reference: [Server List Ping](https://wiki.vg/Server_List_Ping)
|
||||||
pub mod minecraft;
|
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