mirror of
https://github.com/tribufu/rust-gamedig
synced 2026-05-06 07:17:27 +00:00
[Protocol] Add GameSpy 3 support. (#25)
* [Protocol] Gamespy3 initial code * [Protocol] Add rest of challenge solving * [Protocol] Remove unused stuff * [Protocol] Remove adding unused bytes * [Protocol] Clean up code * [Protocol] Make gs3 a struct * [Protocol] Add initial key-value parsing * [Protocol] Manage multiple packets * [Protocol] Split server vars and other vars * Revert "[Protocol] Split server vars and other vars" This reverts commit 9a930aeb68802fcf3d0908a2e031dfea054d37d0. * [Protocol] Proper packet management and initial response struct * [Protocol] Fix players_minimum * [Protocol] Fix server vars to parse only the first packet * [Protocol] Update CHANGELOG.md * [Protocol] Initial player parsing * [Protocol] Split GS one and three * [Protocol] Add common code file * [Protocol] Change static to const * [Protocol] Fix players_online and break on data to map on empty key * [Protocol] Remove unused types and printlns * [Protocol] Add teams parsing * [Protocol] Split key_values and parsing data * [Crate] Update PROTOCOLS.md
This commit is contained in:
parent
1b13d39856
commit
786da81ea5
14 changed files with 459 additions and 36 deletions
|
|
@ -9,6 +9,7 @@ Crate:
|
|||
|
||||
Protocols:
|
||||
- GameSpy 1: Add key `admin` as a possible variable for `admin_name`.
|
||||
- GameSpy 3 support.
|
||||
|
||||
Games:
|
||||
- [Serious Sam](https://www.gog.com/game/serious_sam_the_first_encounter) support.
|
||||
|
|
@ -16,6 +17,7 @@ Games:
|
|||
### Breaking:
|
||||
Protocols:
|
||||
- Valve: Request type enums have been renamed from all caps to starting-only uppercase, ex: `INFO` to `Info`
|
||||
- GameSpy 1: `players_minimum` is now an `Option<u8>` instead of an `u8`
|
||||
|
||||
# 0.2.1 - 03/03/2023
|
||||
### Changes:
|
||||
|
|
|
|||
10
PROTOCOLS.md
10
PROTOCOLS.md
|
|
@ -1,11 +1,11 @@
|
|||
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) | 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. |
|
||||
| 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. |
|
||||
|
||||
## Planned to add support:
|
||||
_
|
||||
|
|
|
|||
|
|
@ -165,6 +165,8 @@ fn main() -> GDResult<()> {
|
|||
"ut" => println!("{:#?}", ut::query(ip, port)),
|
||||
"bf1942" => println!("{:#?}", bf1942::query(ip, port)),
|
||||
"ss" => println!("{:#?}", ss::query(ip, port)),
|
||||
"_gamespy3" => println!("{:#?}", gamespy::three::query(ip, port.unwrap(), None)),
|
||||
"_gamespy3_vars" => println!("{:#?}", gamespy::three::query_vars(ip, port.unwrap(), None)),
|
||||
_ => panic!("Undefined game: {}", args[1]),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -108,6 +108,18 @@ impl Bufferer {
|
|||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn get_string_utf8_optional(&mut self) -> GDResult<String> {
|
||||
match self.get_string_utf8() {
|
||||
Ok(data) => Ok(data),
|
||||
Err(e) => {
|
||||
match e {
|
||||
PacketUnderflow => Ok(String::new()),
|
||||
x => Err(x),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_string_utf8_unended(&mut self) -> GDResult<String> {
|
||||
let sub_buf = self.remaining_data();
|
||||
if sub_buf.is_empty() {
|
||||
|
|
|
|||
17
src/protocols/gamespy/common.rs
Normal file
17
src/protocols/gamespy/common.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
use crate::{GDError, GDResult};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub 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)
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
/// The implementation.
|
||||
pub mod protocol;
|
||||
/// All types used by the implementation.
|
||||
pub mod types;
|
||||
mod common;
|
||||
/// The implementations.
|
||||
pub mod protocols;
|
||||
|
||||
pub use protocol::*;
|
||||
pub use types::*;
|
||||
pub use protocols::*;
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
/// GameSpy 1
|
||||
pub mod one;
|
||||
5
src/protocols/gamespy/protocols/mod.rs
Normal file
5
src/protocols/gamespy/protocols/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod one;
|
||||
pub mod three;
|
||||
|
||||
pub use one::*;
|
||||
pub use three::*;
|
||||
5
src/protocols/gamespy/protocols/one/mod.rs
Normal file
5
src/protocols/gamespy/protocols/one/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod protocol;
|
||||
pub mod types;
|
||||
|
||||
pub use protocol::*;
|
||||
pub use types::*;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
bufferer::{Bufferer, Endianess},
|
||||
protocols::{
|
||||
gamespy::{Player, Response},
|
||||
gamespy::one::{Player, Response},
|
||||
types::TimeoutSettings,
|
||||
},
|
||||
socket::{Socket, UdpSocket},
|
||||
|
|
@ -9,6 +9,7 @@ use crate::{
|
|||
GDResult,
|
||||
};
|
||||
|
||||
use crate::protocols::gamespy::common::has_password;
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn get_server_values(
|
||||
|
|
@ -173,21 +174,6 @@ fn extract_players(server_vars: &mut HashMap<String, String>, players_maximum: u
|
|||
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(
|
||||
|
|
@ -209,6 +195,10 @@ pub fn query(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>
|
|||
.ok_or(GDError::PacketBad)?
|
||||
.parse()
|
||||
.map_err(|_| GDError::TypeParse)?;
|
||||
let players_minimum = match server_vars.remove("minplayers") {
|
||||
None => None,
|
||||
Some(v) => Some(v.parse::<u8>().map_err(|_| GDError::TypeParse)?),
|
||||
};
|
||||
|
||||
let players = extract_players(&mut server_vars, players_maximum)?;
|
||||
|
||||
|
|
@ -225,15 +215,11 @@ pub fn query(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>
|
|||
game_version: server_vars.remove("gamever").ok_or(GDError::PacketBad)?,
|
||||
players_maximum,
|
||||
players_online: players.len(),
|
||||
players_minimum: server_vars
|
||||
.remove("minplayers")
|
||||
.unwrap_or_else(|| "0".to_string())
|
||||
.parse()
|
||||
.map_err(|_| GDError::TypeParse)?,
|
||||
players_minimum,
|
||||
players,
|
||||
tournament: server_vars
|
||||
.remove("tournament")
|
||||
.unwrap_or_else(|| "true".to_string())
|
||||
.unwrap_or("true".to_string())
|
||||
.to_lowercase()
|
||||
.parse()
|
||||
.map_err(|_| GDError::TypeParse)?,
|
||||
|
|
@ -34,7 +34,7 @@ pub struct Response {
|
|||
pub game_version: String,
|
||||
pub players_maximum: usize,
|
||||
pub players_online: usize,
|
||||
pub players_minimum: u8,
|
||||
pub players_minimum: Option<u8>,
|
||||
pub players: Vec<Player>,
|
||||
pub tournament: bool,
|
||||
pub unused_entries: HashMap<String, String>,
|
||||
5
src/protocols/gamespy/protocols/three/mod.rs
Normal file
5
src/protocols/gamespy/protocols/three/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod protocol;
|
||||
pub mod types;
|
||||
|
||||
pub use protocol::*;
|
||||
pub use types::*;
|
||||
351
src/protocols/gamespy/protocols/three/protocol.rs
Normal file
351
src/protocols/gamespy/protocols/three/protocol.rs
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
use crate::bufferer::{Bufferer, Endianess};
|
||||
use crate::protocols::gamespy::common::has_password;
|
||||
use crate::protocols::gamespy::three::{Player, Response};
|
||||
use crate::protocols::gamespy::Team;
|
||||
use crate::protocols::types::TimeoutSettings;
|
||||
use crate::socket::{Socket, UdpSocket};
|
||||
use crate::{GDError, GDResult};
|
||||
use std::collections::HashMap;
|
||||
|
||||
const THIS_SESSION_ID: u32 = 1;
|
||||
|
||||
struct RequestPacket {
|
||||
header: u16,
|
||||
kind: u8,
|
||||
session_id: u32,
|
||||
challenge: Option<i32>,
|
||||
payload: Option<[u8; 4]>,
|
||||
}
|
||||
|
||||
impl RequestPacket {
|
||||
fn to_bytes(self) -> Vec<u8> {
|
||||
let mut packet: Vec<u8> = Vec::with_capacity(7);
|
||||
packet.extend_from_slice(&self.header.to_be_bytes());
|
||||
packet.push(self.kind);
|
||||
packet.extend_from_slice(&self.session_id.to_be_bytes());
|
||||
|
||||
if let Some(challenge) = self.challenge {
|
||||
packet.extend_from_slice(&challenge.to_be_bytes());
|
||||
}
|
||||
|
||||
if let Some(payload) = self.payload {
|
||||
packet.extend_from_slice(&payload);
|
||||
}
|
||||
|
||||
packet
|
||||
}
|
||||
}
|
||||
|
||||
struct GameSpy3 {
|
||||
socket: UdpSocket,
|
||||
}
|
||||
|
||||
const PACKET_SIZE: usize = 2048;
|
||||
|
||||
impl GameSpy3 {
|
||||
fn new(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
|
||||
let socket = UdpSocket::new(address, port)?;
|
||||
socket.apply_timeout(timeout_settings)?;
|
||||
|
||||
Ok(Self { socket })
|
||||
}
|
||||
|
||||
fn receive(&mut self, size: Option<usize>, kind: u8) -> GDResult<Bufferer> {
|
||||
let received = self.socket.receive(size.or(Some(PACKET_SIZE)))?;
|
||||
let mut buf = Bufferer::new_with_data(Endianess::Big, &received);
|
||||
|
||||
if buf.get_u8()? != kind {
|
||||
return Err(GDError::PacketBad);
|
||||
}
|
||||
|
||||
if buf.get_u32()? != THIS_SESSION_ID {
|
||||
return Err(GDError::PacketBad);
|
||||
}
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn make_initial_handshake(&mut self) -> GDResult<Option<i32>> {
|
||||
self.socket.send(
|
||||
&RequestPacket {
|
||||
header: 65277,
|
||||
kind: 9,
|
||||
session_id: THIS_SESSION_ID,
|
||||
challenge: None,
|
||||
payload: None,
|
||||
}
|
||||
.to_bytes(),
|
||||
)?;
|
||||
|
||||
let mut buf = self.receive(Some(16), 9)?;
|
||||
|
||||
let challenge_as_string = buf.get_string_utf8()?;
|
||||
let challenge = challenge_as_string
|
||||
.parse()
|
||||
.map_err(|_| GDError::TypeParse)?;
|
||||
|
||||
Ok(match challenge == 0 {
|
||||
true => None,
|
||||
false => Some(challenge),
|
||||
})
|
||||
}
|
||||
|
||||
fn send_data_request(&mut self, challenge: Option<i32>) -> GDResult<()> {
|
||||
self.socket.send(
|
||||
&RequestPacket {
|
||||
header: 65277,
|
||||
kind: 0,
|
||||
session_id: THIS_SESSION_ID,
|
||||
challenge,
|
||||
payload: Some([0xff, 0xff, 0xff, 0x01]),
|
||||
}
|
||||
.to_bytes(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_server_packets(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<Vec<Vec<u8>>> {
|
||||
let mut gs3 = GameSpy3::new(address, port, timeout_settings)?;
|
||||
|
||||
let challenge = gs3.make_initial_handshake()?;
|
||||
gs3.send_data_request(challenge)?;
|
||||
|
||||
let mut values: Vec<Vec<u8>> = Vec::new();
|
||||
|
||||
let mut expected_number_of_packets: Option<usize> = None;
|
||||
|
||||
while expected_number_of_packets.is_none() || values.len() != expected_number_of_packets.unwrap() {
|
||||
let mut buf = gs3.receive(None, 0)?;
|
||||
|
||||
if buf.get_string_utf8()? != "splitnum" {
|
||||
return Err(GDError::PacketBad);
|
||||
}
|
||||
|
||||
let id = buf.get_u8()?;
|
||||
let is_last = (id & 0x80) > 0;
|
||||
let packet_id = (id & 0x7f) as usize;
|
||||
buf.move_position_ahead(1); //unknown byte regarding packet no.
|
||||
|
||||
if is_last {
|
||||
expected_number_of_packets = Some(packet_id + 1);
|
||||
}
|
||||
|
||||
while values.len() <= packet_id {
|
||||
values.push(Vec::new());
|
||||
}
|
||||
|
||||
values[packet_id] = buf.remaining_data_vec();
|
||||
}
|
||||
|
||||
if values.iter().any(|v| v.is_empty()) {
|
||||
return Err(GDError::PacketBad);
|
||||
}
|
||||
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
fn data_to_map(packet: &Vec<u8>) -> GDResult<(HashMap<String, String>, Vec<u8>)> {
|
||||
let mut vars = HashMap::new();
|
||||
|
||||
let mut buf = Bufferer::new_with_data(Endianess::Big, &packet);
|
||||
while buf.remaining_length() > 0 {
|
||||
let key = buf.get_string_utf8()?;
|
||||
if key.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let value = buf.get_string_utf8_optional()?;
|
||||
|
||||
vars.insert(key, value);
|
||||
}
|
||||
|
||||
Ok((vars, buf.remaining_data_vec()))
|
||||
}
|
||||
|
||||
/// 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>> {
|
||||
let packets = get_server_packets(address, port, timeout_settings)?;
|
||||
|
||||
let mut vars = HashMap::new();
|
||||
|
||||
for packet in &packets {
|
||||
let (key_values, _remaining_data) = data_to_map(packet)?;
|
||||
vars.extend(key_values);
|
||||
}
|
||||
|
||||
Ok(vars)
|
||||
}
|
||||
|
||||
fn parse_players_and_teams(packets: Vec<Vec<u8>>) -> GDResult<(Vec<Player>, Vec<Team>)> {
|
||||
let mut players_data: Vec<HashMap<String, String>> = vec![HashMap::new()];
|
||||
let mut teams_data: Vec<HashMap<String, String>> = vec![HashMap::new()];
|
||||
|
||||
for packet in packets {
|
||||
let mut buf = Bufferer::new_with_data(Endianess::Little, &packet);
|
||||
|
||||
while buf.remaining_length() > 0 {
|
||||
if buf.get_u8()? < 3 {
|
||||
continue;
|
||||
}
|
||||
|
||||
buf.move_position_backward(1);
|
||||
|
||||
let field = buf.get_string_utf8()?;
|
||||
if field.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let field_split: Vec<&str> = field.split('_').collect();
|
||||
let field_name = field_split.get(0).ok_or(GDError::PacketBad)?;
|
||||
if !["player", "score", "ping", "team", "deaths", "pid", "skill"].contains(field_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let field_type = match field_split.get(1) {
|
||||
None => None,
|
||||
Some(v) => {
|
||||
match v.is_empty() {
|
||||
true => None,
|
||||
false => {
|
||||
if v != &"t" {
|
||||
Err(GDError::PacketBad)?
|
||||
}
|
||||
|
||||
Some(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut offset = buf.get_u8()? as usize;
|
||||
|
||||
let data = match field_type.is_none() {
|
||||
true => &mut players_data,
|
||||
false => &mut teams_data,
|
||||
};
|
||||
|
||||
while buf.remaining_length() > 0 {
|
||||
let item = buf.get_string_utf8()?;
|
||||
if item.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
while data.len() <= offset {
|
||||
data.push(HashMap::new())
|
||||
}
|
||||
|
||||
let entry_data = data.get_mut(offset).unwrap();
|
||||
entry_data.insert(field_name.to_string(), item);
|
||||
|
||||
offset += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut players: Vec<Player> = Vec::new();
|
||||
for player_data in players_data {
|
||||
players.push(Player {
|
||||
name: player_data
|
||||
.get("player")
|
||||
.ok_or(GDError::PacketBad)?
|
||||
.to_string(),
|
||||
score: player_data
|
||||
.get("score")
|
||||
.ok_or(GDError::PacketBad)?
|
||||
.parse()
|
||||
.map_err(|_| GDError::PacketBad)?,
|
||||
ping: player_data
|
||||
.get("ping")
|
||||
.ok_or(GDError::PacketBad)?
|
||||
.parse()
|
||||
.map_err(|_| GDError::PacketBad)?,
|
||||
team: player_data
|
||||
.get("team")
|
||||
.ok_or(GDError::PacketBad)?
|
||||
.parse()
|
||||
.map_err(|_| GDError::PacketBad)?,
|
||||
deaths: player_data
|
||||
.get("deaths")
|
||||
.ok_or(GDError::PacketBad)?
|
||||
.parse()
|
||||
.map_err(|_| GDError::PacketBad)?,
|
||||
skill: player_data
|
||||
.get("skill")
|
||||
.ok_or(GDError::PacketBad)?
|
||||
.parse()
|
||||
.map_err(|_| GDError::PacketBad)?,
|
||||
})
|
||||
}
|
||||
|
||||
let mut teams: Vec<Team> = Vec::new();
|
||||
for team_data in teams_data {
|
||||
teams.push(Team {
|
||||
name: team_data.get("team").ok_or(GDError::PacketBad)?.to_string(),
|
||||
score: team_data
|
||||
.get("score")
|
||||
.ok_or(GDError::PacketBad)?
|
||||
.parse()
|
||||
.map_err(|_| GDError::PacketBad)?,
|
||||
})
|
||||
}
|
||||
|
||||
Ok((players, teams))
|
||||
}
|
||||
|
||||
/// 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 packets = get_server_packets(address, port, timeout_settings)?;
|
||||
|
||||
let (mut server_vars, remaining_data) = data_to_map(packets.get(0).ok_or(GDError::PacketBad)?)?;
|
||||
|
||||
let mut remaining_data_packets = vec![remaining_data];
|
||||
remaining_data_packets.extend_from_slice(&packets[1 ..]);
|
||||
let (players, teams) = parse_players_and_teams(remaining_data_packets)?;
|
||||
|
||||
let players_maximum = server_vars
|
||||
.remove("maxplayers")
|
||||
.ok_or(GDError::PacketBad)?
|
||||
.parse()
|
||||
.map_err(|_| GDError::TypeParse)?;
|
||||
let players_minimum = match server_vars.remove("minplayers") {
|
||||
None => None,
|
||||
Some(v) => Some(v.parse::<u8>().map_err(|_| GDError::TypeParse)?),
|
||||
};
|
||||
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,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Response {
|
||||
name: server_vars.remove("hostname").ok_or(GDError::PacketBad)?,
|
||||
map: server_vars.remove("mapname").ok_or(GDError::PacketBad)?,
|
||||
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_minimum,
|
||||
players,
|
||||
teams,
|
||||
tournament: server_vars
|
||||
.remove("tournament")
|
||||
.unwrap_or("true".to_string())
|
||||
.to_lowercase()
|
||||
.parse()
|
||||
.map_err(|_| GDError::TypeParse)?,
|
||||
unused_entries: server_vars,
|
||||
})
|
||||
}
|
||||
42
src/protocols/gamespy/protocols/three/types.rs
Normal file
42
src/protocols/gamespy/protocols/three/types.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A player’s details.
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct Player {
|
||||
pub name: String,
|
||||
pub score: i32,
|
||||
pub ping: u16,
|
||||
pub team: u8,
|
||||
pub deaths: u32,
|
||||
pub skill: u32,
|
||||
}
|
||||
|
||||
/// A team's details
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct Team {
|
||||
pub name: String,
|
||||
pub score: i32,
|
||||
}
|
||||
|
||||
/// A query response.
|
||||
#[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 game_type: String,
|
||||
pub game_version: String,
|
||||
pub players_maximum: usize,
|
||||
pub players_online: usize,
|
||||
pub players_minimum: Option<u8>,
|
||||
pub players: Vec<Player>,
|
||||
pub teams: Vec<Team>,
|
||||
pub tournament: bool,
|
||||
pub unused_entries: HashMap<String, String>,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue