[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

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