diff --git a/protocol/src/admin.rs b/protocol/src/admin.rs index 614ffb2..69bc835 100644 --- a/protocol/src/admin.rs +++ b/protocol/src/admin.rs @@ -1,20 +1,28 @@ // SPDX-License-Identifier: GPL-3.0-only // SPDX-FileCopyrightText: 2023 Denis Drakhnia +//! Admin packets. + use crate::cursor::{Cursor, CursorMut}; use crate::types::Hide; use crate::Error; +/// Default hash length. pub const HASH_LEN: usize = 64; +/// Default hash key. pub const HASH_KEY: &str = "Half-Life"; +/// Default hash personality. pub const HASH_PERSONAL: &str = "Freeman"; +/// Admin challenge request. #[derive(Clone, Debug, PartialEq)] pub struct AdminChallenge; impl AdminChallenge { + /// Packet header. pub const HEADER: &'static [u8] = b"adminchallenge"; + /// Decode packet from `src`. pub fn decode(src: &[u8]) -> Result { if src == Self::HEADER { Ok(Self) @@ -23,21 +31,28 @@ impl AdminChallenge { } } + /// Encode packet to `buf`. pub fn encode(&self, buf: &mut [u8]) -> Result { Ok(CursorMut::new(buf).put_bytes(Self::HEADER)?.pos()) } } +/// Admin command. #[derive(Clone, Debug, PartialEq)] pub struct AdminCommand<'a> { + /// A number received in admin challenge response. pub master_challenge: u32, + /// A password hash mixed with a challenge number received in admin challenge response. pub hash: Hide<&'a [u8]>, + /// A command to execute on a master server. pub command: &'a str, } impl<'a> AdminCommand<'a> { + /// Packet header. pub const HEADER: &'static [u8] = b"admin"; + /// Creates a new `AdminCommand`. pub fn new(master_challenge: u32, hash: &'a [u8], command: &'a str) -> Self { Self { master_challenge, @@ -46,6 +61,7 @@ impl<'a> AdminCommand<'a> { } } + /// Decode packet from `src` with specified hash length. pub fn decode_with_hash_len(hash_len: usize, src: &'a [u8]) -> Result { let mut cur = Cursor::new(src); cur.expect(Self::HEADER)?; @@ -60,11 +76,13 @@ impl<'a> AdminCommand<'a> { }) } + /// Decode packet from `src`. #[inline] pub fn decode(src: &'a [u8]) -> Result { Self::decode_with_hash_len(HASH_LEN, src) } + /// Encode packet to `buf`. pub fn encode(&self, buf: &mut [u8]) -> Result { Ok(CursorMut::new(buf) .put_bytes(Self::HEADER)? @@ -75,13 +93,17 @@ impl<'a> AdminCommand<'a> { } } +/// Admin packet. #[derive(Clone, Debug, PartialEq)] pub enum Packet<'a> { + /// Admin challenge request. AdminChallenge(AdminChallenge), + /// Admin command. AdminCommand(AdminCommand<'a>), } impl<'a> Packet<'a> { + /// Decode packet from `src` with specified hash length. pub fn decode(hash_len: usize, src: &'a [u8]) -> Result { if let Ok(p) = AdminChallenge::decode(src) { return Ok(Self::AdminChallenge(p)); diff --git a/protocol/src/color.rs b/protocol/src/color.rs index 5ad5146..0a30930 100644 --- a/protocol/src/color.rs +++ b/protocol/src/color.rs @@ -1,17 +1,28 @@ // SPDX-License-Identifier: GPL-3.0-only // SPDX-FileCopyrightText: 2023 Denis Drakhnia +//! Color codes for strings. + use std::borrow::Cow; +/// Color codes `^digit`. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Color { + /// Black is coded as `^0`. Black, + /// Red is coded as `^1`. Red, + /// Green is coded as `^2`. Green, + /// Yellow is coded as `^3`. Yellow, + /// Blue is coded as `^4`. Blue, + /// Cyan is coded as `^5`. Cyan, + /// Magenta is coded as `^6`. Magenta, + /// White is coded as `^7`. White, } @@ -33,11 +44,29 @@ impl TryFrom<&str> for Color { } } +/// Test if string starts with color code. +/// +/// # Examples +/// ```rust +/// # use xash3d_protocol::color::is_color_code; +/// assert_eq!(is_color_code("hello"), false); +/// assert_eq!(is_color_code("^4blue ocean"), true); +/// ``` #[inline] pub fn is_color_code(s: &str) -> bool { matches!(s.as_bytes(), [b'^', c, ..] if c.is_ascii_digit()) } +/// Trim color codes from a start of string. +/// +/// # Examples +/// +/// ```rust +/// # use xash3d_protocol::color::trim_start_color; +/// assert_eq!(trim_start_color("hello"), ("", "hello")); +/// assert_eq!(trim_start_color("^1red apple"), ("^1", "red apple")); +/// assert_eq!(trim_start_color("^1^2^3yellow roof"), ("^3", "yellow roof")); +/// ``` #[inline] pub fn trim_start_color(s: &str) -> (&str, &str) { let mut n = 0; @@ -51,11 +80,25 @@ pub fn trim_start_color(s: &str) -> (&str, &str) { } } +/// Iterator for colored parts of a string. +/// +/// # Examples +/// +/// ```rust +/// # use xash3d_protocol::color::ColorIter; +/// let colored = "^1red flower^7 and ^2green grass"; +/// let mut iter = ColorIter::new(colored); +/// assert_eq!(iter.next(), Some(("^1", "red flower"))); +/// assert_eq!(iter.next(), Some(("^7", " and "))); +/// assert_eq!(iter.next(), Some(("^2", "green grass"))); +/// assert_eq!(iter.next(), None); +/// ``` pub struct ColorIter<'a> { inner: &'a str, } impl<'a> ColorIter<'a> { + /// Creates a new `ColorIter`. pub fn new(inner: &'a str) -> Self { Self { inner } } @@ -80,6 +123,14 @@ impl<'a> Iterator for ColorIter<'a> { } } +/// Trim color codes from a string. +/// +/// # Examples +/// +/// ```rust +/// # use xash3d_protocol::color::trim_color; +/// assert_eq!(trim_color("^1no^7 ^2colors^7"), "no colors"); +/// ``` pub fn trim_color(s: &str) -> Cow<'_, str> { let (_, s) = trim_start_color(s); if !s.chars().any(|c| c == '^') { diff --git a/protocol/src/filter.rs b/protocol/src/filter.rs index e5e2b50..ddc816f 100644 --- a/protocol/src/filter.rs +++ b/protocol/src/filter.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only // SPDX-FileCopyrightText: 2023 Denis Drakhnia -//! Server query filter +//! Server query filter. //! //! # Supported filters: //! @@ -43,6 +43,7 @@ use crate::types::Str; use crate::{Error, ServerInfo}; bitflags! { + /// Additional filter flags. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct FilterFlags: u16 { /// Servers running dedicated @@ -84,18 +85,24 @@ impl From<&ServerAdd> for FilterFlags { } } +/// Client or server version. #[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord)] pub struct Version { + /// MAJOR version. pub major: u8, + /// MINOR version. pub minor: u8, + /// PATCH version. pub patch: u8, } impl Version { + /// Creates a new `Version`. pub const fn new(major: u8, minor: u8) -> Self { Self::with_patch(major, minor, 0) } + /// Creates a new `Version` with the specified `patch` version. pub const fn with_patch(major: u8, minor: u8, patch: u8) -> Self { Self { major, @@ -155,6 +162,7 @@ impl PutKeyValue for Version { } } +/// Server filter. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct Filter<'a> { /// Servers running the specified modification (ex. cstrike) @@ -165,18 +173,22 @@ pub struct Filter<'a> { pub clver: Option, /// Protocol version pub protocol: Option, + /// A number that master must sent back to game client. pub key: Option, - + /// Additional filter flags. pub flags: FilterFlags, + /// Filter flags mask. pub flags_mask: FilterFlags, } impl Filter<'_> { + /// Insert filter flag. pub fn insert_flag(&mut self, flag: FilterFlags, value: bool) { self.flags.set(flag, value); self.flags_mask.insert(flag); } + /// Returns `true` if a server matches the filter. pub fn matches(&self, _addr: SocketAddrV4, info: &ServerInfo) -> bool { !((info.flags & self.flags_mask) != self.flags || self.gamedir.map_or(false, |s| *s != &*info.gamedir) diff --git a/protocol/src/game.rs b/protocol/src/game.rs index 7353161..91f4f92 100644 --- a/protocol/src/game.rs +++ b/protocol/src/game.rs @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only // SPDX-FileCopyrightText: 2023 Denis Drakhnia +//! Game client packets. + use std::fmt; use std::net::SocketAddrV4; @@ -9,14 +11,19 @@ use crate::filter::Filter; use crate::server::Region; use crate::Error; +/// Request a list of server addresses from master servers. #[derive(Clone, Debug, PartialEq)] pub struct QueryServers { + /// Servers must be from the `region`. pub region: Region, + /// Last received server address __(not used)__. pub last: SocketAddrV4, + /// Select only servers that match the `filter`. pub filter: T, } impl QueryServers<()> { + /// Packet header. pub const HEADER: &'static [u8] = b"1"; } @@ -24,6 +31,7 @@ impl<'a, T: 'a> QueryServers where T: TryFrom<&'a [u8], Error = Error>, { + /// Decode packet from `src`. pub fn decode(src: &'a [u8]) -> Result { let mut cur = Cursor::new(src); cur.expect(QueryServers::HEADER)?; @@ -46,6 +54,7 @@ impl<'a, T: 'a> QueryServers where for<'b> &'b T: fmt::Display, { + /// Encode packet to `buf`. pub fn encode(&self, buf: &mut [u8]) -> Result { Ok(CursorMut::new(buf) .put_bytes(QueryServers::HEADER)? @@ -58,18 +67,23 @@ where } } +/// Request an information from a game server. #[derive(Clone, Debug, PartialEq)] pub struct GetServerInfo { + /// Client protocol version. pub protocol: u8, } impl GetServerInfo { + /// Packet header. pub const HEADER: &'static [u8] = b"\xff\xff\xff\xffinfo "; + /// Creates a new `GetServerInfo`. pub fn new(protocol: u8) -> Self { Self { protocol } } + /// Decode packet from `src`. pub fn decode(src: &[u8]) -> Result { let mut cur = Cursor::new(src); cur.expect(Self::HEADER)?; @@ -80,6 +94,7 @@ impl GetServerInfo { Ok(Self { protocol }) } + /// Encode packet to `buf`. pub fn encode(&self, buf: &mut [u8]) -> Result { Ok(CursorMut::new(buf) .put_bytes(Self::HEADER)? @@ -88,13 +103,17 @@ impl GetServerInfo { } } +/// Game client packets. #[derive(Clone, Debug, PartialEq)] pub enum Packet<'a> { + /// Request a list of server addresses from master servers. QueryServers(QueryServers>), + /// Request an information from a game server. GetServerInfo(GetServerInfo), } impl<'a> Packet<'a> { + /// Decode packet from `src`. pub fn decode(src: &'a [u8]) -> Result { if let Ok(p) = QueryServers::decode(src) { return Ok(Self::QueryServers(p)); diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs index 83402f3..be03dc3 100644 --- a/protocol/src/lib.rs +++ b/protocol/src/lib.rs @@ -1,6 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-only // SPDX-FileCopyrightText: 2023 Denis Drakhnia +#![deny(missing_docs)] + +//! Xash3D protocol between clients, servers and masters. + mod cursor; mod server_info; @@ -18,18 +22,24 @@ use thiserror::Error; use crate::filter::Version; +/// Current protocol version. pub const PROTOCOL_VERSION: u8 = 49; - +/// Current client version. pub const CLIENT_VERSION: Version = Version::new(0, 20); +/// The error type for decoding and encoding packets. #[derive(Error, Debug, PartialEq, Eq)] pub enum Error { + /// Failed to decode a packet. #[error("Invalid packet")] InvalidPacket, + /// Invalid string in a packet. #[error("Invalid UTF-8 string")] InvalidString, + /// Buffer size is no enougth to decode or encode a packet. #[error("Unexpected end of buffer")] UnexpectedEnd, + /// Server protocol version is not supported. #[error("Invalid protocol version")] InvalidProtocolVersion, } diff --git a/protocol/src/master.rs b/protocol/src/master.rs index 7a2ebe1..41b019c 100644 --- a/protocol/src/master.rs +++ b/protocol/src/master.rs @@ -1,20 +1,27 @@ // SPDX-License-Identifier: GPL-3.0-only // SPDX-FileCopyrightText: 2023 Denis Drakhnia +//! Master server packets. + use std::net::{Ipv4Addr, SocketAddrV4}; use super::cursor::{Cursor, CursorMut}; use super::Error; +/// Master server challenge response packet. #[derive(Clone, Debug, PartialEq)] pub struct ChallengeResponse { + /// A number that a game server must send back. pub master_challenge: u32, + /// A number that a master server received in challenge packet. pub server_challenge: Option, } impl ChallengeResponse { + /// Packet header. pub const HEADER: &'static [u8] = b"\xff\xff\xff\xffs\n"; + /// Creates a new `ChallengeResponse`. pub fn new(master_challenge: u32, server_challenge: Option) -> Self { Self { master_challenge, @@ -22,6 +29,7 @@ impl ChallengeResponse { } } + /// Decode packet from `src`. pub fn decode(src: &[u8]) -> Result { let mut cur = Cursor::new(src); cur.expect(Self::HEADER)?; @@ -38,6 +46,7 @@ impl ChallengeResponse { }) } + /// Encode packet to `buf`. pub fn encode(&self, buf: &mut [u8; N]) -> Result { let mut cur = CursorMut::new(buf); cur.put_bytes(Self::HEADER)?; @@ -49,17 +58,21 @@ impl ChallengeResponse { } } +/// Game server addresses list. #[derive(Clone, Debug, PartialEq)] pub struct QueryServersResponse { inner: I, + /// A challenge number received in a filter string. pub key: Option, } impl QueryServersResponse<()> { + /// Packet header. pub const HEADER: &'static [u8] = b"\xff\xff\xff\xfff\n"; } impl<'a> QueryServersResponse<&'a [u8]> { + /// Decode packet from `src`. pub fn decode(src: &'a [u8]) -> Result { let mut cur = Cursor::new(src); cur.expect(QueryServersResponse::HEADER)?; @@ -83,6 +96,7 @@ impl<'a> QueryServersResponse<&'a [u8]> { Ok(Self { inner, key }) } + /// Iterator over game server addresses. pub fn iter(&self) -> impl 'a + Iterator { let mut cur = Cursor::new(self.inner); (0..self.inner.len() / 6).map(move |_| { @@ -92,6 +106,7 @@ impl<'a> QueryServersResponse<&'a [u8]> { }) } + /// Returns `true` if game server addresses list is empty. pub fn is_empty(&self) -> bool { self.inner.is_empty() } @@ -101,10 +116,17 @@ impl QueryServersResponse where I: Iterator, { + /// Creates a new `QueryServersResponse`. pub fn new(iter: I, key: Option) -> Self { Self { inner: iter, key } } + /// Encode packet to `buf`. + /// + /// If `buf` has not enougth size to hold all addresses the method must be called + /// multiple times until the end flag equals `true`. + /// + /// Returns how many bytes was written in `buf` and the end flag. pub fn encode(&mut self, buf: &mut [u8]) -> Result<(usize, bool), Error> { let mut cur = CursorMut::new(buf); cur.put_bytes(QueryServersResponse::HEADER)?; @@ -129,18 +151,23 @@ where } } +/// Announce a game client to game server behind NAT. #[derive(Clone, Debug, PartialEq)] pub struct ClientAnnounce { + /// Address of the client. pub addr: SocketAddrV4, } impl ClientAnnounce { + /// Packet header. pub const HEADER: &'static [u8] = b"\xff\xff\xff\xffc "; + /// Creates a new `ClientAnnounce`. pub fn new(addr: SocketAddrV4) -> Self { Self { addr } } + /// Decode packet from `src`. pub fn decode(src: &[u8]) -> Result { let mut cur = Cursor::new(src); cur.expect(Self::HEADER)?; @@ -152,6 +179,7 @@ impl ClientAnnounce { Ok(Self { addr }) } + /// Encode packet to `buf`. pub fn encode(&self, buf: &mut [u8]) -> Result { Ok(CursorMut::new(buf) .put_bytes(Self::HEADER)? @@ -160,15 +188,20 @@ impl ClientAnnounce { } } +/// Admin challenge response. #[derive(Clone, Debug, PartialEq)] pub struct AdminChallengeResponse { + /// A number that admin must sent back to a master server. pub master_challenge: u32, + /// A number with which to mix a password hash. pub hash_challenge: u32, } impl AdminChallengeResponse { + /// Packet header. pub const HEADER: &'static [u8] = b"\xff\xff\xff\xffadminchallenge"; + /// Creates a new `AdminChallengeResponse`. pub fn new(master_challenge: u32, hash_challenge: u32) -> Self { Self { master_challenge, @@ -176,6 +209,7 @@ impl AdminChallengeResponse { } } + /// Decode packet from `src`. pub fn decode(src: &[u8]) -> Result { let mut cur = Cursor::new(src); cur.expect(Self::HEADER)?; @@ -188,6 +222,7 @@ impl AdminChallengeResponse { }) } + /// Encode packet to `buf`. pub fn encode(&self, buf: &mut [u8]) -> Result { Ok(CursorMut::new(buf) .put_bytes(Self::HEADER)? @@ -197,14 +232,21 @@ impl AdminChallengeResponse { } } +/// Master server packet. #[derive(Clone, Debug, PartialEq)] pub enum Packet<'a> { + /// Master server challenge response packet. ChallengeResponse(ChallengeResponse), + /// Game server addresses list. QueryServersResponse(QueryServersResponse<&'a [u8]>), + /// Announce a game client to game server behind NAT. + ClientAnnounce(ClientAnnounce), + /// Admin challenge response. AdminChallengeResponse(AdminChallengeResponse), } impl<'a> Packet<'a> { + /// Decode packet from `src`. pub fn decode(src: &'a [u8]) -> Result { if let Ok(p) = ChallengeResponse::decode(src) { return Ok(Self::ChallengeResponse(p)); @@ -214,6 +256,10 @@ impl<'a> Packet<'a> { return Ok(Self::QueryServersResponse(p)); } + if let Ok(p) = ClientAnnounce::decode(src) { + return Ok(Self::ClientAnnounce(p)); + } + if let Ok(p) = AdminChallengeResponse::decode(src) { return Ok(Self::AdminChallengeResponse(p)); } diff --git a/protocol/src/server.rs b/protocol/src/server.rs index b2c43b6..ce42496 100644 --- a/protocol/src/server.rs +++ b/protocol/src/server.rs @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only // SPDX-FileCopyrightText: 2023 Denis Drakhnia +//! Game server packets. + use std::fmt; use bitflags::bitflags; @@ -11,18 +13,23 @@ use super::filter::Version; use super::types::Str; use super::Error; +/// Sended to a master server before `ServerAdd` packet. #[derive(Clone, Debug, PartialEq)] pub struct Challenge { + /// A number that the server must return in response. pub server_challenge: Option, } impl Challenge { + /// Packet header. pub const HEADER: &'static [u8] = b"q\xff"; + /// Creates a new `Challenge`. pub fn new(server_challenge: Option) -> Self { Self { server_challenge } } + /// Decode packet from `src`. pub fn decode(src: &[u8]) -> Result { let mut cur = Cursor::new(src); cur.expect(Self::HEADER)?; @@ -35,6 +42,7 @@ impl Challenge { Ok(Self { server_challenge }) } + /// Encode packet to `buf`. pub fn encode(&self, buf: &mut [u8; N]) -> Result { let mut cur = CursorMut::new(buf); cur.put_bytes(Self::HEADER)?; @@ -45,12 +53,17 @@ impl Challenge { } } +/// The operating system on which the game server runs. #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[repr(u8)] pub enum Os { + /// GNU/Linux. Linux, + /// Microsoft Windows Windows, + /// Apple macOS, OS X, Mac OS X Mac, + /// Unknown Unknown, } @@ -105,12 +118,17 @@ impl fmt::Display for Os { } } +/// Game server type. #[derive(Copy, Clone, Debug, PartialEq)] #[repr(u8)] pub enum ServerType { + /// Dedicated server. Dedicated, + /// Game client. Local, + /// Spectator proxy. Proxy, + /// Unknown. Unknown, } @@ -168,17 +186,27 @@ impl fmt::Display for ServerType { } } +/// The region of the world in which the server is located. #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[repr(u8)] pub enum Region { + /// US East coast. USEastCoast = 0x00, + /// US West coast. USWestCoast = 0x01, + /// South America. SouthAmerica = 0x02, + /// Europe. Europe = 0x03, + /// Asia. Asia = 0x04, + /// Australia. Australia = 0x05, + /// Middle East. MiddleEast = 0x06, + /// Africa. Africa = 0x07, + /// Rest of the world. RestOfTheWorld = 0xff, } @@ -214,33 +242,59 @@ impl GetKeyValue<'_> for Region { } bitflags! { + /// Additional server flags. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct ServerFlags: u8 { + /// Server has bots. const BOTS = 1 << 0; + /// Server is behind a password. const PASSWORD = 1 << 1; + /// Server using anti-cheat. const SECURE = 1 << 2; + /// Server is LAN. const LAN = 1 << 3; + /// Server behind NAT. const NAT = 1 << 4; } } +/// Add/update game server information on the master server. #[derive(Clone, Debug, PartialEq, Default)] pub struct ServerAdd { + /// Server is running the specified modification. + /// + /// ## Examples: + /// + /// * valve - Half-Life + /// * cstrike - Counter-Strike 1.6 + /// * portal - Portal + /// * dod - Day of Defeat + /// * left4dead - Left 4 Dead pub gamedir: T, + /// Server is running `map`. pub map: T, + /// Server version. pub version: Version, - pub product: T, + /// Master server challenge number. pub challenge: u32, + /// Server type. pub server_type: ServerType, + /// Server is running on an operating system. pub os: Os, + /// Server is located in a `region`. pub region: Region, + /// Server protocol version. pub protocol: u8, + /// Current number of players on the server. pub players: u8, + /// Maximum number of players on the server. pub max: u8, + /// See `ServerFalgs`. pub flags: ServerFlags, } impl ServerAdd<()> { + /// Packet header. pub const HEADER: &'static [u8] = b"0\n"; } @@ -248,6 +302,7 @@ impl<'a, T> ServerAdd where T: 'a + Default + GetKeyValue<'a>, { + /// Decode packet from `src`. pub fn decode(src: &'a [u8]) -> Result { let mut cur = Cursor::new(src); cur.expect(ServerAdd::HEADER)?; @@ -280,7 +335,6 @@ where .unwrap_or_default() } b"region" => ret.region = cur.get_key_value()?, - b"product" => ret.product = cur.get_key_value()?, b"bots" => ret.flags.set(ServerFlags::BOTS, cur.get_key_value()?), b"password" => ret.flags.set(ServerFlags::PASSWORD, cur.get_key_value()?), b"secure" => ret.flags.set(ServerFlags::SECURE, cur.get_key_value()?), @@ -308,6 +362,7 @@ impl ServerAdd where T: PutKeyValue + Clone, { + /// Encode packet to `buf`. pub fn encode(&self, buf: &mut [u8]) -> Result { Ok(CursorMut::new(buf) .put_bytes(ServerAdd::HEADER)? @@ -321,7 +376,6 @@ where .put_key("os", self.os)? .put_key("version", self.version)? .put_key("region", self.region as u8)? - .put_key("product", self.product.clone())? .put_key("bots", self.flags.contains(ServerFlags::BOTS))? .put_key("password", self.flags.contains(ServerFlags::PASSWORD))? .put_key("secure", self.flags.contains(ServerFlags::SECURE))? @@ -331,12 +385,15 @@ where } } +/// Remove the game server from a list. #[derive(Clone, Debug, PartialEq)] pub struct ServerRemove; impl ServerRemove { + /// Packet header. pub const HEADER: &'static [u8] = b"b\n"; + /// Decode packet from `src`. pub fn decode(src: &[u8]) -> Result { let mut cur = Cursor::new(src); cur.expect(Self::HEADER)?; @@ -344,26 +401,47 @@ impl ServerRemove { Ok(Self) } + /// Encode packet to `buf`. pub fn encode(&self, buf: &mut [u8; N]) -> Result { Ok(CursorMut::new(buf).put_bytes(Self::HEADER)?.pos()) } } +/// Game server information to game clients. #[derive(Clone, Debug, PartialEq, Default)] pub struct GetServerInfoResponse { + /// Server is running the specified modification. + /// + /// ## Examples: + /// + /// * valve - Half-Life + /// * cstrike - Counter-Strike 1.6 + /// * portal - Portal + /// * dod - Day of Defeat + /// * left4dead - Left 4 Dead pub gamedir: T, + /// Server is running `map`. pub map: T, + /// Server title. pub host: T, + /// Server protocol version. pub protocol: u8, + /// Current number of players on the server. pub numcl: u8, + /// Maximum number of players on the server. pub maxcl: u8, + /// Server is running a deathmatch game mode. pub dm: bool, + /// Players are grouped into teams. pub team: bool, + /// Server is running in a co-op game mode. pub coop: bool, + /// Server is behind a password. pub password: bool, } impl GetServerInfoResponse<()> { + /// Packet header. pub const HEADER: &'static [u8] = b"\xff\xff\xff\xffinfo\n"; } @@ -371,6 +449,7 @@ impl<'a, T> GetServerInfoResponse where T: 'a + Default + GetKeyValue<'a>, { + /// Decode packet from `src`. pub fn decode(src: &'a [u8]) -> Result { let mut cur = Cursor::new(src); cur.expect(GetServerInfoResponse::HEADER)?; @@ -421,6 +500,7 @@ where } impl<'a> GetServerInfoResponse<&'a str> { + /// Encode packet to `buf`. pub fn encode(&self, buf: &mut [u8]) -> Result { Ok(CursorMut::new(buf) .put_bytes(GetServerInfoResponse::HEADER)? @@ -438,15 +518,21 @@ impl<'a> GetServerInfoResponse<&'a str> { } } +/// Game server packet. #[derive(Clone, Debug, PartialEq)] pub enum Packet<'a> { + /// Sended to a master server before `ServerAdd` packet. Challenge(Challenge), + /// Add/update game server information on the master server. ServerAdd(ServerAdd>), + /// Remove the game server from a list. ServerRemove, + /// Game server information to game clients. GetServerInfoResponse(GetServerInfoResponse>), } impl<'a> Packet<'a> { + /// Decode packet from `src`. pub fn decode(src: &'a [u8]) -> Result { if let Ok(p) = Challenge::decode(src) { return Ok(Self::Challenge(p)); @@ -497,7 +583,6 @@ mod tests { gamedir: "valve", map: "crossfire", version: Version::new(0, 20), - product: "foobar", challenge: 0x12345678, server_type: ServerType::Dedicated, os: Os::Linux, diff --git a/protocol/src/server_info.rs b/protocol/src/server_info.rs index ec785f1..0d0e85d 100644 --- a/protocol/src/server_info.rs +++ b/protocol/src/server_info.rs @@ -5,17 +5,25 @@ use super::filter::{FilterFlags, Version}; use super::server::{Region, ServerAdd}; use super::types::Str; +/// Game server information. #[derive(Clone, Debug)] pub struct ServerInfo { + /// Server version. pub version: Version, + /// Server protocol version. pub protocol: u8, + /// Server midification. pub gamedir: Box<[u8]>, + /// Server map. pub map: Box<[u8]>, + /// Server additional filter flags. pub flags: FilterFlags, + /// Server region. pub region: Region, } impl ServerInfo { + /// Creates a new `ServerInfo`. pub fn new(info: &ServerAdd>) -> Self { Self { version: info.version, diff --git a/protocol/src/types.rs b/protocol/src/types.rs index d9a9eb9..54331da 100644 --- a/protocol/src/types.rs +++ b/protocol/src/types.rs @@ -1,10 +1,20 @@ // SPDX-License-Identifier: GPL-3.0-only // SPDX-FileCopyrightText: 2023 Denis Drakhnia +//! Wrappers for byte slices with pretty-printers. + use std::fmt; use std::ops::Deref; -/// Wrapper for slice of bytes with printing the bytes as a string +/// Wrapper for slice of bytes with printing the bytes as a string. +/// +/// # Examples +/// +/// ```rust +/// # use xash3d_protocol::types::Str; +/// let s = format!("{}", Str(b"\xff\talex\n")); +/// assert_eq!(s, "\\xff\\talex\\n"); +/// ``` #[derive(Copy, Clone, PartialEq, Eq, Default)] pub struct Str(pub T); @@ -51,7 +61,15 @@ impl Deref for Str { } } -/// Wrapper for slice of bytes without printing +/// Wrapper for slice of bytes without printing. +/// +/// # Examples +/// +/// ```rust +/// # use xash3d_protocol::types::Hide; +/// let s = format!("{}", Hide([1, 2, 3, 4])); +/// assert_eq!(s, ""); +/// ``` #[derive(Copy, Clone, PartialEq, Eq, Default)] pub struct Hide(pub T);