mirror of
https://github.com/tribufu/rust-gamedig
synced 2026-06-01 09:42:41 +00:00
refator: copy cli into mono
This commit is contained in:
parent
66ae3c296e
commit
80f6b87991
63 changed files with 244 additions and 34 deletions
19
crates/lib/src/protocols/gamespy/common.rs
Normal file
19
crates/lib/src/protocols/gamespy/common.rs
Normal 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)
|
||||
}
|
||||
86
crates/lib/src/protocols/gamespy/mod.rs
Normal file
86
crates/lib/src/protocols/gamespy/mod.rs
Normal 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;
|
||||
3
crates/lib/src/protocols/gamespy/protocols/mod.rs
Normal file
3
crates/lib/src/protocols/gamespy/protocols/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod one;
|
||||
pub mod three;
|
||||
pub mod two;
|
||||
5
crates/lib/src/protocols/gamespy/protocols/one/mod.rs
Normal file
5
crates/lib/src/protocols/gamespy/protocols/one/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod protocol;
|
||||
pub mod types;
|
||||
|
||||
pub use protocol::*;
|
||||
pub use types::*;
|
||||
244
crates/lib/src/protocols/gamespy/protocols/one/protocol.rs
Normal file
244
crates/lib/src/protocols/gamespy/protocols/one/protocol.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
73
crates/lib/src/protocols/gamespy/protocols/one/types.rs
Normal file
73
crates/lib/src/protocols/gamespy/protocols/one/types.rs
Normal 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 player’s details.
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct Player {
|
||||
pub name: String,
|
||||
pub 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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
5
crates/lib/src/protocols/gamespy/protocols/three/mod.rs
Normal file
5
crates/lib/src/protocols/gamespy/protocols/three/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod protocol;
|
||||
pub mod types;
|
||||
|
||||
pub use protocol::*;
|
||||
pub use types::*;
|
||||
412
crates/lib/src/protocols/gamespy/protocols/three/protocol.rs
Normal file
412
crates/lib/src/protocols/gamespy/protocols/three/protocol.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
75
crates/lib/src/protocols/gamespy/protocols/three/types.rs
Normal file
75
crates/lib/src/protocols/gamespy/protocols/three/types.rs
Normal 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 player’s details.
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct Player {
|
||||
pub name: String,
|
||||
pub score: i32,
|
||||
pub ping: u16,
|
||||
pub team: u8,
|
||||
pub deaths: u32,
|
||||
pub skill: u32,
|
||||
}
|
||||
|
||||
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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
5
crates/lib/src/protocols/gamespy/protocols/two/mod.rs
Normal file
5
crates/lib/src/protocols/gamespy/protocols/two/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod protocol;
|
||||
pub mod types;
|
||||
|
||||
pub use protocol::*;
|
||||
pub use types::*;
|
||||
211
crates/lib/src/protocols/gamespy/protocols/two/protocol.rs
Normal file
211
crates/lib/src/protocols/gamespy/protocols/two/protocol.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
64
crates/lib/src/protocols/gamespy/protocols/two/types.rs
Normal file
64
crates/lib/src/protocols/gamespy/protocols/two/types.rs
Normal 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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
8
crates/lib/src/protocols/minecraft/mod.rs
Normal file
8
crates/lib/src/protocols/minecraft/mod.rs
Normal 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::*;
|
||||
114
crates/lib/src/protocols/minecraft/protocol/bedrock.rs
Normal file
114
crates/lib/src/protocols/minecraft/protocol/bedrock.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
177
crates/lib/src/protocols/minecraft/protocol/java.rs
Normal file
177
crates/lib/src/protocols/minecraft/protocol/java.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
82
crates/lib/src/protocols/minecraft/protocol/legacy_v1_3.rs
Normal file
82
crates/lib/src/protocols/minecraft/protocol/legacy_v1_3.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
85
crates/lib/src/protocols/minecraft/protocol/legacy_v1_5.rs
Normal file
85
crates/lib/src/protocols/minecraft/protocol/legacy_v1_5.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
119
crates/lib/src/protocols/minecraft/protocol/legacy_v1_6.rs
Normal file
119
crates/lib/src/protocols/minecraft/protocol/legacy_v1_6.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
91
crates/lib/src/protocols/minecraft/protocol/mod.rs
Normal file
91
crates/lib/src/protocols/minecraft/protocol/mod.rs
Normal 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)
|
||||
}
|
||||
295
crates/lib/src/protocols/minecraft/types.rs
Normal file
295
crates/lib/src/protocols/minecraft/types.rs
Normal 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)
|
||||
}
|
||||
18
crates/lib/src/protocols/mod.rs
Normal file
18
crates/lib/src/protocols/mod.rs
Normal 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};
|
||||
146
crates/lib/src/protocols/quake/client.rs
Normal file
146
crates/lib/src/protocols/quake/client.rs
Normal 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],
|
||||
}
|
||||
}
|
||||
74
crates/lib/src/protocols/quake/mod.rs
Normal file
74
crates/lib/src/protocols/quake/mod.rs
Normal 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;
|
||||
87
crates/lib/src/protocols/quake/one.rs
Normal file
87
crates/lib/src/protocols/quake/one.rs
Normal 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)
|
||||
}
|
||||
24
crates/lib/src/protocols/quake/three.rs
Normal file
24
crates/lib/src/protocols/quake/three.rs
Normal 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)
|
||||
}
|
||||
67
crates/lib/src/protocols/quake/two.rs
Normal file
67
crates/lib/src/protocols/quake/two.rs
Normal 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)
|
||||
}
|
||||
58
crates/lib/src/protocols/quake/types.rs
Normal file
58
crates/lib/src/protocols/quake/types.rs
Normal 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>),
|
||||
}
|
||||
365
crates/lib/src/protocols/types.rs
Normal file
365
crates/lib/src/protocols/types.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
60
crates/lib/src/protocols/valve/mod.rs
Normal file
60
crates/lib/src/protocols/valve/mod.rs
Normal 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;
|
||||
483
crates/lib/src/protocols/valve/protocol.rs
Normal file
483
crates/lib/src/protocols/valve/protocol.rs
Normal 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)?),
|
||||
},
|
||||
})
|
||||
}
|
||||
556
crates/lib/src/protocols/valve/types.rs
Normal file
556
crates/lib/src/protocols/valve/types.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue