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
|
||||
### Changes:
|
||||
To be made...
|
||||
Protocols:
|
||||
- GameSpy 2 support.
|
||||
|
||||
Games:
|
||||
- [Halo: Combat Evolved](https://en.wikipedia.org/wiki/Halo:_Combat_Evolved) support.
|
||||
|
||||
### Breaking...
|
||||
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 | |
|
||||
| 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 | |
|
||||
| Halo: Combat Evolved | HALOCE | GameSpy 2 | |
|
||||
|
||||
## 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).
|
||||
|
||||
# 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) | |
|
||||
| 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. |
|
||||
| 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) | |
|
||||
| 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) 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) | |
|
||||
|
||||
## Planned to add support:
|
||||
_
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ use gamedig::{
|
|||
dst,
|
||||
ffow,
|
||||
gm,
|
||||
haloce,
|
||||
hl2dm,
|
||||
hldms,
|
||||
hll,
|
||||
|
|
@ -186,6 +187,8 @@ fn main() -> GDResult<()> {
|
|||
"quake3a" => println!("{:#?}", quake3a::query(ip, port)?),
|
||||
"hll" => println!("{:#?}", hll::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]),
|
||||
};
|
||||
|
||||
|
|
|
|||
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;
|
||||
/// Garry's Mod
|
||||
pub mod gm;
|
||||
/// Halo: Combat Evolved
|
||||
pub mod haloce;
|
||||
/// Half-Life 2 Deathmatch
|
||||
pub mod hl2dm;
|
||||
/// Half-Life Deathmatch: Source
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
pub mod one;
|
||||
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