mirror of
https://github.com/tribufu/rust-gamedig
synced 2026-06-01 09:42:41 +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:
|
Protocols:
|
||||||
- GameSpy 1: Add key `admin` as a possible variable for `admin_name`.
|
- GameSpy 1: Add key `admin` as a possible variable for `admin_name`.
|
||||||
|
- GameSpy 3 support.
|
||||||
|
|
||||||
Games:
|
Games:
|
||||||
- [Serious Sam](https://www.gog.com/game/serious_sam_the_first_encounter) support.
|
- [Serious Sam](https://www.gog.com/game/serious_sam_the_first_encounter) support.
|
||||||
|
|
@ -16,6 +17,7 @@ Games:
|
||||||
### Breaking:
|
### Breaking:
|
||||||
Protocols:
|
Protocols:
|
||||||
- Valve: Request type enums have been renamed from all caps to starting-only uppercase, ex: `INFO` to `Info`
|
- 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
|
# 0.2.1 - 03/03/2023
|
||||||
### Changes:
|
### Changes:
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ A protocol is defined as proprietary if it is being used only for a single scope
|
||||||
|
|
||||||
# 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) | 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) 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:
|
## Planned to add support:
|
||||||
_
|
_
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,8 @@ fn main() -> GDResult<()> {
|
||||||
"ut" => println!("{:#?}", ut::query(ip, port)),
|
"ut" => println!("{:#?}", ut::query(ip, port)),
|
||||||
"bf1942" => println!("{:#?}", bf1942::query(ip, port)),
|
"bf1942" => println!("{:#?}", bf1942::query(ip, port)),
|
||||||
"ss" => println!("{:#?}", ss::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]),
|
_ => panic!("Undefined game: {}", args[1]),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,18 @@ impl Bufferer {
|
||||||
Ok(value)
|
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> {
|
pub fn get_string_utf8_unended(&mut self) -> GDResult<String> {
|
||||||
let sub_buf = self.remaining_data();
|
let sub_buf = self.remaining_data();
|
||||||
if sub_buf.is_empty() {
|
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.
|
mod common;
|
||||||
pub mod protocol;
|
/// The implementations.
|
||||||
/// All types used by the implementation.
|
pub mod protocols;
|
||||||
pub mod types;
|
|
||||||
|
|
||||||
pub use protocol::*;
|
pub use protocols::*;
|
||||||
pub use types::*;
|
|
||||||
|
|
|
||||||
|
|
@ -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::{
|
use crate::{
|
||||||
bufferer::{Bufferer, Endianess},
|
bufferer::{Bufferer, Endianess},
|
||||||
protocols::{
|
protocols::{
|
||||||
gamespy::{Player, Response},
|
gamespy::one::{Player, Response},
|
||||||
types::TimeoutSettings,
|
types::TimeoutSettings,
|
||||||
},
|
},
|
||||||
socket::{Socket, UdpSocket},
|
socket::{Socket, UdpSocket},
|
||||||
|
|
@ -9,6 +9,7 @@ use crate::{
|
||||||
GDResult,
|
GDResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::protocols::gamespy::common::has_password;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
fn get_server_values(
|
fn get_server_values(
|
||||||
|
|
@ -173,21 +174,6 @@ fn extract_players(server_vars: &mut HashMap<String, String>, players_maximum: u
|
||||||
Ok(players)
|
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
|
/// If there are parsing problems using the `query` function, you can directly
|
||||||
/// get the server's values using this function.
|
/// get the server's values using this function.
|
||||||
pub fn query_vars(
|
pub fn query_vars(
|
||||||
|
|
@ -209,6 +195,10 @@ pub fn query(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>
|
||||||
.ok_or(GDError::PacketBad)?
|
.ok_or(GDError::PacketBad)?
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| GDError::TypeParse)?;
|
.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)?;
|
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)?,
|
game_version: server_vars.remove("gamever").ok_or(GDError::PacketBad)?,
|
||||||
players_maximum,
|
players_maximum,
|
||||||
players_online: players.len(),
|
players_online: players.len(),
|
||||||
players_minimum: server_vars
|
players_minimum,
|
||||||
.remove("minplayers")
|
|
||||||
.unwrap_or_else(|| "0".to_string())
|
|
||||||
.parse()
|
|
||||||
.map_err(|_| GDError::TypeParse)?,
|
|
||||||
players,
|
players,
|
||||||
tournament: server_vars
|
tournament: server_vars
|
||||||
.remove("tournament")
|
.remove("tournament")
|
||||||
.unwrap_or_else(|| "true".to_string())
|
.unwrap_or("true".to_string())
|
||||||
.to_lowercase()
|
.to_lowercase()
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| GDError::TypeParse)?,
|
.map_err(|_| GDError::TypeParse)?,
|
||||||
|
|
@ -34,7 +34,7 @@ pub struct Response {
|
||||||
pub game_version: String,
|
pub game_version: String,
|
||||||
pub players_maximum: usize,
|
pub players_maximum: usize,
|
||||||
pub players_online: usize,
|
pub players_online: usize,
|
||||||
pub players_minimum: u8,
|
pub players_minimum: Option<u8>,
|
||||||
pub players: Vec<Player>,
|
pub players: Vec<Player>,
|
||||||
pub tournament: bool,
|
pub tournament: bool,
|
||||||
pub unused_entries: HashMap<String, String>,
|
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