refator: copy cli into mono

This commit is contained in:
Cain 2023-10-16 23:20:47 +01:00
parent 66ae3c296e
commit 80f6b87991
63 changed files with 244 additions and 34 deletions

View file

@ -0,0 +1,19 @@
use crate::{GDErrorKind, 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(GDErrorKind::PacketBad.context("Missing password (exists) field"))?
.to_lowercase();
if let Ok(has) = password_value.parse::<bool>() {
return Ok(has);
}
let as_numeral: u8 = password_value
.parse()
.map_err(|e| GDErrorKind::TypeParse.context(e))?;
Ok(as_numeral != 0)
}

View file

@ -0,0 +1,86 @@
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub(crate) mod common;
/// The implementations.
pub mod protocols;
pub use protocols::*;
/// Versions of the gamespy protocol
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GameSpyVersion {
One,
Two,
Three,
}
/// Versioned response type
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionedResponse<'a> {
One(&'a one::Response),
Two(&'a two::Response),
Three(&'a three::Response),
}
/// Versioned player type
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionedPlayer<'a> {
One(&'a one::Player),
Two(&'a two::Player),
Three(&'a three::Player),
}
/// Generate a module containing a query function for a gamespy game.
///
/// * `mod_name` - The name to be given to the game module (see ID naming
/// conventions in CONTRIBUTING.md).
/// * `pretty_name` - The full name of the game, will be used as the
/// documentation for the created module.
/// * `gamespy_ver`, `default_port` - Passed through to [game_query_fn].
macro_rules! game_query_mod {
($mod_name: ident, $pretty_name: expr, $gamespy_ver: ident, $default_port: literal) => {
#[doc = $pretty_name]
pub mod $mod_name {
crate::protocols::gamespy::game_query_fn!($gamespy_ver, $default_port);
}
};
}
pub(crate) use game_query_mod;
// Allow generating doc comments:
// https://users.rust-lang.org/t/macros-filling-text-in-comments/20473
/// Generate a query function for a gamespy game.
///
/// * `gamespy_ver` - The name of the [module](crate::protocols::gamespy) for
/// the gamespy version the game uses.
/// * `default_port` - The default port the game uses.
///
/// ```rust,ignore
/// use crate::protocols::gamespy::game_query_fn;
/// game_query_fn!(one, 7778);
/// ```
macro_rules! game_query_fn {
($gamespy_ver: ident, $default_port: literal) => {
crate::protocols::gamespy::game_query_fn! {@gen $gamespy_ver, $default_port, concat!(
"Make a gamespy ", stringify!($gamespy_ver), " query with default timeout settings.\n\n",
"If port is `None`, then the default port (", stringify!($default_port), ") will be used.")}
};
(@gen $gamespy_ver: ident, $default_port: literal, $doc: expr) => {
#[doc = $doc]
pub fn query(
address: &std::net::IpAddr,
port: Option<u16>,
) -> crate::GDResult<crate::protocols::gamespy::$gamespy_ver::Response> {
crate::protocols::gamespy::$gamespy_ver::query(
&std::net::SocketAddr::new(*address, port.unwrap_or($default_port)),
None,
)
}
};
}
pub(crate) use game_query_fn;

View file

@ -0,0 +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,244 @@
use byteorder::LittleEndian;
use crate::buffer::Utf8Decoder;
use crate::protocols::gamespy::common::has_password;
use crate::GDErrorKind::TypeParse;
use crate::utils::retry_on_timeout;
use crate::{
buffer::Buffer,
protocols::{
gamespy::one::{Player, Response},
types::TimeoutSettings,
},
socket::{Socket, UdpSocket},
GDErrorKind,
GDResult,
};
use std::collections::HashMap;
use std::net::SocketAddr;
/// Send status request, and parse response into HashMap.
/// This function will retry fetch on timeouts.
fn get_server_values(
address: &SocketAddr,
timeout_settings: &Option<TimeoutSettings>,
) -> GDResult<HashMap<String, String>> {
let mut socket = UdpSocket::new(address)?;
socket.apply_timeout(timeout_settings)?;
retry_on_timeout(
TimeoutSettings::get_retries_or_default(timeout_settings),
move || get_server_values_impl(&mut socket),
)
}
/// Send status request, and parse response into HashMap (without retry logic).
fn get_server_values_impl(socket: &mut UdpSocket) -> GDResult<HashMap<String, String>> {
socket.send(b"\\status\\xserverquery")?;
let mut received_query_id: Option<usize> = None;
let mut parts: Vec<usize> = Vec::new();
let mut is_finished = false;
let mut server_values = HashMap::new();
while !is_finished {
let data = socket.receive(None)?;
let mut bufferer = Buffer::<LittleEndian>::new(&data);
let mut as_string = bufferer.read_string::<Utf8Decoder>(None)?;
as_string.remove(0);
let splited: Vec<String> = as_string.split('\\').map(str::to_string).collect();
for i in 0 .. splited.len() / 2 {
let position = i * 2;
let key = splited[position].clone();
let value = splited
.get(position + 1)
.map_or_else(String::new, |v| v.clone());
server_values.insert(key, value);
}
is_finished = server_values.remove("final").is_some();
let query_data = server_values.get("queryid");
let mut part = parts.len(); // if the part number isn't provided, it's value is the parts length
let mut query_id = None;
if let Some(qid) = query_data {
let split: Vec<&str> = qid.split('.').collect();
query_id = Some(split[0].parse().map_err(|e| TypeParse.context(e))?);
match split.len() {
1 => (),
2 => part = split[1].parse().map_err(|e| TypeParse.context(e))?,
_ => Err(GDErrorKind::PacketBad)?, /* the queryid can't be splitted in more than 2
* elements */
};
}
server_values.remove("queryid");
if received_query_id.is_some() && received_query_id != query_id {
return Err(GDErrorKind::PacketBad.into()); // wrong query id!
}
received_query_id = query_id;
match parts.contains(&part) {
true => Err(GDErrorKind::PacketBad)?,
false => parts.push(part),
}
}
Ok(server_values)
}
fn extract_players(server_vars: &mut HashMap<String, String>, players_maximum: u32) -> GDResult<Vec<Player>> {
let mut players_data: Vec<HashMap<String, String>> = Vec::with_capacity(players_maximum as usize);
server_vars.retain(|key, value| {
let split: Vec<&str> = key.split('_').collect();
if split.len() != 2 {
return true;
}
let kind = split[0];
let id: usize = match split[1].parse() {
Ok(v) => v,
Err(_) => return true,
};
let early_return = match kind {
"team" | "player" | "playername" | "ping" | "face" | "skin" | "mesh" | "frags" | "ngsecret" | "deaths"
| "health" => false,
_x => true, // println!("UNKNOWN {id} {x} {value}");
};
if early_return {
return true;
}
if id >= players_data.len() {
let others = vec![HashMap::new(); id - players_data.len() + 1];
players_data.extend_from_slice(&others);
}
players_data[id].insert(kind.to_string(), value.to_string());
false
});
let mut players: Vec<Player> = Vec::with_capacity(players_data.len());
for player_data in players_data {
let new_player = Player {
name: match player_data.get("player") {
Some(v) => v.clone(),
None => {
player_data
.get("playername")
.ok_or(GDErrorKind::PacketBad)?
.clone()
}
},
team: match player_data.get("team") {
Some(t) => Some(t.trim().parse().map_err(|e| TypeParse.context(e))?),
None => None,
},
ping: player_data
.get("ping")
.ok_or(GDErrorKind::PacketBad)?
.trim()
.parse()
.map_err(|e| TypeParse.context(e))?,
face: player_data.get("face").cloned(),
skin: player_data.get("skin").cloned(),
mesh: player_data.get("mesh").cloned(),
score: player_data
.get("frags")
.ok_or(GDErrorKind::PacketBad)?
.trim()
.parse()
.map_err(|e| TypeParse.context(e))?,
deaths: match player_data.get("deaths") {
Some(v) => Some(v.trim().parse().map_err(|e| TypeParse.context(e))?),
None => None,
},
health: match player_data.get("health") {
Some(v) => Some(v.trim().parse().map_err(|e| TypeParse.context(e))?),
None => None,
},
secret: match player_data.get("ngsecret") {
Some(s) => Some(s.to_lowercase().parse().map_err(|e| TypeParse.context(e))?),
None => None,
},
};
players.push(new_player);
}
Ok(players)
}
/// 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: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<HashMap<String, String>> {
get_server_values(address, &timeout_settings)
}
/// 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: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
let mut server_vars = query_vars(address, timeout_settings)?;
let players_maximum: u32 = server_vars
.remove("maxplayers")
.ok_or(GDErrorKind::PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?;
let players_minimum = match server_vars.remove("minplayers") {
None => None,
Some(v) => Some(v.parse::<u8>().map_err(|e| TypeParse.context(e))?),
};
let players = extract_players(&mut server_vars, players_maximum)?;
Ok(Response {
name: server_vars
.remove("hostname")
.ok_or(GDErrorKind::PacketBad)?,
map: server_vars
.remove("mapname")
.ok_or(GDErrorKind::PacketBad)?,
map_title: server_vars.remove("maptitle"),
admin_contact: server_vars.remove("AdminEMail"),
admin_name: server_vars
.remove("AdminName")
.or_else(|| server_vars.remove("admin")),
has_password: has_password(&mut server_vars)?,
game_mode: server_vars
.remove("gametype")
.ok_or(GDErrorKind::PacketBad)?,
game_version: server_vars
.remove("gamever")
.ok_or(GDErrorKind::PacketBad)?,
players_maximum,
players_online: players.len() as u32,
players_minimum,
players,
tournament: server_vars
.remove("tournament")
.unwrap_or_else(|| "true".to_string())
.to_lowercase()
.parse()
.map_err(|e| TypeParse.context(e))?,
unused_entries: server_vars,
})
}

View file

@ -0,0 +1,73 @@
use std::collections::HashMap;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::protocols::gamespy::{VersionedPlayer, VersionedResponse};
use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer};
use crate::protocols::GenericResponse;
/// A players details.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
pub name: String,
pub team: Option<u8>,
/// The ping from the server's perspective.
pub ping: u16,
pub face: Option<String>,
pub skin: Option<String>,
pub mesh: Option<String>,
pub score: i32,
pub deaths: Option<u32>,
pub health: Option<u32>,
pub secret: Option<bool>,
}
impl CommonPlayer for Player {
fn as_original(&self) -> GenericPlayer { GenericPlayer::Gamespy(VersionedPlayer::One(self)) }
fn name(&self) -> &str { &self.name }
fn score(&self) -> Option<i32> { Some(self.score) }
}
/// 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 map_title: Option<String>,
pub admin_contact: Option<String>,
pub admin_name: Option<String>,
pub has_password: bool,
pub game_mode: String,
pub game_version: String,
pub players_maximum: u32,
pub players_online: u32,
pub players_minimum: Option<u8>,
pub players: Vec<Player>,
pub tournament: bool,
pub unused_entries: HashMap<String, String>,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse { GenericResponse::GameSpy(VersionedResponse::One(self)) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
Some(
self.players
.iter()
.map(|p| p as &dyn CommonPlayer)
.collect(),
)
}
}

View file

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

View file

@ -0,0 +1,412 @@
use byteorder::{BigEndian, LittleEndian};
use crate::buffer::{Buffer, Utf8Decoder};
use crate::protocols::gamespy::common::has_password;
use crate::protocols::gamespy::three::{Player, Response, Team};
use crate::protocols::types::TimeoutSettings;
use crate::socket::{Socket, UdpSocket};
use crate::utils::retry_on_timeout;
use crate::GDErrorKind::{PacketBad, TypeParse};
use crate::{GDErrorKind, GDResult};
use std::collections::HashMap;
use std::net::SocketAddr;
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
}
}
pub(crate) struct GameSpy3 {
socket: UdpSocket,
payload: [u8; 4],
single_packets: bool,
retry_count: usize,
}
const PACKET_SIZE: usize = 2048;
const DEFAULT_PAYLOAD: [u8; 4] = [0xFF, 0xFF, 0xFF, 0x01];
impl GameSpy3 {
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = UdpSocket::new(address)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
socket.apply_timeout(&timeout_settings)?;
Ok(Self {
socket,
payload: DEFAULT_PAYLOAD,
single_packets: false,
retry_count,
})
}
pub(crate) fn new_custom(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
payload: [u8; 4],
single_packets: bool,
) -> GDResult<Self> {
let socket = UdpSocket::new(address)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
socket.apply_timeout(&timeout_settings)?;
Ok(Self {
socket,
payload,
single_packets,
retry_count,
})
}
fn receive(&mut self, size: Option<usize>, kind: u8) -> GDResult<Vec<u8>> {
let received = self.socket.receive(size.or(Some(PACKET_SIZE)))?;
let mut buf = Buffer::<BigEndian>::new(&received);
if buf.read::<u8>()? != kind {
return Err(PacketBad.context("Kind of packet did not match"));
}
if buf.read::<u32>()? != THIS_SESSION_ID {
return Err(PacketBad.context("Session ID did not match"));
}
Ok(buf.remaining_bytes().to_vec())
}
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 data = self.receive(Some(16), 9)?;
let mut buf = Buffer::<LittleEndian>::new(&data);
let challenge_as_string = buf.read_string::<Utf8Decoder>(None)?;
let challenge = challenge_as_string
.parse()
.map_err(|e| TypeParse.context(e))?;
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(self.payload),
}
.to_bytes(),
)
}
/// Fetch packets from server and store in buffer.
/// This function will retry fetch on timeouts.
pub(crate) fn get_server_packets(&mut self) -> GDResult<Vec<Vec<u8>>> {
retry_on_timeout(self.retry_count, move || self.get_server_packets_impl())
}
/// Fetch packets from server and store in buffer (without retry logic).
fn get_server_packets_impl(&mut self) -> GDResult<Vec<Vec<u8>>> {
let challenge = self.make_initial_handshake()?;
self.send_data_request(challenge)?;
let mut values: Vec<Vec<u8>> = Vec::new();
let mut reached_expected_packets_size = false;
while !reached_expected_packets_size {
let received_data = self.receive(None, 0)?;
let mut buf = Buffer::<BigEndian>::new(&received_data);
if self.single_packets {
buf.move_cursor(11)?;
return Ok(vec![buf.remaining_bytes().to_vec()]);
}
if buf.read_string::<Utf8Decoder>(None)? != "splitnum" {
return Err(PacketBad.context("Expected string \"splitnum\""));
}
let id = buf.read::<u8>()?;
let is_last = (id & 0x80) > 0;
let packet_id = (id & 0x7f) as usize;
buf.move_cursor(1)?; //unknown byte regarding packet no.
if is_last && packet_id + 1 != values.len() {
reached_expected_packets_size = true;
}
while values.len() <= packet_id {
values.push(Vec::new());
}
values[packet_id] = buf.remaining_bytes().to_vec();
}
if values.iter().any(Vec::is_empty) {
return Err(PacketBad.context("One (or more) packets is empty"));
}
Ok(values)
}
}
pub(crate) fn data_to_map(packet: &[u8]) -> GDResult<(HashMap<String, String>, Vec<u8>)> {
let mut vars = HashMap::new();
let mut buf = Buffer::<BigEndian>::new(packet);
while buf.remaining_length() != 0 {
let key = buf.read_string::<Utf8Decoder>(None)?;
if key.is_empty() {
break;
}
let value = buf.read_string::<Utf8Decoder>(None)?;
vars.insert(key, value);
}
Ok((vars, buf.remaining_bytes().to_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: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<HashMap<String, String>> {
let mut client = GameSpy3::new(address, timeout_settings)?;
let packets = client.get_server_packets()?;
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 = Buffer::<LittleEndian>::new(&packet);
while buf.remaining_length() != 0 {
if buf.read::<u8>()? < 3 {
continue;
}
buf.move_cursor(1)?;
let field = buf.read_string::<Utf8Decoder>(None)?;
if field.is_empty() {
continue;
}
let field_split: Vec<&str> = field.split('_').collect();
let field_name = field_split.first().ok_or(GDErrorKind::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(GDErrorKind::PacketBad)?;
}
Some(v)
}
}
}
};
let mut offset = buf.read::<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.read_string::<Utf8Decoder>(None)?;
if item.is_empty() {
break;
}
while data.len() <= offset {
data.push(HashMap::new());
}
let entry_data = data.get_mut(offset).ok_or(PacketBad)?;
entry_data.insert(field_name.to_string(), item);
offset += 1;
}
}
}
let mut players: Vec<Player> = Vec::new();
for player_data in players_data {
if player_data.is_empty() {
continue;
}
players.push(Player {
name: player_data.get("player").ok_or(PacketBad)?.to_string(),
score: player_data
.get("score")
.ok_or(GDErrorKind::PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?,
ping: player_data
.get("ping")
.ok_or(GDErrorKind::PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?,
team: player_data
.get("team")
.ok_or(GDErrorKind::PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?,
deaths: player_data
.get("deaths")
.ok_or(GDErrorKind::PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?,
skill: player_data
.get("skill")
.ok_or(GDErrorKind::PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?,
});
}
let mut teams: Vec<Team> = Vec::new();
for team_data in teams_data {
if team_data.is_empty() {
continue;
}
teams.push(Team {
name: team_data
.get("team")
.ok_or(GDErrorKind::PacketBad)?
.to_string(),
score: team_data
.get("score")
.ok_or(GDErrorKind::PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?,
});
}
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: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
let mut client = GameSpy3::new(address, timeout_settings)?;
let packets = client.get_server_packets()?;
let (mut server_vars, remaining_data) = data_to_map(packets.get(0).ok_or(GDErrorKind::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(GDErrorKind::PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?;
let players_minimum = match server_vars.remove("minplayers") {
None => None,
Some(v) => Some(v.parse::<u8>().map_err(|e| TypeParse.context(e))?),
};
let players_online: u32 = match server_vars.remove("numplayers") {
None => players.len(),
Some(v) => {
let reported_players = v.parse().map_err(|e| TypeParse.context(e))?;
match reported_players < players.len() {
true => players.len(),
false => reported_players,
}
}
} as u32;
Ok(Response {
name: server_vars
.remove("hostname")
.ok_or(GDErrorKind::PacketBad)?,
map: server_vars
.remove("mapname")
.ok_or(GDErrorKind::PacketBad)?,
has_password: has_password(&mut server_vars)?,
game_mode: server_vars
.remove("gametype")
.ok_or(GDErrorKind::PacketBad)?,
game_version: server_vars
.remove("gamever")
.ok_or(GDErrorKind::PacketBad)?,
players_maximum,
players_online,
players_minimum,
players,
teams,
tournament: server_vars
.remove("tournament")
.unwrap_or_else(|| "true".to_string())
.to_lowercase()
.parse()
.map_err(|e| TypeParse.context(e))?,
unused_entries: server_vars,
})
}

View file

@ -0,0 +1,75 @@
use crate::protocols::gamespy::{VersionedPlayer, VersionedResponse};
use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer};
use crate::protocols::GenericResponse;
use std::collections::HashMap;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// A players 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,
}
impl CommonPlayer for Player {
fn as_original(&self) -> crate::protocols::types::GenericPlayer {
GenericPlayer::Gamespy(VersionedPlayer::Three(self))
}
fn name(&self) -> &str { &self.name }
fn score(&self) -> Option<i32> { Some(self.score) }
}
/// 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_mode: String,
pub game_version: String,
pub players_maximum: u32,
pub players_online: u32,
pub players_minimum: Option<u8>,
pub players: Vec<Player>,
pub teams: Vec<Team>,
pub tournament: bool,
pub unused_entries: HashMap<String, String>,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse { GenericResponse::GameSpy(VersionedResponse::Three(self)) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
Some(
self.players
.iter()
.map(|p| p as &dyn CommonPlayer)
.collect(),
)
}
}

View file

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

View file

@ -0,0 +1,211 @@
use crate::buffer::{Buffer, Utf8Decoder};
use crate::protocols::gamespy::two::{Player, Response, Team};
use crate::protocols::types::TimeoutSettings;
use crate::socket::{Socket, UdpSocket};
use crate::utils::retry_on_timeout;
use crate::GDErrorKind::{PacketBad, TypeParse};
use crate::{GDErrorKind, GDResult};
use byteorder::BigEndian;
use std::collections::HashMap;
use std::net::SocketAddr;
struct GameSpy2 {
socket: UdpSocket,
retry_count: usize,
}
macro_rules! table_extract {
($table:expr, $name:literal, $index:expr) => {
$table
.get($name)
.ok_or(GDErrorKind::PacketBad)?
.get($index)
.ok_or(GDErrorKind::PacketBad)?
};
}
macro_rules! table_extract_parse {
($table:expr, $name:literal, $index:expr) => {
table_extract!($table, $name, $index)
.parse()
.map_err(|e| PacketBad.context(e))?
};
}
fn data_as_table(data: &mut Buffer<BigEndian>) -> GDResult<(HashMap<String, Vec<String>>, usize)> {
if data.read::<u8>()? != 0 {
Err(GDErrorKind::PacketBad)?;
}
let rows = data.read::<u8>()? as usize;
if rows == 0 {
return Ok((HashMap::new(), 0));
}
let mut column_heads = Vec::new();
let mut current_column = data.read_string::<Utf8Decoder>(None)?;
while !current_column.is_empty() {
column_heads.push(current_column);
current_column = data.read_string::<Utf8Decoder>(None)?;
}
let columns = column_heads.len();
let mut table = HashMap::with_capacity(columns);
for head in &column_heads {
// TODO: This doesn't look good nor it is performant, fix later
// By using &column_heads in the for loop instead of cloning column_heads, you
// avoid creating an unnecessary copy. However, column_heads is a
// Vec<String> and head is a &String (a reference to a string). Hence, to use
// head as a key to the HashMap, we still need to call clone(). This is because
// HashMap takes ownership of its keys and we cannot give it a reference to a
// local variable (head) that will be dropped at the end of the function.
table.insert(head.clone(), Vec::new());
}
for _ in 0 .. rows {
for column in &column_heads {
let value = data.read_string::<Utf8Decoder>(None)?;
table
.get_mut(column)
.ok_or(GDErrorKind::PacketBad)?
.push(value);
}
}
Ok((table, rows))
}
impl GameSpy2 {
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = UdpSocket::new(address)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
socket.apply_timeout(&timeout_settings)?;
Ok(Self {
socket,
retry_count,
})
}
/// Send fetch request to server and store result in buffer.
/// This function will retry fetch on timeouts.
fn request_data(&mut self) -> GDResult<(Vec<u8>, usize)> {
retry_on_timeout(self.retry_count, move || self.request_data_impl())
}
/// Send fetch request to server and store result in buffer (without retry
/// logic).
fn request_data_impl(&mut self) -> GDResult<(Vec<u8>, usize)> {
self.socket
.send(&[0xFE, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF])?;
let received = self.socket.receive(None)?;
let mut buf = Buffer::<BigEndian>::new(&received);
if buf.read::<u8>()? != 0 || buf.read::<u32>()? != 1 {
return Err(PacketBad.into());
}
let buf_index = buf.current_position();
Ok((received, buf_index))
}
}
fn get_server_vars(bufferer: &mut Buffer<BigEndian>) -> 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.read_string::<Utf8Decoder>(None)?;
let value = bufferer.read_string::<Utf8Decoder>(None)?;
if key.is_empty() {
if value.is_empty() {
bufferer.move_cursor(-1)?;
done_processing_vars = true;
}
continue;
}
values.insert(key, value);
}
Ok(values)
}
fn get_teams(bufferer: &mut Buffer<BigEndian>) -> 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 Buffer<BigEndian>) -> 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 (data, buf_index) = client.request_data()?;
let mut buffer = Buffer::<BigEndian>::new(&data);
buffer.move_cursor(buf_index as isize)?;
let mut server_vars = get_server_vars(&mut buffer)?;
let players = get_players(&mut buffer)?;
let players_online = match server_vars.remove("numplayers") {
None => players.len(),
Some(v) => {
let reported_players = v.parse().map_err(|e| TypeParse.context(e))?;
match reported_players < players.len() {
true => players.len(),
false => reported_players,
}
}
} as u32;
let players_minimum = match server_vars.remove("minplayers") {
None => None,
Some(v) => Some(v.parse::<u32>().map_err(|e| TypeParse.context(e))?),
};
Ok(Response {
name: server_vars.remove("hostname").ok_or(PacketBad)?,
map: server_vars.remove("mapname").ok_or(PacketBad)?,
has_password: server_vars.remove("password").ok_or(PacketBad)? == "1",
teams: get_teams(&mut buffer)?,
players_maximum: server_vars
.remove("maxplayers")
.ok_or(PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?,
players_online,
players_minimum,
players,
unused_entries: server_vars,
})
}

View file

@ -0,0 +1,64 @@
use std::collections::HashMap;
use crate::protocols::gamespy::{VersionedPlayer, VersionedResponse};
use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer};
use crate::protocols::GenericResponse;
#[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,
}
impl CommonPlayer for Player {
fn as_original(&self) -> GenericPlayer { GenericPlayer::Gamespy(VersionedPlayer::Two(self)) }
fn name(&self) -> &str { &self.name }
fn score(&self) -> Option<i32> { Some(self.score.into()) }
}
#[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: u32,
pub players_online: u32,
pub players_minimum: Option<u32>,
pub players: Vec<Player>,
pub unused_entries: HashMap<String, String>,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse { GenericResponse::GameSpy(VersionedResponse::Two(self)) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
Some(
self.players
.iter()
.map(|p| p as &dyn CommonPlayer)
.collect(),
)
}
}

View file

@ -0,0 +1,8 @@
/// The implementation.
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use protocol::*;
pub use types::*;

View file

@ -0,0 +1,114 @@
// This file has code that has been documented by the NodeJS GameDig library
// (MIT) from https://github.com/gamedig/node-gamedig/blob/master/protocols/minecraftbedrock.js
use crate::{
buffer::{Buffer, Utf8Decoder},
protocols::{
minecraft::{BedrockResponse, GameMode, Server},
types::TimeoutSettings,
},
socket::{Socket, UdpSocket},
utils::{error_by_expected_size, retry_on_timeout},
GDErrorKind::{PacketBad, TypeParse},
GDResult,
};
use std::net::SocketAddr;
use byteorder::LittleEndian;
pub struct Bedrock {
socket: UdpSocket,
retry_count: usize,
}
impl Bedrock {
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = UdpSocket::new(address)?;
socket.apply_timeout(&timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
Ok(Self {
socket,
retry_count,
})
}
fn send_status_request(&mut self) -> GDResult<()> {
self.socket.send(&[
0x01, // Message ID: ID_UNCONNECTED_PING
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, // Nonce / timestamp
0x00, 0xff, 0xff, 0x00, 0xfe, 0xfe, 0xfe, 0xfe, 0xfd, 0xfd, 0xfd, 0xfd, 0x12, 0x34, // Magic
0x56, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Client GUID
])?;
Ok(())
}
/// Send a status request, and parse the response.
/// This function will retry fetch on timeouts.
fn get_info(&mut self) -> GDResult<BedrockResponse> {
retry_on_timeout(self.retry_count, move || self.get_info_impl())
}
/// Send a status request, and parse the response (without retry logic).
fn get_info_impl(&mut self) -> GDResult<BedrockResponse> {
self.send_status_request()?;
let received = self.socket.receive(None)?;
let mut buffer = Buffer::<LittleEndian>::new(&received);
if buffer.read::<u8>()? != 0x1c {
return Err(PacketBad.context("Expected 0x1c"));
}
// Checking for our nonce directly from a u64 (as the nonce is 8 bytes).
if buffer.read::<u64>()? != 9_833_440_827_789_222_417 {
return Err(PacketBad.context("Invalid nonce"));
}
// These 8 bytes are identical to the serverId string we receive in decimal
// below
buffer.move_cursor(8)?;
// Verifying the magic value (as we need 16 bytes, cast to two u64 values)
if buffer.read::<u64>()? != 18_374_403_896_610_127_616 {
return Err(PacketBad.context("Invalid magic"));
}
if buffer.read::<u64>()? != 8_671_175_388_723_805_693 {
return Err(PacketBad.context("Invalid magic"));
}
let remaining_length = buffer.switch_endian_chunk(2)?.read::<u16>()? as usize;
error_by_expected_size(remaining_length, buffer.remaining_length())?;
let binding = buffer.read_string::<Utf8Decoder>(None)?;
let status: Vec<&str> = binding.split(';').collect();
// We must have at least 6 values
if status.len() < 6 {
return Err(PacketBad.context("Not enough values"));
}
Ok(BedrockResponse {
edition: status[0].to_string(),
name: status[1].to_string(),
version_name: status[3].to_string(),
protocol_version: status[2].to_string(),
players_maximum: status[5].parse().map_err(|e| TypeParse.context(e))?,
players_online: status[4].parse().map_err(|e| TypeParse.context(e))?,
id: status.get(6).map(std::string::ToString::to_string),
map: status.get(7).map(std::string::ToString::to_string),
game_mode: match status.get(8) {
None => None,
Some(v) => Some(GameMode::from_bedrock(v)?),
},
server_type: Server::Bedrock,
})
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<BedrockResponse> {
Self::new(address, timeout_settings)?.get_info()
}
}

View file

@ -0,0 +1,177 @@
use crate::{
buffer::Buffer,
protocols::{
minecraft::{as_varint, get_string, get_varint, JavaResponse, Player, Server},
types::TimeoutSettings,
},
socket::{Socket, TcpSocket},
utils::retry_on_timeout,
GDErrorKind::{JsonParse, PacketBad},
GDResult,
};
use std::net::SocketAddr;
use crate::protocols::minecraft::{as_string, RequestSettings};
use byteorder::LittleEndian;
use serde_json::Value;
pub struct Java {
socket: TcpSocket,
request_settings: RequestSettings,
retry_count: usize,
}
impl Java {
fn new(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
request_settings: Option<RequestSettings>,
) -> GDResult<Self> {
let socket = TcpSocket::new(address)?;
socket.apply_timeout(&timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
Ok(Self {
socket,
request_settings: request_settings.unwrap_or_default(),
retry_count,
})
}
fn send(&mut self, data: Vec<u8>) -> GDResult<()> {
self.socket
.send(&[as_varint(data.len() as i32), data].concat())
}
fn receive(&mut self) -> GDResult<Vec<u8>> {
let data = &self.socket.receive(None)?;
let mut buffer = Buffer::<LittleEndian>::new(data);
let _packet_length = get_varint(&mut buffer)? as usize;
// this declared 'packet length' from within the packet might be wrong (?), not
// checking with it...
Ok(buffer.remaining_bytes().to_vec())
}
fn send_handshake(&mut self) -> GDResult<()> {
let handshake_payload = [
&[
// Packet ID (0)
0x00,
], // Protocol Version (-1 to determine version)
as_varint(self.request_settings.protocol_version).as_slice(),
// Server address (can be anything)
as_string(&self.request_settings.hostname)?.as_slice(),
// Server port (can be anything)
&self.socket.port().to_le_bytes(),
&[
// Next state (1 for status)
0x01,
],
]
.concat();
self.send(handshake_payload)?;
Ok(())
}
fn send_status_request(&mut self) -> GDResult<()> {
self.send(
[0x00] // Packet ID (0)
.to_vec(),
)?;
Ok(())
}
fn send_ping_request(&mut self) -> GDResult<()> {
self.send(
[0x01] // Packet ID (1)
.to_vec(),
)?;
Ok(())
}
/// Send minecraft ping request and parse the response.
/// This function will retry fetch on timeouts.
fn get_info(&mut self) -> GDResult<JavaResponse> {
retry_on_timeout(self.retry_count, move || self.get_info_impl())
}
/// Send minecraft ping request and parse the response (without retry
/// logic).
fn get_info_impl(&mut self) -> GDResult<JavaResponse> {
self.send_handshake()?;
self.send_status_request()?;
self.send_ping_request()?;
let socket_data = self.receive()?;
let mut buffer = Buffer::<LittleEndian>::new(&socket_data);
if get_varint(&mut buffer)? != 0 {
// first var int is the packet id
return Err(PacketBad.context("Expected 0"));
}
let json_response = get_string(&mut buffer)?;
let value_response: Value = serde_json::from_str(&json_response).map_err(|e| JsonParse.context(e))?;
let game_version = value_response["version"]["name"]
.as_str()
.ok_or(PacketBad)?
.to_string();
let protocol_version = value_response["version"]["protocol"]
.as_i64()
.ok_or(PacketBad)? as i32;
let max_players = value_response["players"]["max"].as_u64().ok_or(PacketBad)? as u32;
let online_players = value_response["players"]["online"]
.as_u64()
.ok_or(PacketBad)? as u32;
let players: Option<Vec<Player>> = match value_response["players"]["sample"].is_null() {
true => None,
false => {
Some({
let players_values = value_response["players"]["sample"]
.as_array()
.ok_or(PacketBad)?;
let mut players = Vec::with_capacity(players_values.len());
for player in players_values {
players.push(Player {
name: player["name"].as_str().ok_or(PacketBad)?.to_string(),
id: player["id"].as_str().ok_or(PacketBad)?.to_string(),
});
}
players
})
}
};
Ok(JavaResponse {
game_version,
protocol_version,
players_maximum: max_players,
players_online: online_players,
players,
description: value_response["description"].to_string(),
favicon: value_response["favicon"].as_str().map(str::to_string),
previews_chat: value_response["previewsChat"].as_bool(),
enforces_secure_chat: value_response["enforcesSecureChat"].as_bool(),
server_type: Server::Java,
})
}
pub fn query(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
request_settings: Option<RequestSettings>,
) -> GDResult<JavaResponse> {
Self::new(address, timeout_settings, request_settings)?.get_info()
}
}

View file

@ -0,0 +1,82 @@
use crate::{
buffer::{Buffer, Utf16Decoder},
protocols::{
minecraft::{JavaResponse, LegacyGroup, Server},
types::TimeoutSettings,
},
socket::{Socket, TcpSocket},
utils::{error_by_expected_size, retry_on_timeout},
GDErrorKind::{PacketBad, ProtocolFormat},
GDResult,
};
use std::net::SocketAddr;
use byteorder::BigEndian;
pub struct LegacyV1_3 {
socket: TcpSocket,
retry_count: usize,
}
impl LegacyV1_3 {
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = TcpSocket::new(address)?;
socket.apply_timeout(&timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
Ok(Self {
socket,
retry_count,
})
}
fn send_initial_request(&mut self) -> GDResult<()> { self.socket.send(&[0xFE]) }
/// Send request for info and parse response.
/// This function will retry fetch on timeouts.
fn get_info(&mut self) -> GDResult<JavaResponse> {
retry_on_timeout(self.retry_count, move || self.get_info_impl())
}
/// Send request for info and parse response (without retry logic).
fn get_info_impl(&mut self) -> GDResult<JavaResponse> {
self.send_initial_request()?;
let data = self.socket.receive(None)?;
let mut buffer = Buffer::<BigEndian>::new(&data);
if buffer.read::<u8>()? != 0xFF {
return Err(ProtocolFormat.context("Expected 0xFF"));
}
let length = buffer.read::<u16>()? * 2;
error_by_expected_size((length + 3) as usize, data.len())?;
let packet_string = buffer.read_string::<Utf16Decoder<BigEndian>>(None)?;
let split: Vec<&str> = packet_string.split('§').collect();
error_by_expected_size(3, split.len())?;
let description = split[0].to_string();
let online_players = split[1].parse().map_err(|e| PacketBad.context(e))?;
let max_players = split[2].parse().map_err(|e| PacketBad.context(e))?;
Ok(JavaResponse {
game_version: "Beta 1.8+".to_string(),
protocol_version: -1,
players_maximum: max_players,
players_online: online_players,
players: None,
description,
favicon: None,
previews_chat: None,
enforces_secure_chat: None,
server_type: Server::Legacy(LegacyGroup::V1_3),
})
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<JavaResponse> {
Self::new(address, timeout_settings)?.get_info()
}
}

View file

@ -0,0 +1,85 @@
use byteorder::BigEndian;
use crate::{
buffer::{Buffer, Utf16Decoder},
protocols::{
minecraft::{protocol::legacy_v1_6::LegacyV1_6, JavaResponse, LegacyGroup, Server},
types::TimeoutSettings,
},
socket::{Socket, TcpSocket},
utils::{error_by_expected_size, retry_on_timeout},
GDErrorKind::{PacketBad, ProtocolFormat},
GDResult,
};
use std::net::SocketAddr;
pub struct LegacyV1_5 {
socket: TcpSocket,
retry_count: usize,
}
impl LegacyV1_5 {
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = TcpSocket::new(address)?;
socket.apply_timeout(&timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
Ok(Self {
socket,
retry_count,
})
}
fn send_initial_request(&mut self) -> GDResult<()> { self.socket.send(&[0xFE, 0x01]) }
/// Send info request and parse response.
/// This function will retry fetch on timeouts.
fn get_info(&mut self) -> GDResult<JavaResponse> {
retry_on_timeout(self.retry_count, move || self.get_info_impl())
}
/// Send info request and parse response (without retry logic).
fn get_info_impl(&mut self) -> GDResult<JavaResponse> {
self.send_initial_request()?;
let data = self.socket.receive(None)?;
let mut buffer = Buffer::<BigEndian>::new(&data);
if buffer.read::<u8>()? != 0xFF {
return Err(ProtocolFormat.context("Expected 0xFF"));
}
let length = buffer.read::<u16>()? * 2;
error_by_expected_size((length + 3) as usize, data.len())?;
if LegacyV1_6::is_protocol(&mut buffer)? {
return LegacyV1_6::get_response(&mut buffer);
}
let packet_string = buffer.read_string::<Utf16Decoder<BigEndian>>(None)?;
let split: Vec<&str> = packet_string.split('§').collect();
error_by_expected_size(3, split.len())?;
let description = split[0].to_string();
let online_players = split[1].parse().map_err(|e| PacketBad.context(e))?;
let max_players = split[2].parse().map_err(|e| PacketBad.context(e))?;
Ok(JavaResponse {
game_version: "1.4+".to_string(),
protocol_version: -1,
players_maximum: max_players,
players_online: online_players,
players: None,
description,
favicon: None,
previews_chat: None,
enforces_secure_chat: None,
server_type: Server::Legacy(LegacyGroup::V1_5),
})
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<JavaResponse> {
Self::new(address, timeout_settings)?.get_info()
}
}

View file

@ -0,0 +1,119 @@
use byteorder::BigEndian;
use crate::{
buffer::{Buffer, Utf16Decoder},
protocols::{
minecraft::{JavaResponse, LegacyGroup, Server},
types::TimeoutSettings,
},
socket::{Socket, TcpSocket},
utils::{error_by_expected_size, retry_on_timeout},
GDErrorKind::{PacketBad, ProtocolFormat},
GDResult,
};
use std::net::SocketAddr;
pub struct LegacyV1_6 {
socket: TcpSocket,
retry_count: usize,
}
impl LegacyV1_6 {
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = TcpSocket::new(address)?;
socket.apply_timeout(&timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
Ok(Self {
socket,
retry_count,
})
}
fn send_initial_request(&mut self) -> GDResult<()> {
self.socket.send(&[
0xfe, // Packet ID (FE)
0x01, // Ping payload (01)
0xfa, // Packet identifier for plugin message
0x00, 0x07, // Length of 'GameDig' string (7) as unsigned short
0x00, 0x47, 0x00, 0x61, 0x00, 0x6D, 0x00, 0x65, 0x00, 0x44, 0x00, 0x69, 0x00,
0x67, // 'GameDig' string as UTF-16BE
])?;
Ok(())
}
pub(crate) fn is_protocol(buffer: &mut Buffer<BigEndian>) -> GDResult<bool> {
let state = buffer
.remaining_bytes()
.starts_with(&[0x00, 0xA7, 0x00, 0x31, 0x00, 0x00]);
if state {
buffer.move_cursor(6)?;
}
Ok(state)
}
pub(crate) fn get_response(buffer: &mut Buffer<BigEndian>) -> GDResult<JavaResponse> {
// This is a specific order!
let protocol_version = buffer
.read_string::<Utf16Decoder<BigEndian>>(None)?
.parse()
.map_err(|e| PacketBad.context(e))?;
let game_version = buffer.read_string::<Utf16Decoder<BigEndian>>(None)?;
let description = buffer.read_string::<Utf16Decoder<BigEndian>>(None)?;
let online_players = buffer
.read_string::<Utf16Decoder<BigEndian>>(None)?
.parse()
.map_err(|e| PacketBad.context(e))?;
let max_players = buffer
.read_string::<Utf16Decoder<BigEndian>>(None)?
.parse()
.map_err(|e| PacketBad.context(e))?;
Ok(JavaResponse {
game_version,
protocol_version,
players_maximum: max_players,
players_online: online_players,
players: None,
description,
favicon: None,
previews_chat: None,
enforces_secure_chat: None,
server_type: Server::Legacy(LegacyGroup::V1_6),
})
}
/// Send info request and parse response.
/// This function will retry fetch on timeouts.
fn get_info(&mut self) -> GDResult<JavaResponse> {
retry_on_timeout(self.retry_count, move || self.get_info_impl())
}
/// Send info request and parse response (without retry logic).
fn get_info_impl(&mut self) -> GDResult<JavaResponse> {
self.send_initial_request()?;
let data = self.socket.receive(None)?;
let mut buffer = Buffer::<BigEndian>::new(&data);
if buffer.read::<u8>()? != 0xFF {
return Err(ProtocolFormat.context("Expected 0xFF"));
}
let length = buffer.read::<u16>()? * 2;
error_by_expected_size((length + 3) as usize, data.len())?;
if !Self::is_protocol(&mut buffer)? {
return Err(ProtocolFormat.context("Not legacy 1.6 protocol"));
}
Self::get_response(&mut buffer)
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<JavaResponse> {
Self::new(address, timeout_settings)?.get_info()
}
}

View file

@ -0,0 +1,91 @@
use crate::protocols::minecraft::types::RequestSettings;
use crate::{
protocols::minecraft::{
protocol::{
bedrock::Bedrock,
java::Java,
legacy_v1_3::LegacyV1_3,
legacy_v1_5::LegacyV1_5,
legacy_v1_6::LegacyV1_6,
},
BedrockResponse,
JavaResponse,
LegacyGroup,
},
protocols::types::TimeoutSettings,
GDErrorKind::AutoQuery,
GDResult,
};
use std::net::SocketAddr;
mod bedrock;
mod java;
mod legacy_v1_3;
mod legacy_v1_5;
mod legacy_v1_6;
/// Queries a Minecraft server with all the protocol variants one by one (Java
/// -> Bedrock -> Legacy (1.6 -> 1.4 -> Beta 1.8)).
pub fn query(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
request_settings: Option<RequestSettings>,
) -> GDResult<JavaResponse> {
if let Ok(response) = query_java(address, timeout_settings.clone(), request_settings) {
return Ok(response);
}
if let Ok(response) = query_bedrock(address, timeout_settings.clone()) {
return Ok(JavaResponse::from_bedrock_response(response));
}
if let Ok(response) = query_legacy(address, timeout_settings) {
return Ok(response);
}
Err(AutoQuery.into())
}
/// Query a Java Server.
pub fn query_java(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
request_settings: Option<RequestSettings>,
) -> GDResult<JavaResponse> {
Java::query(address, timeout_settings, request_settings)
}
/// Query a (Java) Legacy Server (1.6 -> 1.4 -> Beta 1.8).
pub fn query_legacy(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<JavaResponse> {
if let Ok(response) = query_legacy_specific(LegacyGroup::V1_6, address, timeout_settings.clone()) {
return Ok(response);
}
if let Ok(response) = query_legacy_specific(LegacyGroup::V1_5, address, timeout_settings.clone()) {
return Ok(response);
}
if let Ok(response) = query_legacy_specific(LegacyGroup::V1_3, address, timeout_settings) {
return Ok(response);
}
Err(AutoQuery.into())
}
/// Query a specific (Java) Legacy Server.
pub fn query_legacy_specific(
group: LegacyGroup,
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<JavaResponse> {
match group {
LegacyGroup::V1_6 => LegacyV1_6::query(address, timeout_settings),
LegacyGroup::V1_5 => LegacyV1_5::query(address, timeout_settings),
LegacyGroup::V1_3 => LegacyV1_3::query(address, timeout_settings),
}
}
/// Query a Bedrock Server.
pub fn query_bedrock(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<BedrockResponse> {
Bedrock::query(address, timeout_settings)
}

View file

@ -0,0 +1,295 @@
// Although its a lightly modified version, this file contains code
// by Jaiden Bernard (2021-2022 - MIT) from
// https://github.com/thisjaiden/golden_apple/blob/master/src/lib.rs
use crate::{
buffer::Buffer,
protocols::{
types::{CommonPlayer, CommonResponse, ExtraRequestSettings, GenericPlayer},
GenericResponse,
},
GDErrorKind::{InvalidInput, PacketBad, UnknownEnumCast},
GDResult,
};
use byteorder::ByteOrder;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// The type of Minecraft Server you want to query.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Server {
/// Java Edition.
Java,
/// Legacy Java.
Legacy(LegacyGroup),
/// Bedrock Edition.
Bedrock,
}
/// Legacy Java (Versions) Groups.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum LegacyGroup {
/// 1.6
V1_6,
/// 1.4 - 1.5
V1_5,
/// Beta 1.8 - 1.3
V1_3,
}
/// Information about a player.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
pub name: String,
pub id: String,
}
impl CommonPlayer for Player {
fn as_original(&self) -> GenericPlayer { GenericPlayer::Minecraft(self) }
fn name(&self) -> &str { &self.name }
}
/// Versioned response type
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionedResponse<'a> {
Bedrock(&'a BedrockResponse),
Java(&'a JavaResponse),
}
/// A Java query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct JavaResponse {
/// Version name, example: "1.19.2".
pub game_version: String,
/// Protocol version, example: 760 (for 1.19.1 or 1.19.2).
/// Note that for versions below 1.6 this field is always -1.
pub protocol_version: i32,
/// Number of server capacity.
pub players_maximum: u32,
/// Number of online players.
pub players_online: u32,
/// Some online players (can be missing).
pub players: Option<Vec<Player>>,
/// Server's description or MOTD.
pub description: String,
/// The favicon (can be missing).
pub favicon: Option<String>,
/// Tells if the chat preview is enabled (can be missing).
pub previews_chat: Option<bool>,
/// Tells if secure chat is enforced (can be missing).
pub enforces_secure_chat: Option<bool>,
/// Tell's the server type.
pub server_type: Server,
}
/// Java-only additional request settings.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct RequestSettings {
/// Some Minecraft servers do not respond as expected if this
/// isn't a specific value, `mc.hypixel.net` is an example.
pub hostname: String,
/// Specifies the client [protocol version number](https://wiki.vg/Protocol_version_numbers),
/// `-1` means anything.
pub protocol_version: i32,
}
impl Default for RequestSettings {
/// `hostname`: "gamedig"
/// `protocol_version`: -1
fn default() -> Self {
Self {
hostname: "gamedig".to_string(),
protocol_version: -1,
}
}
}
impl RequestSettings {
/// Make a new *RequestSettings* with just the hostname, the protocol
/// version defaults to -1
pub fn new_just_hostname(hostname: String) -> Self {
Self {
hostname,
protocol_version: -1,
}
}
}
impl From<ExtraRequestSettings> for RequestSettings {
fn from(value: ExtraRequestSettings) -> Self {
let default = Self::default();
Self {
hostname: value.hostname.unwrap_or(default.hostname),
protocol_version: value.protocol_version.unwrap_or(default.protocol_version),
}
}
}
impl CommonResponse for JavaResponse {
fn as_original(&self) -> GenericResponse { GenericResponse::Minecraft(VersionedResponse::Java(self)) }
fn description(&self) -> Option<&str> { Some(&self.description) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
self.players
.as_ref()
.map(|players| players.iter().map(|p| p as &dyn CommonPlayer).collect())
}
}
/// A Bedrock Edition query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct BedrockResponse {
/// Server's edition.
pub edition: String,
/// Server's name.
pub name: String,
/// Version name, example: "1.19.40".
pub version_name: String,
/// Protocol version, example: 760 (for 1.19.2).
pub protocol_version: String,
/// Maximum number of players the server reports it can hold.
pub players_maximum: u32,
/// Number of players on the server.
pub players_online: u32,
/// Server id.
pub id: Option<String>,
/// Currently running map's name.
pub map: Option<String>,
/// Current game mode.
pub game_mode: Option<GameMode>,
/// Tells the server type.
pub server_type: Server,
}
impl CommonResponse for BedrockResponse {
fn as_original(&self) -> GenericResponse { GenericResponse::Minecraft(VersionedResponse::Bedrock(self)) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn map(&self) -> Option<&str> { self.map.as_deref() }
fn game_version(&self) -> Option<&str> { Some(&self.version_name) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
}
impl JavaResponse {
pub fn from_bedrock_response(response: BedrockResponse) -> Self {
Self {
game_version: response.version_name,
protocol_version: 0,
players_maximum: response.players_maximum,
players_online: response.players_online,
players: None,
description: response.name,
favicon: None,
previews_chat: None,
enforces_secure_chat: None,
server_type: Server::Bedrock,
}
}
}
/// A server's game mode (used only by Bedrock servers.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum GameMode {
Survival,
Creative,
Hardcore,
Spectator,
Adventure,
}
impl GameMode {
pub fn from_bedrock(value: &&str) -> GDResult<Self> {
match *value {
"Survival" => Ok(Self::Survival),
"Creative" => Ok(Self::Creative),
"Hardcore" => Ok(Self::Hardcore),
"Spectator" => Ok(Self::Spectator),
"Adventure" => Ok(Self::Adventure),
_ => Err(UnknownEnumCast.context(format!("Unknown gamemode {value:?}"))),
}
}
}
pub(crate) fn get_varint<B: ByteOrder>(buffer: &mut Buffer<B>) -> GDResult<i32> {
let mut result = 0;
let msb: u8 = 0b1000_0000;
let mask: u8 = !msb;
for i in 0 .. 5 {
let current_byte = buffer.read::<u8>()?;
result |= ((current_byte & mask) as i32) << (7 * i);
// The 5th byte is only allowed to have the 4 smallest bits set
if i == 4 && (current_byte & 0xf0 != 0) {
return Err(PacketBad.context("Bad 5th byte"));
}
if (current_byte & msb) == 0 {
break;
}
}
Ok(result)
}
pub(crate) fn as_varint(value: i32) -> Vec<u8> {
let mut bytes = vec![];
let mut reading_value = value;
let msb: u8 = 0b1000_0000;
let mask: i32 = 0b0111_1111;
for _ in 0 .. 5 {
let tmp = (reading_value & mask) as u8;
reading_value &= !mask;
reading_value = reading_value.rotate_right(7);
if reading_value == 0 {
bytes.push(tmp);
break;
}
bytes.push(tmp | msb);
}
bytes
}
pub(crate) fn get_string<B: ByteOrder>(buffer: &mut Buffer<B>) -> GDResult<String> {
let length = get_varint(buffer)? as usize;
let mut text = Vec::with_capacity(length);
for _ in 0 .. length {
text.push(buffer.read::<u8>()?)
}
String::from_utf8(text).map_err(|e| PacketBad.context(e))
}
pub(crate) fn as_string(value: &str) -> GDResult<Vec<u8>> {
let length = value
.len()
.try_into()
.map_err(|e| InvalidInput.context(e))?;
let mut buf = as_varint(length);
buf.extend(value.as_bytes());
Ok(buf)
}

View file

@ -0,0 +1,18 @@
//! Protocols that are currently implemented.
//!
//! A protocol will be here if it supports multiple entries, if not, its
//! implementation will be in that specific needed place, a protocol can be
//! independently queried.
/// Reference: [node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy1.js)
pub mod gamespy;
/// Reference: [Server List Ping](https://wiki.vg/Server_List_Ping)
pub mod minecraft;
/// Reference: [node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake1.js)
pub mod quake;
/// General types that are used by all protocols.
pub mod types;
/// Reference: [Server Query](https://developer.valvesoftware.com/wiki/Server_queries)
pub mod valve;
pub use types::{ExtraRequestSettings, GenericResponse, Protocol};

View file

@ -0,0 +1,146 @@
use byteorder::LittleEndian;
use crate::buffer::{Buffer, Utf8Decoder};
use crate::protocols::quake::types::Response;
use crate::protocols::types::TimeoutSettings;
use crate::socket::{Socket, UdpSocket};
use crate::utils::retry_on_timeout;
use crate::GDErrorKind::{PacketBad, TypeParse};
use crate::{GDErrorKind, GDResult};
use std::collections::HashMap;
use std::net::SocketAddr;
use std::slice::Iter;
pub trait QuakeClient {
type Player;
fn get_send_header<'a>() -> &'a str;
fn get_response_header<'a>() -> &'a str;
fn parse_player_string(data: Iter<&str>) -> GDResult<Self::Player>;
}
/// Send request and return result buffer.
/// This function will retry fetch on timeouts.
fn get_data<Client: QuakeClient>(
address: &SocketAddr,
timeout_settings: &Option<TimeoutSettings>,
) -> GDResult<Vec<u8>> {
let mut socket = UdpSocket::new(address)?;
socket.apply_timeout(timeout_settings)?;
retry_on_timeout(
TimeoutSettings::get_retries_or_default(timeout_settings),
move || get_data_impl::<Client>(&mut socket),
)
}
/// Send request and return result buffer (without retry logic).
fn get_data_impl<Client: QuakeClient>(socket: &mut UdpSocket) -> GDResult<Vec<u8>> {
socket.send(
&[
&[0xFF, 0xFF, 0xFF, 0xFF],
Client::get_send_header().as_bytes(),
&[0x00],
]
.concat(),
)?;
let data = socket.receive(None)?;
let mut bufferer = Buffer::<LittleEndian>::new(&data);
if bufferer.read::<u32>()? != u32::MAX {
return Err(PacketBad.context("Expected 4294967295"));
}
let response_header = Client::get_response_header().as_bytes();
if !bufferer.remaining_bytes().starts_with(response_header) {
Err(GDErrorKind::PacketBad)?;
}
bufferer.move_cursor(response_header.len() as isize)?;
Ok(bufferer.remaining_bytes().to_vec())
}
fn get_server_values(bufferer: &mut Buffer<LittleEndian>) -> GDResult<HashMap<String, String>> {
let data = bufferer.read_string::<Utf8Decoder>(Some([0x0A]))?;
let mut data_split = data.split('\\').collect::<Vec<&str>>();
if let Some(first) = data_split.first() {
if first == &"" {
data_split.remove(0);
}
}
let values = data_split.chunks(2);
let mut vars: HashMap<String, String> = HashMap::new();
for data in values {
let key = data.first();
let value = data.get(1);
if let Some(k) = key {
if let Some(v) = value {
vars.insert(k.to_string(), v.to_string());
}
}
}
Ok(vars)
}
fn get_players<Client: QuakeClient>(bufferer: &mut Buffer<LittleEndian>) -> GDResult<Vec<Client::Player>> {
let mut players: Vec<Client::Player> = Vec::new();
// this needs to be looked at again as theres no way to check if the buffer has
// a remaining null byte the original code was:
// while !bufferer.is_remaining_empty() && bufferer.remaining_data() != [0x00]
while !bufferer.remaining_length() == 0 {
let data = bufferer.read_string::<Utf8Decoder>(Some([0x0A]))?;
let data_split = data.split(' ').collect::<Vec<&str>>();
let data_iter = data_split.iter();
players.push(Client::parse_player_string(data_iter)?);
}
Ok(players)
}
pub fn client_query<Client: QuakeClient>(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response<Client::Player>> {
let data = get_data::<Client>(address, &timeout_settings)?;
let mut bufferer = Buffer::<LittleEndian>::new(&data);
let mut server_vars = get_server_values(&mut bufferer)?;
let players = get_players::<Client>(&mut bufferer)?;
Ok(Response {
name: server_vars
.remove("hostname")
.or(server_vars.remove("sv_hostname"))
.ok_or(GDErrorKind::PacketBad)?,
map: server_vars
.remove("mapname")
.or(server_vars.remove("map"))
.ok_or(GDErrorKind::PacketBad)?,
players_online: players.len() as u8,
players_maximum: server_vars
.remove("maxclients")
.or(server_vars.remove("sv_maxclients"))
.ok_or(GDErrorKind::PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?,
players,
game_version: server_vars
.remove("version")
.or(server_vars.remove("*version")),
unused_entries: server_vars,
})
}
pub fn remove_wrapping_quotes<'a>(string: &&'a str) -> &'a str {
match string.starts_with('\"') && string.ends_with('\"') {
false => string,
true => &string[1 .. string.len() - 1],
}
}

View file

@ -0,0 +1,74 @@
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub mod one;
pub mod three;
pub mod two;
/// All types used by the implementation.
pub mod types;
pub use types::*;
mod client;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QuakeVersion {
One,
Two,
Three,
}
/// Generate a module containing a query function for a quake game.
///
/// * `mod_name` - The name to be given to the game module (see ID naming
/// conventions in CONTRIBUTING.md).
/// * `pretty_name` - The full name of the game, will be used as the
/// documentation for the created module.
/// * `quake_ver`, `default_port` - Passed through to [game_query_fn].
macro_rules! game_query_mod {
($mod_name: ident, $pretty_name: expr, $quake_ver: ident, $default_port: literal) => {
#[doc = $pretty_name]
pub mod $mod_name {
crate::protocols::quake::game_query_fn!($quake_ver, $default_port);
}
};
}
pub(crate) use game_query_mod;
// Allow generating doc comments:
// https://users.rust-lang.org/t/macros-filling-text-in-comments/20473
/// Generate a query function for a quake game.
///
/// * `quake_ver` - The name of the [module](crate::protocols::quake) for the
/// quake version the game uses.
/// * `default_port` - The default port the game uses.
///
/// ```rust,ignore
/// use crate::protocols::quake::game_query_fn;
/// game_query_fn!(one, 27500);
/// ```
macro_rules! game_query_fn {
($quake_ver: ident, $default_port: literal) => {
use crate::protocols::quake::$quake_ver::Player;
crate::protocols::quake::game_query_fn! {@gen $quake_ver, Player, $default_port, concat!(
"Make a quake ", stringify!($quake_ver), " query with default timeout settings.\n\n",
"If port is `None`, then the default port (", stringify!($default_port), ") will be used.")}
};
(@gen $quake_ver: ident, $player_type: ty, $default_port: literal, $doc: expr) => {
#[doc = $doc]
pub fn query(
address: &std::net::IpAddr,
port: Option<u16>,
) -> crate::GDResult<crate::protocols::quake::Response<$player_type>> {
crate::protocols::quake::$quake_ver::query(
&std::net::SocketAddr::new(*address, port.unwrap_or($default_port)),
None,
)
}
};
}
pub(crate) use game_query_fn;

View file

@ -0,0 +1,87 @@
use crate::protocols::quake::client::{client_query, remove_wrapping_quotes, QuakeClient};
use crate::protocols::quake::Response;
use crate::protocols::types::{CommonPlayer, GenericPlayer, TimeoutSettings};
use crate::GDErrorKind::TypeParse;
use crate::{GDErrorKind, GDResult};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::slice::Iter;
use super::QuakePlayerType;
/// Quake 1 player data.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
/// Player's server id.
pub id: u8,
pub score: u16,
pub time: u16,
pub ping: u16,
pub name: String,
pub skin: String,
pub color_primary: u8,
pub color_secondary: u8,
}
impl QuakePlayerType for Player {
fn version(response: &Response<Self>) -> super::VersionedResponse { super::VersionedResponse::One(response) }
}
impl CommonPlayer for Player {
fn as_original(&self) -> GenericPlayer { GenericPlayer::QuakeOne(self) }
fn name(&self) -> &str { &self.name }
fn score(&self) -> Option<i32> { Some(self.score.into()) }
}
pub(crate) struct QuakeOne;
impl QuakeClient for QuakeOne {
type Player = Player;
fn get_send_header<'a>() -> &'a str { "status" }
fn get_response_header<'a>() -> &'a str { "n" }
fn parse_player_string(mut data: Iter<&str>) -> GDResult<Self::Player> {
Ok(Player {
id: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => v.parse().map_err(|e| TypeParse.context(e))?,
},
score: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => v.parse().map_err(|e| TypeParse.context(e))?,
},
time: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => v.parse().map_err(|e| TypeParse.context(e))?,
},
ping: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => v.parse().map_err(|e| TypeParse.context(e))?,
},
name: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => remove_wrapping_quotes(v).to_string(),
},
skin: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => remove_wrapping_quotes(v).to_string(),
},
color_primary: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => v.parse().map_err(|e| TypeParse.context(e))?,
},
color_secondary: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => v.parse().map_err(|e| TypeParse.context(e))?,
},
})
}
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response<Player>> {
client_query::<QuakeOne>(address, timeout_settings)
}

View file

@ -0,0 +1,24 @@
use crate::protocols::quake::client::{client_query, QuakeClient};
use crate::protocols::quake::two::QuakeTwo;
use crate::protocols::quake::Response;
use crate::protocols::types::TimeoutSettings;
use crate::GDResult;
use std::net::SocketAddr;
use std::slice::Iter;
pub use crate::protocols::quake::two::Player;
struct QuakeThree;
impl QuakeClient for QuakeThree {
type Player = Player;
fn get_send_header<'a>() -> &'a str { "getstatus" }
fn get_response_header<'a>() -> &'a str { "statusResponse\n" }
fn parse_player_string(data: Iter<&str>) -> GDResult<Self::Player> { QuakeTwo::parse_player_string(data) }
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response<Player>> {
client_query::<QuakeThree>(address, timeout_settings)
}

View file

@ -0,0 +1,67 @@
use crate::protocols::quake::client::{client_query, remove_wrapping_quotes, QuakeClient};
use crate::protocols::quake::one::QuakeOne;
use crate::protocols::quake::Response;
use crate::protocols::types::{CommonPlayer, GenericPlayer, TimeoutSettings};
use crate::GDErrorKind::TypeParse;
use crate::{GDErrorKind, GDResult};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::slice::Iter;
use super::QuakePlayerType;
/// Quake 2 player data.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
pub score: i32,
pub ping: u16,
pub name: String,
pub address: Option<String>,
}
impl QuakePlayerType for Player {
fn version(response: &Response<Self>) -> super::VersionedResponse {
super::VersionedResponse::TwoAndThree(response)
}
}
impl CommonPlayer for Player {
fn as_original(&self) -> GenericPlayer { GenericPlayer::QuakeTwo(self) }
fn name(&self) -> &str { &self.name }
fn score(&self) -> Option<i32> { Some(self.score) }
}
pub(crate) struct QuakeTwo;
impl QuakeClient for QuakeTwo {
type Player = Player;
fn get_send_header<'a>() -> &'a str { QuakeOne::get_send_header() }
fn get_response_header<'a>() -> &'a str { "print\n" }
fn parse_player_string(mut data: Iter<&str>) -> GDResult<Self::Player> {
Ok(Player {
score: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => v.parse().map_err(|e| TypeParse.context(e))?,
},
ping: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => v.parse().map_err(|e| TypeParse.context(e))?,
},
name: match data.next() {
None => Err(GDErrorKind::PacketBad)?,
Some(v) => remove_wrapping_quotes(v).to_string(),
},
address: data.next().map(|v| remove_wrapping_quotes(v).to_string()),
})
}
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response<Player>> {
client_query::<QuakeTwo>(address, timeout_settings)
}

View file

@ -0,0 +1,58 @@
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::protocols::{
types::{CommonPlayer, CommonResponse},
GenericResponse,
};
/// General server information's.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response<P> {
/// Name of the server.
pub name: String,
/// Map name.
pub map: String,
/// Current online players.
pub players: Vec<P>,
/// Number of players on the server.
pub players_online: u8,
/// Maximum number of players the server reports it can hold.
pub players_maximum: u8,
/// The server version.
pub game_version: Option<String>,
/// Other server entries that weren't used.
pub unused_entries: HashMap<String, String>,
}
pub trait QuakePlayerType: Sized + CommonPlayer {
fn version(response: &Response<Self>) -> VersionedResponse;
}
impl<P: QuakePlayerType> CommonResponse for Response<P> {
fn as_original(&self) -> GenericResponse { GenericResponse::Quake(P::version(self)) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn game_version(&self) -> Option<&str> { self.game_version.as_deref() }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn players_maximum(&self) -> u32 { self.players_maximum.into() }
fn players_online(&self) -> u32 { self.players_online.into() }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
Some(
self.players
.iter()
.map(|p| p as &dyn CommonPlayer)
.collect(),
)
}
}
/// Versioned response type
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionedResponse<'a> {
One(&'a Response<crate::protocols::quake::one::Player>),
TwoAndThree(&'a Response<crate::protocols::quake::two::Player>),
}

View file

@ -0,0 +1,365 @@
use crate::protocols::{gamespy, minecraft, quake, valve};
use crate::GDErrorKind::InvalidInput;
use crate::GDResult;
use std::time::Duration;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// Enumeration of all custom protocols
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ProprietaryProtocol {
TheShip,
FFOW,
JC2M,
}
/// Enumeration of all valid protocol types
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Protocol {
Gamespy(gamespy::GameSpyVersion),
Minecraft(Option<minecraft::types::Server>),
Quake(quake::QuakeVersion),
Valve(valve::SteamApp),
#[cfg(feature = "games")]
PROPRIETARY(ProprietaryProtocol),
}
/// All response types
#[derive(Debug, Clone, PartialEq)]
pub enum GenericResponse<'a> {
GameSpy(gamespy::VersionedResponse<'a>),
Minecraft(minecraft::VersionedResponse<'a>),
Quake(quake::VersionedResponse<'a>),
Valve(&'a valve::Response),
#[cfg(feature = "games")]
TheShip(&'a crate::games::theship::Response),
#[cfg(feature = "games")]
FFOW(&'a crate::games::ffow::Response),
#[cfg(feature = "games")]
JC2M(&'a crate::games::jc2m::Response),
}
/// All player types
#[derive(Debug, Clone, PartialEq)]
pub enum GenericPlayer<'a> {
Valve(&'a valve::ServerPlayer),
QuakeOne(&'a quake::one::Player),
QuakeTwo(&'a quake::two::Player),
Minecraft(&'a minecraft::Player),
Gamespy(gamespy::VersionedPlayer<'a>),
#[cfg(feature = "games")]
TheShip(&'a crate::games::theship::TheShipPlayer),
#[cfg(feature = "games")]
JCMP2(&'a crate::games::jc2m::Player),
}
pub trait CommonResponse {
/// Get the original response type
fn as_original(&self) -> GenericResponse;
/// Get a struct that can be stored as JSON (you don't need to override
/// this)
fn as_json(&self) -> CommonResponseJson {
CommonResponseJson {
name: self.name(),
description: self.description(),
game_mode: self.game_mode(),
game_version: self.game_version(),
has_password: self.has_password(),
map: self.map(),
players_maximum: self.players_maximum(),
players_online: self.players_online(),
players_bots: self.players_bots(),
players: self
.players()
.map(|players| players.iter().map(|p| p.as_json()).collect()),
}
}
/// The name of the server
fn name(&self) -> Option<&str> { None }
/// Description of the server
fn description(&self) -> Option<&str> { None }
/// Name of the current game or game mode
fn game_mode(&self) -> Option<&str> { None }
/// Version of the game being run on the server
fn game_version(&self) -> Option<&str> { None }
/// The current map name
fn map(&self) -> Option<&str> { None }
/// Maximum number of players allowed to connect
fn players_maximum(&self) -> u32;
/// Number of players currently connected
fn players_online(&self) -> u32;
/// Number of bots currently connected
fn players_bots(&self) -> Option<u32> { None }
/// Whether the server requires a password to join
fn has_password(&self) -> Option<bool> { None }
/// Currently connected players
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> { None }
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct CommonResponseJson<'a> {
pub name: Option<&'a str>,
pub description: Option<&'a str>,
pub game_mode: Option<&'a str>,
pub game_version: Option<&'a str>,
pub map: Option<&'a str>,
pub players_maximum: u32,
pub players_online: u32,
pub players_bots: Option<u32>,
pub has_password: Option<bool>,
pub players: Option<Vec<CommonPlayerJson<'a>>>,
}
pub trait CommonPlayer {
/// Get the original player type
fn as_original(&self) -> GenericPlayer;
/// Get a struct that can be stored as JSON (you don't need to override
/// this)
fn as_json(&self) -> CommonPlayerJson {
CommonPlayerJson {
name: self.name(),
score: self.score(),
}
}
/// Player name
fn name(&self) -> &str;
/// Player score
fn score(&self) -> Option<i32> { None }
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct CommonPlayerJson<'a> {
pub name: &'a str,
pub score: Option<i32>,
}
/// Timeout settings for socket operations
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TimeoutSettings {
read: Option<Duration>,
write: Option<Duration>,
retries: usize,
}
impl TimeoutSettings {
/// Construct new settings, passing None will block indefinitely.
/// Passing zero Duration throws GDErrorKind::[InvalidInput].
///
/// The retry count is the number of extra tries once the original request
/// fails, so a value of "0" will only make a single request, whereas
/// "1" will try the request again once if it fails.
/// The retry count is per-request so for multi-request queries (valve) if a
/// single part fails that part can be retried up to `retries` times.
pub fn new(read: Option<Duration>, write: Option<Duration>, retries: usize) -> GDResult<Self> {
if let Some(read_duration) = read {
if read_duration == Duration::new(0, 0) {
return Err(InvalidInput.context("Read duration must not be 0"));
}
}
if let Some(write_duration) = write {
if write_duration == Duration::new(0, 0) {
return Err(InvalidInput.context("Write duration must not be 0"));
}
}
Ok(Self {
read,
write,
retries,
})
}
/// Get the read timeout.
pub const fn get_read(&self) -> Option<Duration> { self.read }
/// Get the write timeout.
pub const fn get_write(&self) -> Option<Duration> { self.write }
/// Get number of retries
pub const fn get_retries(&self) -> usize { self.retries }
/// Get the number of retries if there are timeout settings else fall back
/// to the default
pub const fn get_retries_or_default(timeout_settings: &Option<TimeoutSettings>) -> usize {
if let Some(timeout_settings) = timeout_settings {
timeout_settings.get_retries()
} else {
TimeoutSettings::const_default().get_retries()
}
}
/// Get the read and write durations if there are timeout settings else fall
/// back to the defaults
pub const fn get_read_and_write_or_defaults(
timeout_settings: &Option<TimeoutSettings>,
) -> (Option<Duration>, Option<Duration>) {
if let Some(timeout_settings) = timeout_settings {
(timeout_settings.get_read(), timeout_settings.get_write())
} else {
let default = TimeoutSettings::const_default();
(default.get_read(), default.get_write())
}
}
/// Default values are 4 seconds for both read and write, no retries.
pub const fn const_default() -> Self {
Self {
read: Some(Duration::from_secs(4)),
write: Some(Duration::from_secs(4)),
retries: 0,
}
}
}
impl Default for TimeoutSettings {
/// Default values are 4 seconds for both read and write, no retries.
fn default() -> Self { Self::const_default() }
}
/// Generic extra request settings
///
/// Fields of this struct may not be used depending on which protocol
/// is selected, the individual fields link to the specific places
/// they will be used with additional documentation.
///
/// ## Examples
/// Create minecraft settings with builder:
/// ```
/// use gamedig::protocols::{minecraft, ExtraRequestSettings};
/// let mc_settings: minecraft::RequestSettings = ExtraRequestSettings::default().set_hostname("mc.hypixel.net".to_string()).into();
/// ```
///
/// Create valve settings with builder:
/// ```
/// use gamedig::protocols::{valve, ExtraRequestSettings};
/// let valve_settings: valve::GatheringSettings = ExtraRequestSettings::default().set_check_app_id(false).into();
/// ```
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
pub struct ExtraRequestSettings {
/// The server's hostname.
///
/// Used by:
/// - [minecraft::RequestSettings#structfield.hostname]
pub hostname: Option<String>,
/// The protocol version to use.
///
/// Used by:
/// - [minecraft::RequestSettings#structfield.protocol_version]
pub protocol_version: Option<i32>,
/// Whether to gather player information
///
/// Used by:
/// - [valve::GatheringSettings#structfield.players]
pub gather_players: Option<bool>,
/// Whether to gather rule information.
///
/// Used by:
/// - [valve::GatheringSettings#structfield.rules]
pub gather_rules: Option<bool>,
/// Whether to check if the App ID is valid.
///
/// Used by:
/// - [valve::GatheringSettings#structfield.check_app_id]
pub check_app_id: Option<bool>,
}
impl ExtraRequestSettings {
/// [Sets hostname](ExtraRequestSettings#structfield.hostname)
pub fn set_hostname(mut self, hostname: String) -> Self {
self.hostname = Some(hostname);
self
}
/// [Sets protocol
/// version](ExtraRequestSettings#structfield.protocol_version)
pub fn set_protocol_version(mut self, protocol_version: i32) -> Self {
self.protocol_version = Some(protocol_version);
self
}
/// [Sets gather players](ExtraRequestSettings#structfield.gather_players)
pub fn set_gather_players(mut self, gather_players: bool) -> Self {
self.gather_players = Some(gather_players);
self
}
/// [Sets gather rules](ExtraRequestSettings#structfield.gather_rules)
pub fn set_gather_rules(mut self, gather_rules: bool) -> Self {
self.gather_rules = Some(gather_rules);
self
}
/// [Sets check app ID](ExtraRequestSettings#structfield.check_app_id)
pub fn set_check_app_id(mut self, check_app_id: bool) -> Self {
self.check_app_id = Some(check_app_id);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
// Test creating new TimeoutSettings with valid durations
#[test]
fn test_new_with_valid_durations() -> GDResult<()> {
// Define valid read and write durations
let read_duration = Duration::from_secs(1);
let write_duration = Duration::from_secs(2);
// Create new TimeoutSettings with the valid durations
let timeout_settings = TimeoutSettings::new(Some(read_duration), Some(write_duration), 0)?;
// Verify that the get_read and get_write methods return the expected values
assert_eq!(timeout_settings.get_read(), Some(read_duration));
assert_eq!(timeout_settings.get_write(), Some(write_duration));
Ok(())
}
// Test creating new TimeoutSettings with a zero duration
#[test]
fn test_new_with_zero_duration() {
// Define a zero read duration and a valid write duration
let read_duration = Duration::new(0, 0);
let write_duration = Duration::from_secs(2);
// Try to create new TimeoutSettings with the zero read duration (this should
// fail)
let result = TimeoutSettings::new(Some(read_duration), Some(write_duration), 0);
// Verify that the function returned an error and that the error type is
// InvalidInput
assert!(result.is_err());
assert_eq!(result.unwrap_err(), crate::GDErrorKind::InvalidInput.into());
}
// Test that the default TimeoutSettings values are correct
#[test]
fn test_default_values() {
// Get the default TimeoutSettings values
let default_settings = TimeoutSettings::default();
// Verify that the get_read and get_write methods return the expected default
// values
assert_eq!(default_settings.get_read(), Some(Duration::from_secs(4)));
assert_eq!(default_settings.get_write(), Some(Duration::from_secs(4)));
}
// Test that extra request settings can be converted
#[test]
fn test_extra_request_settings() {
let settings = ExtraRequestSettings::default();
let _: minecraft::RequestSettings = settings.clone().into();
let _: valve::GatheringSettings = settings.into();
}
}

View file

@ -0,0 +1,60 @@
/// The implementation.
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;
/// Generate a module containing a query function for a valve game.
///
/// * `mod_name` - The name to be given to the game module (see ID naming
/// conventions in CONTRIBUTING.md).
/// * `pretty_name` - The full name of the game, will be used as the
/// documentation for the created module.
/// * `steam_app`, `default_port` - Passed through to [game_query_fn].
macro_rules! game_query_mod {
($mod_name: ident, $pretty_name: expr, $steam_app: ident, $default_port: literal) => {
#[doc = $pretty_name]
pub mod $mod_name {
crate::protocols::valve::game_query_fn!($steam_app, $default_port);
}
};
}
pub(crate) use game_query_mod;
// Allow generating doc comments:
// https://users.rust-lang.org/t/macros-filling-text-in-comments/20473
/// Generate a query function for a valve game.
///
/// * `steam_app` - The entry in the [SteamApp] enum that the game uses.
/// * `default_port` - The default port the game uses.
///
/// ```rust,ignore
/// use crate::protocols::valve::game_query_fn;
/// game_query_fn!(TEAMFORTRESS2, 27015);
/// ```
macro_rules! game_query_fn {
($steam_app: ident, $default_port: literal) => {
crate::protocols::valve::game_query_fn!{@gen $steam_app, $default_port, concat!(
"Make a valve query for ", stringify!($steam_app), " with default timeout settings and default extra request settings.\n\n",
"If port is `None`, then the default port (", stringify!($default_port), ") will be used.")}
};
(@gen $steam_app: ident, $default_port: literal, $doc: expr) => {
#[doc = $doc]
pub fn query(address: &std::net::IpAddr, port: Option<u16>) -> crate::GDResult<crate::protocols::valve::game::Response> {
let valve_response = crate::protocols::valve::query(
&std::net::SocketAddr::new(*address, port.unwrap_or($default_port)),
crate::protocols::valve::SteamApp::$steam_app.as_engine(),
None,
None,
)?;
Ok(crate::protocols::valve::game::Response::new_from_valve_response(valve_response))
}
};
}
pub(crate) use game_query_fn;

View file

@ -0,0 +1,483 @@
use crate::{
buffer::Buffer,
protocols::{
types::TimeoutSettings,
valve::{
types::{
Environment,
ExtraData,
GatheringSettings,
Request,
Response,
Server,
ServerInfo,
ServerPlayer,
TheShip,
},
Engine,
ModData,
SteamApp,
},
},
socket::{Socket, UdpSocket},
utils::{retry_on_timeout, u8_lower_upper},
GDErrorKind::{BadGame, Decompress, UnknownEnumCast},
GDResult,
};
use bzip2_rs::decoder::Decoder;
use crate::buffer::Utf8Decoder;
use crate::protocols::valve::Packet;
use byteorder::LittleEndian;
use std::collections::HashMap;
use std::net::SocketAddr;
#[derive(Debug)]
#[allow(dead_code)] //remove this later on
struct SplitPacket {
pub header: u32,
pub id: u32,
pub total: u8,
pub number: u8,
pub size: u16,
/// None means its not compressed, Some means it is
/// and it contains (size and crc32)
pub decompressed: Option<(u32, u32)>,
payload: Vec<u8>,
}
impl SplitPacket {
fn new(engine: &Engine, protocol: u8, buffer: &mut Buffer<LittleEndian>) -> GDResult<Self> {
let header = buffer.read()?; //buffer.get_u32()?;
let id = buffer.read()?;
let (total, number, size, decompressed) = match engine {
Engine::GoldSrc(_) => {
let (lower, upper) = u8_lower_upper(buffer.read()?);
(lower, upper, 0, None)
}
Engine::Source(_) => {
let total = buffer.read()?;
let number = buffer.read()?;
let size = match protocol == 7 && (*engine == SteamApp::CSS.as_engine()) {
// certain apps with protocol = 7 dont have this field
false => buffer.read()?,
true => 1248,
};
let is_compressed = ((id >> 31) & 1u32) == 1u32;
let decompressed = match is_compressed {
false => None,
true => Some((buffer.read()?, buffer.read()?)),
};
(total, number, size, decompressed)
}
};
Ok(Self {
header,
id,
total,
number,
size,
decompressed,
payload: buffer.remaining_bytes().to_vec(),
})
}
fn get_payload(&self) -> GDResult<Vec<u8>> {
if let Some(decompressed) = self.decompressed {
let mut decoder = Decoder::new();
decoder
.write(&self.payload)
.map_err(|e| Decompress.context(e))?;
let decompressed_size = decompressed.0 as usize;
let mut decompressed_payload = vec![0; decompressed_size];
decoder
.read(&mut decompressed_payload)
.map_err(|e| Decompress.context(e))?;
if decompressed_payload.len() != decompressed_size
|| crc32fast::hash(&decompressed_payload) != decompressed.1
{
Err(Decompress.context(format!(
"Decompressed size {} was not expected {}",
decompressed_payload.len(),
decompressed_size
)))
} else {
Ok(decompressed_payload)
}
} else {
Ok(self.payload.clone())
}
}
}
pub(crate) struct ValveProtocol {
socket: UdpSocket,
retry_count: usize,
}
static PACKET_SIZE: usize = 6144;
impl ValveProtocol {
pub fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = UdpSocket::new(address)?;
let retry_count = timeout_settings
.as_ref()
.map(|t| t.get_retries())
.unwrap_or_else(|| TimeoutSettings::default().get_retries());
socket.apply_timeout(&timeout_settings)?;
Ok(Self {
socket,
retry_count,
})
}
fn receive(&mut self, engine: &Engine, protocol: u8, buffer_size: usize) -> GDResult<Packet> {
let data = self.socket.receive(Some(buffer_size))?;
let mut buffer = Buffer::<LittleEndian>::new(&data);
let header: u8 = buffer.read()?;
buffer.move_cursor(-1)?;
if header == 0xFE {
// the packet is split
let mut main_packet = SplitPacket::new(engine, protocol, &mut buffer)?;
let mut chunk_packets = Vec::with_capacity((main_packet.total - 1) as usize);
for _ in 1 .. main_packet.total {
let new_data = self.socket.receive(Some(buffer_size))?;
buffer = Buffer::<LittleEndian>::new(&new_data);
let chunk_packet = SplitPacket::new(engine, protocol, &mut buffer)?;
chunk_packets.push(chunk_packet);
}
chunk_packets.sort_by(|a, b| a.number.cmp(&b.number));
for chunk_packet in chunk_packets {
main_packet.payload.extend(chunk_packet.payload);
}
let payload = main_packet.get_payload()?; // Creating a non-temporary value here
let mut new_packet_buffer = Buffer::<LittleEndian>::new(&payload); // Using the non-temporary value here
Ok(Packet::new_from_bufferer(&mut new_packet_buffer)?)
} else {
Packet::new_from_bufferer(&mut buffer)
}
}
pub fn get_kind_request_data(&mut self, engine: &Engine, protocol: u8, kind: Request) -> GDResult<Vec<u8>> {
let data = self.get_request_data(engine, protocol, kind as u8, kind.get_default_payload())?;
Ok(data)
}
/// Ask for a specific request only.
/// This function will retry fetch on timeouts.
pub fn get_request_data(&mut self, engine: &Engine, protocol: u8, kind: u8, payload: Vec<u8>) -> GDResult<Vec<u8>> {
retry_on_timeout(self.retry_count, || {
self.get_request_data_impl(engine, protocol, kind, payload.clone())
})
}
/// Ask for a specific request only (without retry logic).
fn get_request_data_impl(
&mut self,
engine: &Engine,
protocol: u8,
kind: u8,
payload: Vec<u8>,
) -> GDResult<Vec<u8>> {
let request_initial_packet = Packet::new(kind, payload).to_bytes();
self.socket.send(&request_initial_packet)?;
let mut packet = self.receive(engine, protocol, PACKET_SIZE)?;
while packet.kind == 0x41 {
// 'A'
let challenge = packet.payload;
const INFO: u8 = Request::Info as u8;
let challenge_packet = Packet::new(
kind,
match kind {
INFO => [Request::Info.get_default_payload(), challenge].concat(),
_ => challenge,
},
)
.to_bytes();
self.socket.send(&challenge_packet)?;
packet = self.receive(engine, protocol, PACKET_SIZE)?;
}
Ok(packet.payload)
}
fn get_goldsrc_server_info(buffer: &mut Buffer<LittleEndian>) -> GDResult<ServerInfo> {
let _header: u8 = buffer.read()?; //get the header (useless info)
let _address: String = buffer.read_string::<Utf8Decoder>(None)?; //get the server address (useless info)
let name = buffer.read_string::<Utf8Decoder>(None)?;
let map = buffer.read_string::<Utf8Decoder>(None)?;
let folder = buffer.read_string::<Utf8Decoder>(None)?;
let game_mode = buffer.read_string::<Utf8Decoder>(None)?;
let players = buffer.read()?;
let max_players = buffer.read()?;
let protocol = buffer.read()?;
let server_type = match buffer.read::<u8>()? {
68 => Server::Dedicated, //'D'
76 => Server::NonDedicated, //'L'
80 => Server::TV, //'P'
_ => Err(UnknownEnumCast)?,
};
let environment_type = match buffer.read::<u8>()? {
76 => Environment::Linux, //'L'
87 => Environment::Windows, //'W'
_ => Err(UnknownEnumCast)?,
};
let has_password = buffer.read::<u8>()? == 1;
let is_mod = buffer.read::<u8>()? == 1;
let mod_data = match is_mod {
false => None,
true => {
Some(ModData {
link: buffer.read_string::<Utf8Decoder>(None)?,
download_link: buffer.read_string::<Utf8Decoder>(None)?,
version: buffer.read()?,
size: buffer.read()?,
multiplayer_only: buffer.read::<u8>()? == 1,
has_own_dll: buffer.read::<u8>()? == 1,
})
}
};
let vac_secured = buffer.read::<u8>()? == 1;
let bots = buffer.read::<u8>()?;
Ok(ServerInfo {
protocol_version: protocol,
name,
map,
folder,
game_mode,
appid: 0, // not present in the obsolete response
players_online: players,
players_maximum: max_players,
players_bots: bots,
server_type,
environment_type,
has_password,
vac_secured,
the_ship: None,
game_version: "".to_string(), // a version field only for the mod
extra_data: None,
is_mod,
mod_data,
})
}
/// Get the server information's.
fn get_server_info(&mut self, engine: &Engine) -> GDResult<ServerInfo> {
let data = self.get_kind_request_data(engine, 0, Request::Info)?;
let mut buffer = Buffer::<LittleEndian>::new(&data);
if let Engine::GoldSrc(force) = engine {
if *force {
return Self::get_goldsrc_server_info(&mut buffer);
}
}
let protocol = buffer.read()?;
let name = buffer.read_string::<Utf8Decoder>(None)?;
let map = buffer.read_string::<Utf8Decoder>(None)?;
let folder = buffer.read_string::<Utf8Decoder>(None)?;
let game_mode = buffer.read_string::<Utf8Decoder>(None)?;
let mut appid = buffer.read::<u16>()? as u32;
let players = buffer.read()?;
let max_players = buffer.read()?;
let bots = buffer.read()?;
let server_type = Server::from_gldsrc(buffer.read()?)?;
let environment_type = Environment::from_gldsrc(buffer.read()?)?;
let has_password = buffer.read::<u8>()? == 1;
let vac_secured = buffer.read::<u8>()? == 1;
let the_ship = match *engine == SteamApp::THESHIP.as_engine() {
false => None,
true => {
Some(TheShip {
mode: buffer.read()?,
witnesses: buffer.read()?,
duration: buffer.read()?,
})
}
};
let game_version = buffer.read_string::<Utf8Decoder>(None)?;
let extra_data = match buffer.read::<u8>() {
Err(_) => None,
Ok(value) => {
Some(ExtraData {
port: match (value & 0x80) > 0 {
false => None,
true => Some(buffer.read()?),
},
steam_id: match (value & 0x10) > 0 {
false => None,
true => Some(buffer.read()?),
},
tv_port: match (value & 0x40) > 0 {
false => None,
true => Some(buffer.read()?),
},
tv_name: match (value & 0x40) > 0 {
false => None,
true => Some(buffer.read_string::<Utf8Decoder>(None)?),
},
keywords: match (value & 0x20) > 0 {
false => None,
true => Some(buffer.read_string::<Utf8Decoder>(None)?),
},
game_id: match (value & 0x01) > 0 {
false => None,
true => {
let gid = buffer.read()?;
appid = (gid & ((1 << 24) - 1)) as u32;
Some(gid)
}
},
})
}
};
Ok(ServerInfo {
protocol_version: protocol,
name,
map,
folder,
game_mode,
appid,
players_online: players,
players_maximum: max_players,
players_bots: bots,
server_type,
environment_type,
has_password,
vac_secured,
the_ship,
game_version,
extra_data,
is_mod: false,
mod_data: None,
})
}
/// Get the server player's.
fn get_server_players(&mut self, engine: &Engine, protocol: u8) -> GDResult<Vec<ServerPlayer>> {
let data = self.get_kind_request_data(engine, protocol, Request::Players)?;
let mut buffer = Buffer::<LittleEndian>::new(&data);
let count = buffer.read::<u8>()? as usize;
let mut players: Vec<ServerPlayer> = Vec::with_capacity(count);
for _ in 0 .. count {
buffer.move_cursor(1)?; //skip the index byte
players.push(ServerPlayer {
name: buffer.read_string::<Utf8Decoder>(None)?,
score: buffer.read()?,
duration: buffer.read()?,
deaths: match *engine == SteamApp::THESHIP.as_engine() {
false => None,
true => Some(buffer.read()?),
},
money: match *engine == SteamApp::THESHIP.as_engine() {
false => None,
true => Some(buffer.read()?),
},
});
}
Ok(players)
}
/// Get the server's rules.
fn get_server_rules(&mut self, engine: &Engine, protocol: u8) -> GDResult<HashMap<String, String>> {
let data = self.get_kind_request_data(engine, protocol, Request::Rules)?;
let mut buffer = Buffer::<LittleEndian>::new(&data);
let count = buffer.read::<u16>()? as usize;
let mut rules: HashMap<String, String> = HashMap::with_capacity(count);
for _ in 0 .. count {
let name = buffer.read_string::<Utf8Decoder>(None)?;
let value = buffer.read_string::<Utf8Decoder>(None)?;
rules.insert(name, value);
}
if *engine == SteamApp::ROR2.as_engine() {
rules.remove("Test");
}
Ok(rules)
}
}
/// Query a server by providing the address, the port, the app, gather and
/// timeout settings. Providing None to the settings results in using the
/// default values for them
/// (GatherSettings::[default](GatheringSettings::default),
/// TimeoutSettings::[default](TimeoutSettings::default)).
pub fn query(
address: &SocketAddr,
engine: Engine,
gather_settings: Option<GatheringSettings>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let response_gather_settings = gather_settings.unwrap_or_default();
get_response(address, engine, response_gather_settings, timeout_settings)
}
fn get_response(
address: &SocketAddr,
engine: Engine,
gather_settings: GatheringSettings,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let mut client = ValveProtocol::new(address, timeout_settings)?;
let info = client.get_server_info(&engine)?;
if let Engine::Source(Some(appids)) = &engine {
let mut is_specified_id = false;
if appids.0 == info.appid {
is_specified_id = true;
} else if let Some(dedicated_appid) = appids.1 {
if dedicated_appid == info.appid {
is_specified_id = true;
}
}
if !is_specified_id && gather_settings.check_app_id {
return Err(BadGame.context(format!("AppId: {}", info.appid)));
}
}
let protocol = info.protocol_version;
Ok(Response {
info,
players: match gather_settings.players {
false => None,
true => Some(client.get_server_players(&engine, protocol)?),
},
rules: match gather_settings.rules {
false => None,
true => Some(client.get_server_rules(&engine, protocol)?),
},
})
}

View file

@ -0,0 +1,556 @@
use std::collections::HashMap;
use crate::protocols::types::{CommonPlayer, CommonResponse, ExtraRequestSettings, GenericPlayer};
use crate::GDErrorKind::UnknownEnumCast;
use crate::GDResult;
use crate::{buffer::Buffer, protocols::GenericResponse};
use byteorder::LittleEndian;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// The type of the server.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Server {
Dedicated,
NonDedicated,
TV,
}
impl Server {
pub(crate) fn from_gldsrc(value: u8) -> GDResult<Self> {
Ok(match value {
100 => Self::Dedicated, //'d'
108 => Self::NonDedicated, //'l'
112 => Self::TV, //'p'
_ => Err(UnknownEnumCast)?,
})
}
}
/// The Operating System that the server is on.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Environment {
Linux,
Windows,
Mac,
}
impl Environment {
pub(crate) fn from_gldsrc(value: u8) -> GDResult<Self> {
Ok(match value {
108 => Self::Linux, //'l'
119 => Self::Windows, //'w'
109 | 111 => Self::Mac, //'m' or 'o'
_ => Err(UnknownEnumCast)?,
})
}
}
/// A query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct Response {
pub info: ServerInfo,
pub players: Option<Vec<ServerPlayer>>,
pub rules: Option<HashMap<String, String>>,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse { GenericResponse::Valve(self) }
fn name(&self) -> Option<&str> { Some(&self.info.name) }
fn game_mode(&self) -> Option<&str> { Some(&self.info.game_mode) }
fn game_version(&self) -> Option<&str> { Some(&self.info.game_version) }
fn map(&self) -> Option<&str> { Some(&self.info.map) }
fn players_maximum(&self) -> u32 { self.info.players_maximum.into() }
fn players_online(&self) -> u32 { self.info.players_online.into() }
fn players_bots(&self) -> Option<u32> { Some(self.info.players_bots.into()) }
fn has_password(&self) -> Option<bool> { Some(self.info.has_password) }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
self.players
.as_ref()
.map(|p| p.iter().map(|p| p as &dyn CommonPlayer).collect())
}
}
/// General server information's.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ServerInfo {
/// Protocol used by the server.
pub protocol_version: u8,
/// Name of the server.
pub name: String,
/// Map name.
pub map: String,
/// Name of the folder containing the game files.
pub folder: String,
/// The server-declared name of the game/game mode.
pub game_mode: String,
/// [Steam Application ID](https://developer.valvesoftware.com/wiki/Steam_Application_ID) of game.
pub appid: u32,
/// Number of players on the server.
pub players_online: u8,
/// Maximum number of players the server reports it can hold.
pub players_maximum: u8,
/// Number of bots on the server.
pub players_bots: u8,
/// Dedicated, NonDedicated or SourceTV
pub server_type: Server,
/// The Operating System that the server is on.
pub environment_type: Environment,
/// Indicates whether the server requires a password.
pub has_password: bool,
/// Indicates whether the server uses VAC.
pub vac_secured: bool,
/// [The ship](https://developer.valvesoftware.com/wiki/The_Ship) extra data
pub the_ship: Option<TheShip>,
/// Version of the game installed on the server.
pub game_version: String,
/// Some extra data that the server might provide or not.
pub extra_data: Option<ExtraData>,
/// GoldSrc only: Indicates whether the hosted game is a mod.
pub is_mod: bool,
/// GoldSrc only: If the game is a mod, provide additional data.
pub mod_data: Option<ModData>,
}
/// A server player.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub struct ServerPlayer {
/// Player's name.
pub name: String,
/// General score.
pub score: i32,
/// How long a player has been in the server (seconds).
pub duration: f32,
/// Only for [the ship](https://developer.valvesoftware.com/wiki/The_Ship): deaths count
pub deaths: Option<u32>, // the_ship
/// Only for [the ship](https://developer.valvesoftware.com/wiki/The_Ship): money amount
pub money: Option<u32>, // the_ship
}
impl CommonPlayer for ServerPlayer {
fn as_original(&self) -> GenericPlayer { GenericPlayer::Valve(self) }
fn name(&self) -> &str { &self.name }
fn score(&self) -> Option<i32> { Some(self.score) }
}
/// Only present for [the ship](https://developer.valvesoftware.com/wiki/The_Ship).
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TheShip {
pub mode: u8,
pub witnesses: u8,
pub duration: u8,
}
/// Some extra data that the server might provide or not.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ExtraData {
/// The server's game port number.
pub port: Option<u16>,
/// Server's SteamID.
pub steam_id: Option<u64>,
/// SourceTV's port.
pub tv_port: Option<u16>,
/// SourceTV's name.
pub tv_name: Option<String>,
/// Keywords that describe the server according to it.
pub keywords: Option<String>,
/// The server's 64-bit GameID.
pub game_id: Option<u64>,
}
/// Data related to GoldSrc Mod response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ModData {
pub link: String,
pub download_link: String,
pub version: u32,
pub size: u32,
pub multiplayer_only: bool,
pub has_own_dll: bool,
}
pub(crate) type ExtractedData = (
Option<u16>,
Option<u64>,
Option<u16>,
Option<String>,
Option<String>,
);
pub(crate) fn get_optional_extracted_data(data: Option<ExtraData>) -> ExtractedData {
match data {
None => (None, None, None, None, None),
Some(ed) => (ed.port, ed.steam_id, ed.tv_port, ed.tv_name, ed.keywords),
}
}
#[derive(Debug, Clone)]
pub(crate) struct Packet {
pub header: u32,
pub kind: u8,
pub payload: Vec<u8>,
}
impl Packet {
pub fn new(kind: u8, payload: Vec<u8>) -> Self {
Self {
header: u32::MAX, // FF FF FF FF
kind,
payload,
}
}
pub fn new_from_bufferer(buffer: &mut Buffer<LittleEndian>) -> GDResult<Self> {
Ok(Self {
header: buffer.read::<u32>()?,
kind: buffer.read::<u8>()?,
payload: buffer.remaining_bytes().to_vec(),
})
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut buf = Vec::from(self.header.to_be_bytes());
buf.push(self.kind);
buf.extend(&self.payload);
buf
}
}
/// The type of the request, see the [protocol](https://developer.valvesoftware.com/wiki/Server_queries).
#[derive(Eq, PartialEq, Copy, Clone)]
#[repr(u8)]
pub(crate) enum Request {
/// Known as `A2S_INFO`
Info = 0x54,
/// Known as `A2S_PLAYERS`
Players = 0x55,
/// Known as `A2S_RULES`
Rules = 0x56,
}
impl Request {
pub fn get_default_payload(self) -> Vec<u8> {
match self {
Self::Info => String::from("Source Engine Query\0").into_bytes(),
_ => vec![0xFF, 0xFF, 0xFF, 0xFF],
}
}
}
/// Supported steam apps
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum SteamApp {
/// Counter-Strike
COUNTERSTRIKE,
/// Creativerse
CREATIVERSE,
/// Team Fortress Classic
TFC,
/// Day of Defeat
DOD,
/// Counter-Strike: Condition Zero
CSCZ,
/// Counter-Strike: Source
CSS,
/// Day of Defeat: Source
DODS,
/// Half-Life 2 Deathmatch
HL2D,
/// Half-Life Deathmatch: Source
HLDS,
/// Team Fortress 2
TEAMFORTRESS2,
/// Left 4 Dead
LEFT4DEAD,
/// Left 4 Dead
LEFT4DEAD2,
/// Alien Swarm
ALIENSWARM,
/// Counter-Strike: Global Offensive
CSGO,
/// The Ship
THESHIP,
/// Garry's Mod
GARRYSMOD,
/// Age of Chivalry
AOC,
/// Insurgency: Modern Infantry Combat
IMIC,
/// ARMA 2: Operation Arrowhead
A2OA,
/// Project Zomboid
PROJECTZOMBOID,
/// Insurgency
INSURGENCY,
/// Sven Co-op
SCO,
/// 7 Days To Die
SD2D,
/// Rust
RUST,
/// Ballistic Overkill
BALLISTICOVERKILL,
/// Don't Starve Together
DST,
/// BrainBread 2
BRAINBREAD2,
/// Codename CURE
CODENAMECURE,
/// Black Mesa
BLACKMESA,
/// Colony Survival
COLONYSURVIVAL,
/// Avorion
AVORION,
/// Day of Infamy
DOI,
/// The Forest
THEFOREST,
/// Unturned
UNTURNED,
/// ARK: Survival Evolved
ASE,
/// Battalion 1944
BATTALION1944,
/// Insurgency: Sandstorm
INSURGENCYSANDSTORM,
/// Alien Swarm: Reactive Drop
ASRD,
/// Risk of Rain 2
ROR2,
/// Operation: Harsh Doorstop
OHD,
/// Onset
ONSET,
/// V Rising
VRISING,
/// Hell Let Loose
HLL,
/// Barotrauma
BAROTRAUMA,
}
impl SteamApp {
/// Get the specified app as engine.
pub const fn as_engine(&self) -> Engine {
match self {
Self::CSS => Engine::new_source(240),
Self::DODS => Engine::new_source(300),
Self::HL2D => Engine::new_source(320),
Self::HLDS => Engine::new_source(360),
Self::TEAMFORTRESS2 => Engine::new_source(440),
Self::LEFT4DEAD => Engine::new_source(500),
Self::LEFT4DEAD2 => Engine::new_source(550),
Self::ALIENSWARM => Engine::new_source(630),
Self::CSGO => Engine::new_source(730),
Self::THESHIP => Engine::new_source(2400),
Self::GARRYSMOD => Engine::new_source(4000),
Self::AOC => Engine::new_source(17510),
Self::IMIC => Engine::new_source(17700),
Self::A2OA => Engine::new_source(33930),
Self::PROJECTZOMBOID => Engine::new_source(108_600),
Self::INSURGENCY => Engine::new_source(222_880),
Self::SD2D => Engine::new_source(251_570),
Self::RUST => Engine::new_source(252_490),
Self::CREATIVERSE => Engine::new_source(280_790),
Self::BALLISTICOVERKILL => Engine::new_source(296_300),
Self::DST => Engine::new_source(322_320),
Self::BRAINBREAD2 => Engine::new_source(346_330),
Self::CODENAMECURE => Engine::new_source(355_180),
Self::BLACKMESA => Engine::new_source(362_890),
Self::COLONYSURVIVAL => Engine::new_source(366_090),
Self::AVORION => Engine::new_source(445_220),
Self::DOI => Engine::new_source(447_820),
Self::THEFOREST => Engine::new_source(556_450),
Self::UNTURNED => Engine::new_source(304_930),
Self::ASE => Engine::new_source(346_110),
Self::BATTALION1944 => Engine::new_source(489_940),
Self::INSURGENCYSANDSTORM => Engine::new_source(581_320),
Self::ASRD => Engine::new_source(563_560),
Self::BAROTRAUMA => Engine::new_source(602960),
Self::ROR2 => Engine::new_source(632_360),
Self::OHD => Engine::new_source_with_dedicated(736_590, 950_900),
Self::ONSET => Engine::new_source(1_105_810),
Self::VRISING => Engine::new_source(1_604_030),
Self::HLL => Engine::new_source(686_810),
_ => Engine::GoldSrc(false), // CS - 10, TFC - 20, DOD - 30, CSCZ - 80, SC - 225840
}
}
}
/// Engine type.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Engine {
/// A Source game, the argument represents the possible steam app ids, if
/// its **None**, let the query find it, if its **Some**, the query
/// fails if the response id is not the first one, which is the game app
/// id, or the other one, which is the dedicated server app id.
Source(Option<(u32, Option<u32>)>),
/// A GoldSrc game, the argument indicates whether to enforce
/// requesting the obsolete A2S_INFO response or not.
GoldSrc(bool),
}
impl Engine {
pub const fn new_source(appid: u32) -> Self { Self::Source(Some((appid, None))) }
pub const fn new_source_with_dedicated(appid: u32, dedicated_appid: u32) -> Self {
Self::Source(Some((appid, Some(dedicated_appid))))
}
}
/// What data to gather, purely used only with the query function.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct GatheringSettings {
pub players: bool,
pub rules: bool,
pub check_app_id: bool,
}
impl Default for GatheringSettings {
/// Default values are true for both the players and the rules.
fn default() -> Self {
Self {
players: true,
rules: true,
check_app_id: true,
}
}
}
impl From<ExtraRequestSettings> for GatheringSettings {
fn from(value: ExtraRequestSettings) -> Self {
let default = Self::default();
Self {
players: value.gather_players.unwrap_or(default.players),
rules: value.gather_rules.unwrap_or(default.rules),
check_app_id: value.check_app_id.unwrap_or(default.check_app_id),
}
}
}
/// Generic response types that are used by many games, they are the protocol
/// ones, but without the unnecessary bits (example: the **The Ship**-only
/// fields).
pub mod game {
use super::{Server, ServerPlayer};
use crate::protocols::valve::types::get_optional_extracted_data;
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, PartialOrd)]
pub struct Player {
/// Player's name.
pub name: String,
/// Player's score.
pub score: i32,
/// How long a player has been in the server (seconds).
pub duration: f32,
}
impl Player {
pub fn from_valve_response(player: &ServerPlayer) -> Self {
Self {
name: player.name.clone(),
score: player.score,
duration: player.duration,
}
}
}
/// The query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct Response {
/// Protocol used by the server.
pub protocol: u8,
/// Name of the server.
pub name: String,
/// Map name.
pub map: String,
/// The name of the game.
pub game: String,
/// Server's app id.
pub appid: u32,
/// Number of players on the server.
pub players_online: u8,
/// Details about the server's players (not all players necessarily).
pub players_details: Vec<Player>,
/// Maximum number of players the server reports it can hold.
pub players_maximum: u8,
/// Number of bots on the server.
pub players_bots: u8,
/// Dedicated, NonDedicated or SourceTV
pub server_type: Server,
/// Indicates whether the server requires a password.
pub has_password: bool,
/// Indicated whether the server uses VAC.
pub vac_secured: bool,
/// Version of the game installed on the server.
pub version: String,
/// The server's reported connection port.
pub port: Option<u16>,
/// Server's SteamID.
pub steam_id: Option<u64>,
/// SourceTV's connection port.
pub tv_port: Option<u16>,
/// SourceTV's name.
pub tv_name: Option<String>,
/// Keywords that describe the server according to it.
pub keywords: Option<String>,
/// Server's rules.
pub rules: HashMap<String, String>,
}
impl Response {
pub fn new_from_valve_response(response: super::Response) -> Self {
let (port, steam_id, tv_port, tv_name, keywords) = get_optional_extracted_data(response.info.extra_data);
Self {
protocol: response.info.protocol_version,
name: response.info.name,
map: response.info.map,
game: response.info.game_mode,
appid: response.info.appid,
players_online: response.info.players_online,
players_details: response
.players
.unwrap_or_default()
.iter()
.map(Player::from_valve_response)
.collect(),
players_maximum: response.info.players_maximum,
players_bots: response.info.players_bots,
server_type: response.info.server_type,
has_password: response.info.has_password,
vac_secured: response.info.vac_secured,
version: response.info.game_version,
port,
steam_id,
tv_port,
tv_name,
keywords,
rules: response.rules.unwrap_or_default(),
}
}
}
}