mirror of
https://github.com/tribufu/rust-gamedig
synced 2026-06-01 09:42:41 +00:00
[Protocol] Add GameSpy 2 support. (#47)
* [Protocol] Add initial files * [Protocol] Add test to test the request * [Protocol] Add initial query response type * [Protocol] Parse teams * [Protocol] Add players parse and add nice macro * [Protocol] Add proper derives to structs * [Protocol] Change to get all informations from one request * [Protocol] Add Halo: CE support and update CHANGELOG.md * [Protocol] Remove a .clone usage * [Protocol] Add todo comment regarding code performance * [Protocol] Use iterator instead of index range
This commit is contained in:
parent
80637f2398
commit
26ad1f5d19
10 changed files with 250 additions and 7 deletions
|
|
@ -3,7 +3,11 @@ Who knows what the future holds...
|
||||||
|
|
||||||
# 0.X.Y - DD/MM/2023
|
# 0.X.Y - DD/MM/2023
|
||||||
### Changes:
|
### Changes:
|
||||||
To be made...
|
Protocols:
|
||||||
|
- GameSpy 2 support.
|
||||||
|
|
||||||
|
Games:
|
||||||
|
- [Halo: Combat Evolved](https://en.wikipedia.org/wiki/Halo:_Combat_Evolved) support.
|
||||||
|
|
||||||
### Breaking...
|
### Breaking...
|
||||||
Crate:
|
Crate:
|
||||||
|
|
|
||||||
1
GAMES.md
1
GAMES.md
|
|
@ -55,6 +55,7 @@ Beware of the `Notes` column, as it contains information about query port offset
|
||||||
| Quake 3: Arena | QUAKE3A | Quake 3 | |
|
| Quake 3: Arena | QUAKE3A | Quake 3 | |
|
||||||
| Hell Let Loose | HLL | Valve Protocol | Query port is 26420. Note that on this port it might not send players data, as there might be another query port that does send players data. |
|
| Hell Let Loose | HLL | Valve Protocol | Query port is 26420. Note that on this port it might not send players data, as there might be another query port that does send players data. |
|
||||||
| Soldier of Fortune 2 | SOF2 | Quake 3 | |
|
| Soldier of Fortune 2 | SOF2 | Quake 3 | |
|
||||||
|
| Halo: Combat Evolved | HALOCE | GameSpy 2 | |
|
||||||
|
|
||||||
## Planned to add support:
|
## Planned to add support:
|
||||||
_
|
_
|
||||||
|
|
|
||||||
12
PROTOCOLS.md
12
PROTOCOLS.md
|
|
@ -1,12 +1,12 @@
|
||||||
A protocol is defined as proprietary if it is being used only for a single scope (or series, like Minecraft).
|
A protocol is defined as proprietary if it is being used only for a single scope (or series, like Minecraft).
|
||||||
|
|
||||||
# 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) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy3.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. |
|
| GameSpy | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy1.js) Two: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy2.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy3.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. |
|
||||||
| Quake | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake1.js) Two: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake2.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake3.js) | |
|
| Quake | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake1.js) Two: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake2.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake3.js) | |
|
||||||
|
|
||||||
## Planned to add support:
|
## Planned to add support:
|
||||||
_
|
_
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ use gamedig::{
|
||||||
dst,
|
dst,
|
||||||
ffow,
|
ffow,
|
||||||
gm,
|
gm,
|
||||||
|
haloce,
|
||||||
hl2dm,
|
hl2dm,
|
||||||
hldms,
|
hldms,
|
||||||
hll,
|
hll,
|
||||||
|
|
@ -186,6 +187,8 @@ fn main() -> GDResult<()> {
|
||||||
"quake3a" => println!("{:#?}", quake3a::query(ip, port)?),
|
"quake3a" => println!("{:#?}", quake3a::query(ip, port)?),
|
||||||
"hll" => println!("{:#?}", hll::query(ip, port)?),
|
"hll" => println!("{:#?}", hll::query(ip, port)?),
|
||||||
"sof2" => println!("{:#?}", sof2::query(ip, port)?),
|
"sof2" => println!("{:#?}", sof2::query(ip, port)?),
|
||||||
|
"_gamespy2" => println!("{:#?}", gamespy::two::query(address, None)),
|
||||||
|
"haloce" => println!("{:#?}", haloce::query(ip, port)?),
|
||||||
_ => panic!("Undefined game: {}", args[1]),
|
_ => panic!("Undefined game: {}", args[1]),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
8
src/games/haloce.rs
Normal file
8
src/games/haloce.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
use crate::protocols::gamespy;
|
||||||
|
use crate::protocols::gamespy::two::Response;
|
||||||
|
use crate::GDResult;
|
||||||
|
use std::net::{IpAddr, SocketAddr};
|
||||||
|
|
||||||
|
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> {
|
||||||
|
gamespy::two::query(&SocketAddr::new(*address, port.unwrap_or(2302)), None)
|
||||||
|
}
|
||||||
|
|
@ -48,6 +48,8 @@ pub mod dst;
|
||||||
pub mod ffow;
|
pub mod ffow;
|
||||||
/// Garry's Mod
|
/// Garry's Mod
|
||||||
pub mod gm;
|
pub mod gm;
|
||||||
|
/// Halo: Combat Evolved
|
||||||
|
pub mod haloce;
|
||||||
/// Half-Life 2 Deathmatch
|
/// Half-Life 2 Deathmatch
|
||||||
pub mod hl2dm;
|
pub mod hl2dm;
|
||||||
/// Half-Life Deathmatch: Source
|
/// Half-Life Deathmatch: Source
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
pub mod one;
|
pub mod one;
|
||||||
pub mod three;
|
pub mod three;
|
||||||
|
pub mod two;
|
||||||
|
|
|
||||||
5
src/protocols/gamespy/protocols/two/mod.rs
Normal file
5
src/protocols/gamespy/protocols/two/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod protocol;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
pub use protocol::*;
|
||||||
|
pub use types::*;
|
||||||
185
src/protocols/gamespy/protocols/two/protocol.rs
Normal file
185
src/protocols/gamespy/protocols/two/protocol.rs
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
use crate::bufferer::{Bufferer, Endianess};
|
||||||
|
use crate::protocols::gamespy::two::{Player, Response, Team};
|
||||||
|
use crate::protocols::types::TimeoutSettings;
|
||||||
|
use crate::socket::{Socket, UdpSocket};
|
||||||
|
use crate::{GDError, GDResult};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
struct GameSpy2 {
|
||||||
|
socket: UdpSocket,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! table_extract {
|
||||||
|
($table:expr, $name:literal, $index:expr) => {
|
||||||
|
$table
|
||||||
|
.get($name)
|
||||||
|
.ok_or(GDError::PacketBad)?
|
||||||
|
.get($index)
|
||||||
|
.ok_or(GDError::PacketBad)?
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! table_extract_parse {
|
||||||
|
($table:expr, $name:literal, $index:expr) => {
|
||||||
|
table_extract!($table, $name, $index)
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| GDError::PacketBad)?
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data_as_table(data: &mut Bufferer) -> GDResult<(HashMap<String, Vec<String>>, usize)> {
|
||||||
|
if data.get_u8()? != 0 {
|
||||||
|
Err(GDError::PacketBad)?
|
||||||
|
}
|
||||||
|
|
||||||
|
let rows = data.get_u8()? as usize;
|
||||||
|
|
||||||
|
if rows == 0 {
|
||||||
|
return Ok((HashMap::new(), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut column_heads = Vec::new();
|
||||||
|
|
||||||
|
let mut current_column = data.get_string_utf8()?;
|
||||||
|
while !current_column.is_empty() {
|
||||||
|
column_heads.push(current_column);
|
||||||
|
current_column = data.get_string_utf8()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let columns = column_heads.len();
|
||||||
|
let mut table = HashMap::with_capacity(columns);
|
||||||
|
for head in &column_heads {
|
||||||
|
table.insert(head.clone(), Vec::new()); // TODO: This doesn't look good nor it is performant, fix later
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in 0 .. rows {
|
||||||
|
for column in column_heads.iter() {
|
||||||
|
let value = data.get_string_utf8()?;
|
||||||
|
table.get_mut(column).ok_or(GDError::PacketBad)?.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((table, rows))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameSpy2 {
|
||||||
|
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
|
||||||
|
let socket = UdpSocket::new(address)?;
|
||||||
|
socket.apply_timeout(timeout_settings)?;
|
||||||
|
|
||||||
|
Ok(Self { socket })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_data(&mut self) -> GDResult<Bufferer> {
|
||||||
|
self.socket
|
||||||
|
.send(&[0xFE, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF])?;
|
||||||
|
|
||||||
|
let received = self.socket.receive(None)?;
|
||||||
|
let mut buf = Bufferer::new_with_data(Endianess::Big, &received);
|
||||||
|
|
||||||
|
if buf.get_u8()? != 0 {
|
||||||
|
return Err(GDError::PacketBad);
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf.get_u32()? != 1 {
|
||||||
|
return Err(GDError::PacketBad);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_server_vars(bufferer: &mut Bufferer) -> GDResult<HashMap<String, String>> {
|
||||||
|
let mut values = HashMap::new();
|
||||||
|
|
||||||
|
let mut done_processing_vars = false;
|
||||||
|
while !done_processing_vars && bufferer.remaining_length() > 0 {
|
||||||
|
let key = bufferer.get_string_utf8()?;
|
||||||
|
let value = bufferer.get_string_utf8_optional()?;
|
||||||
|
|
||||||
|
if key.is_empty() {
|
||||||
|
if value.is_empty() {
|
||||||
|
bufferer.move_position_backward(1);
|
||||||
|
done_processing_vars = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
values.insert(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_teams(bufferer: &mut Bufferer) -> GDResult<Vec<Team>> {
|
||||||
|
let mut teams = Vec::new();
|
||||||
|
|
||||||
|
let (table, entries) = data_as_table(bufferer)?;
|
||||||
|
|
||||||
|
for index in 0 .. entries {
|
||||||
|
teams.push(Team {
|
||||||
|
name: table_extract!(table, "team_t", index).clone(),
|
||||||
|
score: table_extract_parse!(table, "score_t", index),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(teams)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_players(bufferer: &mut Bufferer) -> GDResult<Vec<Player>> {
|
||||||
|
let mut players = Vec::new();
|
||||||
|
|
||||||
|
let (table, entries) = data_as_table(bufferer)?;
|
||||||
|
|
||||||
|
for index in 0 .. entries {
|
||||||
|
players.push(Player {
|
||||||
|
name: table_extract!(table, "player_", index).clone(),
|
||||||
|
score: table_extract_parse!(table, "score_", index),
|
||||||
|
ping: table_extract_parse!(table, "ping_", index),
|
||||||
|
team_index: table_extract_parse!(table, "team_", index),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(players)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
|
||||||
|
let mut client = GameSpy2::new(address, timeout_settings)?;
|
||||||
|
let mut data = client.request_data()?;
|
||||||
|
|
||||||
|
let mut server_vars = get_server_vars(&mut data)?;
|
||||||
|
let players = get_players(&mut data)?;
|
||||||
|
|
||||||
|
let players_online = match server_vars.remove("numplayers") {
|
||||||
|
None => players.len(),
|
||||||
|
Some(v) => {
|
||||||
|
let reported_players = v.parse().map_err(|_| GDError::TypeParse)?;
|
||||||
|
match reported_players < players.len() {
|
||||||
|
true => players.len(),
|
||||||
|
false => reported_players,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let players_minimum = match server_vars.remove("minplayers") {
|
||||||
|
None => None,
|
||||||
|
Some(v) => Some(v.parse::<u8>().map_err(|_| GDError::TypeParse)?),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Response {
|
||||||
|
name: server_vars.remove("hostname").ok_or(GDError::PacketBad)?,
|
||||||
|
map: server_vars.remove("mapname").ok_or(GDError::PacketBad)?,
|
||||||
|
has_password: server_vars.remove("password").ok_or(GDError::PacketBad)? == "1",
|
||||||
|
teams: get_teams(&mut data)?,
|
||||||
|
players_maximum: server_vars
|
||||||
|
.remove("maxplayers")
|
||||||
|
.ok_or(GDError::PacketBad)?
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| GDError::PacketBad)?,
|
||||||
|
players_online,
|
||||||
|
players_minimum,
|
||||||
|
players,
|
||||||
|
unused_entries: server_vars,
|
||||||
|
})
|
||||||
|
}
|
||||||
34
src/protocols/gamespy/protocols/two/types.rs
Normal file
34
src/protocols/gamespy/protocols/two/types.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
pub struct Team {
|
||||||
|
pub name: String,
|
||||||
|
pub score: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
pub struct Player {
|
||||||
|
pub name: String,
|
||||||
|
pub score: u16,
|
||||||
|
pub ping: u16,
|
||||||
|
pub team_index: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Response {
|
||||||
|
pub name: String,
|
||||||
|
pub map: String,
|
||||||
|
pub has_password: bool,
|
||||||
|
pub teams: Vec<Team>,
|
||||||
|
pub players_maximum: usize,
|
||||||
|
pub players_online: usize,
|
||||||
|
pub players_minimum: Option<u8>,
|
||||||
|
pub players: Vec<Player>,
|
||||||
|
pub unused_entries: HashMap<String, String>,
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue