[Games] Programmatic games by storing information as data (#45)

* Define games as structs

* Create table of response types

* Ensure serde is always included

* Remove server_ prefix in GenericResponse

* Make players online/max non-optional in generic response

* Use already existing minecraft server enum

* Implement ExtraResponses to prevent cloning when creating generic

* Add game definitions

* Add doc comments to generic types

* Include players in gamespy extra responses

* Add custom response types for TheShip and FFOW

* Cargo format differing files

* Final cleanup
This commit is contained in:
Tom 2023-06-13 18:49:58 +00:00 committed by GitHub
parent 26ad1f5d19
commit d853189e06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 806 additions and 102 deletions

View file

@ -1,5 +1,22 @@
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
mod common;
/// The implementations.
pub mod protocols;
pub use protocols::*;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub enum GameSpyVersion {
One,
Three,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub enum VersionedExtraResponse {
One(protocols::one::ExtraResponse),
Three(protocols::three::ExtraResponse),
}

View file

@ -3,6 +3,9 @@ use std::collections::HashMap;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::protocols::gamespy::VersionedExtraResponse;
use crate::protocols::{types::SpecificResponse, GenericResponse};
/// A players details.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
@ -39,3 +42,41 @@ pub struct Response {
pub tournament: bool,
pub unused_entries: HashMap<String, String>,
}
/// Non-generic query response
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtraResponse {
pub map_title: Option<String>,
pub admin_contact: Option<String>,
pub admin_name: Option<String>,
pub players_minimum: Option<u8>,
pub tournament: bool,
pub unused_entries: HashMap<String, String>,
pub players: Vec<Player>,
}
impl From<Response> for GenericResponse {
fn from(r: Response) -> Self {
Self {
name: Some(r.name),
description: None,
game: Some(r.game_type),
game_version: Some(r.game_version),
map: Some(r.map),
players_maximum: r.players_maximum.try_into().unwrap(), // FIXME: usize to u64 may fail
players_online: r.players_online.try_into().unwrap(),
players_bots: None,
has_password: Some(r.has_password),
inner: SpecificResponse::Gamespy(VersionedExtraResponse::One(ExtraResponse {
map_title: r.map_title,
admin_contact: r.admin_contact,
admin_name: r.admin_name,
players_minimum: r.players_minimum,
tournament: r.tournament,
unused_entries: r.unused_entries,
players: r.players,
})),
}
}
}

View file

@ -1,3 +1,5 @@
use crate::protocols::gamespy::VersionedExtraResponse;
use crate::protocols::{types::SpecificResponse, GenericResponse};
use std::collections::HashMap;
#[cfg(feature = "serde")]
@ -40,3 +42,37 @@ pub struct Response {
pub tournament: bool,
pub unused_entries: HashMap<String, String>,
}
/// Non-generic query response
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtraResponse {
pub players_minimum: Option<u8>,
pub teams: Vec<Team>,
pub tournament: bool,
pub unused_entries: HashMap<String, String>,
pub players: Vec<Player>,
}
impl From<Response> for GenericResponse {
fn from(r: Response) -> Self {
Self {
name: Some(r.name),
description: None,
game: Some(r.game_type),
game_version: Some(r.game_version),
map: Some(r.map),
players_maximum: r.players_maximum.try_into().unwrap(), // FIXME: usize to u64 may fail
players_online: r.players_online.try_into().unwrap(),
players_bots: None,
has_password: Some(r.has_password),
inner: SpecificResponse::Gamespy(VersionedExtraResponse::Three(ExtraResponse {
players_minimum: r.players_minimum,
teams: r.teams,
tournament: r.tournament,
unused_entries: r.unused_entries,
players: r.players,
})),
}
}
}

View file

@ -4,6 +4,7 @@
use crate::{
bufferer::Bufferer,
protocols::{types::SpecificResponse, GenericResponse},
GDError::{PacketBad, UnknownEnumCast},
GDResult,
};
@ -43,6 +44,13 @@ pub struct Player {
pub id: String,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum VersionedExtraResponse {
Bedrock(BedrockExtraResponse),
Java(JavaExtraResponse),
}
/// A Java query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
@ -70,6 +78,48 @@ pub struct JavaResponse {
pub server_type: Server,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct JavaExtraResponse {
/// Version protocol, example: 760 (for 1.19.2). Note that for versions
/// below 1.6 this field is always -1.
pub version_protocol: i32,
/// Some online players (can be missing).
pub players_sample: Option<Vec<Player>>,
/// 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,
}
impl From<JavaResponse> for GenericResponse {
fn from(r: JavaResponse) -> Self {
Self {
name: None,
description: Some(r.description),
game: Some(String::from("Minecraft")),
game_version: Some(r.version_name),
map: None,
players_maximum: r.players_maximum.into(),
players_online: r.players_online.into(),
players_bots: None,
has_password: None,
inner: SpecificResponse::Minecraft(VersionedExtraResponse::Java(JavaExtraResponse {
version_protocol: r.version_protocol,
players_sample: r.players_sample,
favicon: r.favicon,
previews_chat: r.previews_chat,
enforces_secure_chat: r.enforces_secure_chat,
server_type: r.server_type,
})),
}
}
}
/// A Bedrock Edition query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
@ -96,6 +146,44 @@ pub struct BedrockResponse {
pub server_type: Server,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct BedrockExtraResponse {
/// Server's edition.
pub edition: String,
/// Version protocol, example: 760 (for 1.19.2).
pub version_protocol: String,
/// Server id.
pub id: Option<String>,
/// Current game mode.
pub game_mode: Option<GameMode>,
/// Tells the server type.
pub server_type: Server,
}
impl From<BedrockResponse> for GenericResponse {
fn from(r: BedrockResponse) -> Self {
Self {
name: Some(r.name),
description: None,
game: None,
game_version: Some(r.version_name),
map: r.map,
players_maximum: r.players_maximum.into(),
players_online: r.players_online.into(),
players_bots: None,
has_password: None,
inner: SpecificResponse::Minecraft(VersionedExtraResponse::Bedrock(BedrockExtraResponse {
edition: r.edition,
version_protocol: r.version_protocol,
id: r.id,
game_mode: r.game_mode,
server_type: r.server_type,
})),
}
}
}
impl JavaResponse {
pub fn from_bedrock_response(response: BedrockResponse) -> Self {
Self {

View file

@ -14,3 +14,5 @@ pub mod quake;
pub mod types;
/// Reference: [Server Query](https://developer.valvesoftware.com/wiki/Server_queries)
pub mod valve;
pub use types::{GenericResponse, Protocol};

View file

@ -1,3 +1,6 @@
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub mod one;
pub mod three;
pub mod two;
@ -7,3 +10,11 @@ pub mod types;
pub use types::*;
mod client;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub enum QuakeVersion {
One,
Two,
Three,
}

View file

@ -2,6 +2,8 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::protocols::{types::SpecificResponse, GenericResponse};
/// General server information's.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
@ -21,3 +23,30 @@ pub struct Response<P> {
/// Other server entries that weren't used.
pub unused_entries: HashMap<String, String>,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtraResponse {
/// Other server entries that weren't used.
pub unused_entries: HashMap<String, String>,
}
impl<T> From<Response<T>> for GenericResponse {
fn from(r: Response<T>) -> Self {
Self {
name: Some(r.name),
description: None,
game: None,
game_version: Some(r.version),
map: Some(r.map),
players_maximum: r.players_maximum.into(),
players_online: r.players_online.into(),
players_bots: None,
has_password: None,
inner: SpecificResponse::Quake(ExtraResponse {
// TODO: Players
unused_entries: r.unused_entries,
}),
}
}
}

View file

@ -1,98 +1,154 @@
use crate::{GDError::InvalidInput, GDResult};
use std::time::Duration;
/// Timeout settings for socket operations
#[derive(Clone, Debug)]
pub struct TimeoutSettings {
read: Option<Duration>,
write: Option<Duration>,
}
impl TimeoutSettings {
/// Construct new settings, passing None will block indefinitely. Passing
/// zero Duration throws GDError::[InvalidInput](InvalidInput).
pub fn new(read: Option<Duration>, write: Option<Duration>) -> GDResult<Self> {
if let Some(read_duration) = read {
if read_duration == Duration::new(0, 0) {
return Err(InvalidInput);
}
}
if let Some(write_duration) = write {
if write_duration == Duration::new(0, 0) {
return Err(InvalidInput);
}
}
Ok(Self { read, write })
}
/// Get the read timeout.
pub fn get_read(&self) -> Option<Duration> { self.read }
/// Get the write timeout.
pub fn get_write(&self) -> Option<Duration> { self.write }
}
impl Default for TimeoutSettings {
/// Default values are 4 seconds for both read and write.
fn default() -> Self {
Self {
read: Some(Duration::from_secs(4)),
write: Some(Duration::from_secs(4)),
}
}
}
#[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))?;
// 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));
// Verify that the function returned an error and that the error type is
// InvalidInput
assert!(result.is_err());
assert_eq!(result.unwrap_err(), InvalidInput);
}
// 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)));
}
}
use crate::protocols::{gamespy, minecraft, quake, valve};
use crate::{GDError::InvalidInput, GDResult};
use std::time::Duration;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// Enumeration of all valid protocol types
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub enum Protocol {
Gamespy(gamespy::GameSpyVersion),
Minecraft(Option<minecraft::types::Server>),
Quake(quake::QuakeVersion),
Valve(valve::SteamApp),
TheShip,
FFOW,
}
/// A generic version of a response
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct GenericResponse {
/// The name of the server
pub name: Option<String>,
/// Description of the server
pub description: Option<String>,
/// Name of the current game or game mode
pub game: Option<String>,
/// Version of the game being run on the server
pub game_version: Option<String>,
/// The current map name
pub map: Option<String>,
/// Maximum number of players allowed to connect
pub players_maximum: u64,
/// Number of players currently connected
pub players_online: u64,
/// Number of bots currently connected
pub players_bots: Option<u64>,
/// Whether the server requires a password to join
pub has_password: Option<bool>,
/// Data specific to non-generic responses
pub inner: SpecificResponse,
}
/// A specific response containing extra data that isn't generic
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub enum SpecificResponse {
Gamespy(gamespy::VersionedExtraResponse),
Minecraft(minecraft::VersionedExtraResponse),
Quake(quake::ExtraResponse),
Valve(valve::ExtraResponse),
#[cfg(not(feature = "no_games"))]
TheShip(crate::games::ts::ExtraResponse),
#[cfg(not(feature = "no_games"))]
FFOW(crate::games::ffow::ExtraResponse),
}
/// Timeout settings for socket operations
#[derive(Clone, Debug)]
pub struct TimeoutSettings {
read: Option<Duration>,
write: Option<Duration>,
}
impl TimeoutSettings {
/// Construct new settings, passing None will block indefinitely. Passing
/// zero Duration throws GDError::[InvalidInput](InvalidInput).
pub fn new(read: Option<Duration>, write: Option<Duration>) -> GDResult<Self> {
if let Some(read_duration) = read {
if read_duration == Duration::new(0, 0) {
return Err(InvalidInput);
}
}
if let Some(write_duration) = write {
if write_duration == Duration::new(0, 0) {
return Err(InvalidInput);
}
}
Ok(Self { read, write })
}
/// Get the read timeout.
pub fn get_read(&self) -> Option<Duration> { self.read }
/// Get the write timeout.
pub fn get_write(&self) -> Option<Duration> { self.write }
}
impl Default for TimeoutSettings {
/// Default values are 4 seconds for both read and write.
fn default() -> Self {
Self {
read: Some(Duration::from_secs(4)),
write: Some(Duration::from_secs(4)),
}
}
}
#[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))?;
// 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));
// Verify that the function returned an error and that the error type is
// InvalidInput
assert!(result.is_err());
assert_eq!(result.unwrap_err(), InvalidInput);
}
// 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)));
}
}

View file

@ -1,8 +1,11 @@
use std::collections::HashMap;
use crate::bufferer::Bufferer;
use crate::GDError::UnknownEnumCast;
use crate::GDResult;
use crate::{
bufferer::Bufferer,
protocols::{types::SpecificResponse, GenericResponse},
};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
@ -55,6 +58,63 @@ pub struct Response {
pub rules: Option<HashMap<String, String>>,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct ExtraResponse {
pub players: Option<Vec<ServerPlayer>>,
pub rules: Option<HashMap<String, String>>,
/// Protocol used by the server.
pub protocol: u8,
/// Name of the folder containing the game files.
pub folder: String,
/// [Steam Application ID](https://developer.valvesoftware.com/wiki/Steam_Application_ID) of game.
pub appid: u32,
/// Dedicated, NonDedicated or SourceTV
pub server_type: Server,
/// The Operating System that the server is on.
pub environment_type: Environment,
/// 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>,
/// 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>,
}
impl From<Response> for GenericResponse {
fn from(r: Response) -> Self {
GenericResponse {
name: Some(r.info.name),
description: None,
game: Some(r.info.game),
game_version: Some(r.info.version),
map: Some(r.info.map),
players_maximum: r.info.players_maximum.into(),
players_online: r.info.players_online.into(),
players_bots: Some(r.info.players_bots.into()),
has_password: Some(r.info.has_password),
inner: SpecificResponse::Valve(ExtraResponse {
players: r.players,
rules: r.rules,
protocol: r.info.protocol,
folder: r.info.folder,
appid: r.info.appid,
server_type: r.info.server_type,
environment_type: r.info.environment_type,
vac_secured: r.info.vac_secured,
the_ship: r.info.the_ship,
extra_data: r.info.extra_data,
is_mod: r.info.is_mod,
mod_data: r.info.mod_data,
}),
}
}
}
/// General server information's.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]