[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:
CosminPerRam 2023-06-12 19:38:34 +03:00 committed by GitHub
parent 80637f2398
commit 26ad1f5d19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 250 additions and 7 deletions

View file

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

View file

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

View file

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

View file

@ -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
View 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)
}

View file

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

View file

@ -1,2 +1,3 @@
pub mod one;
pub mod three;
pub mod two;

View file

@ -0,0 +1,5 @@
pub mod protocol;
pub mod types;
pub use protocol::*;
pub use types::*;

View 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,
})
}

View 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>,
}