[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

83
src/games/definitions.rs Normal file
View file

@ -0,0 +1,83 @@
//! Static definitions of currently supported games
use crate::protocols::{
gamespy::GameSpyVersion,
minecraft::{LegacyGroup, Server},
quake::QuakeVersion,
valve::SteamApp,
Protocol,
};
use crate::Game;
use phf::{phf_map, Map};
macro_rules! game {
($name: literal, $default_port: literal, $protocol: expr) => {
Game {
name: $name,
default_port: $default_port,
protocol: $protocol,
}
};
}
/// Map of all currently supported games
pub static GAMES: Map<&'static str, Game> = phf_map! {
"mc" => game!("Minecraft", 25565, Protocol::Minecraft(None)),
"mc-java" => game!("Minecraft (java)", 25565, Protocol::Minecraft(Some(Server::Java))),
"mc-bedrock" => game!("Minecraft (bedrock)", 19132, Protocol::Minecraft(Some(Server::Bedrock))),
"mc-legacy-1.6" => game!("Minecraft (legacy v1.6)", 25565, Protocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_6)))),
"mc-legacy-1.4" => game!("Minecraft (legacy v1.4-1.5)", 25565, Protocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_4)))),
"mc-legacy-b1.8" => game!("Minecraft (legacy vB1.8-1.3)", 25565, Protocol::Minecraft(Some(Server::Legacy(LegacyGroup::VB1_8)))),
"aliens" => game!("Alien Swarm", 27015, Protocol::Valve(SteamApp::ALIENS)),
"aoc" => game!("Age of Chivalry", 27015, Protocol::Valve(SteamApp::AOC)),
"arma2oa" => game!("ARMA 2: Operation Arrowhead", 2304, Protocol::Valve(SteamApp::ARMA2OA)),
"ase" => game!("ARK: Survival Evolved", 27015, Protocol::Valve(SteamApp::ASE)),
"asrd" => game!("Alien Swarm: Reactive Drop", 2304, Protocol::Valve(SteamApp::ASRD)),
"avorion" => game!("Avorion", 27020, Protocol::Valve(SteamApp::AVORION)),
"bat1944" => game!("Battalion 1944", 7780, Protocol::Valve(SteamApp::BAT1944)),
"bb2" => game!("BrainBread 2", 27015, Protocol::Valve(SteamApp::BB2)),
"bf1942" => game!("Battlefield 1942", 23000, Protocol::Gamespy(GameSpyVersion::One)),
"bm" => game!("Black Mesa", 27015, Protocol::Valve(SteamApp::BM)),
"bo" => game!("Ballistic Overkill", 27016, Protocol::Valve(SteamApp::BO)),
"ccure" => game!("Codename CURE", 27015, Protocol::Valve(SteamApp::CCURE)),
"cosu" => game!("Colony Survival", 27004, Protocol::Valve(SteamApp::COSU)),
"cs" => game!("Counter-Strike", 27015, Protocol::Valve(SteamApp::CS)),
"cscz" => game!("Counter Strike: Condition Zero", 27015, Protocol::Valve(SteamApp::CSCZ)),
"csgo" => game!("Counter-Strike: Global Offensive", 27015, Protocol::Valve(SteamApp::CSGO)),
"css" => game!("Counter-Strike: Source", 27015, Protocol::Valve(SteamApp::CSS)),
"cw" => game!("Crysis Wars", 64100, Protocol::Gamespy(GameSpyVersion::Three)),
"dod" => game!("Day of Defeat", 27015, Protocol::Valve(SteamApp::DOD)),
"dods" => game!("Day of Defeat: Source", 27015, Protocol::Valve(SteamApp::DODS)),
"doi" => game!("Day of Infamy", 27015, Protocol::Valve(SteamApp::DOI)),
"dst" => game!("Don't Starve Together", 27016, Protocol::Valve(SteamApp::DST)),
"ffow" => game!("Frontlines: Fuel of War", 5478, Protocol::FFOW),
"gm" => game!("Garry's Mod", 27016, Protocol::Valve(SteamApp::GM)),
"hl2dm" => game!("Half-Life 2 Deathmatch", 27015, Protocol::Valve(SteamApp::HL2DM)),
"hldms" => game!("Half-Life Deathmatch: Source", 27015, Protocol::Valve(SteamApp::HLDMS)),
"hll" => game!("Hell Let Loose", 26420, Protocol::Valve(SteamApp::HLL)),
"ins" => game!("Insurgency", 27015, Protocol::Valve(SteamApp::INS)),
"insmic" => game!("Insurgency: Modern Infantry Combat", 27015, Protocol::Valve(SteamApp::INSMIC)),
"inss" => game!("Insurgency: Sandstorm", 27131, Protocol::Valve(SteamApp::INSS)),
"l4d" => game!("Left 4 Dead", 27015, Protocol::Valve(SteamApp::L4D)),
"l4d2" => game!("Left 4 Dead 2", 27015, Protocol::Valve(SteamApp::L4D2)),
"ohd" => game!("Operation: Harsh Doorstop", 27005, Protocol::Valve(SteamApp::OHD)),
"onset" => game!("Onset", 7776, Protocol::Valve(SteamApp::ONSET)),
"pz" => game!("Project Zomboid", 16261, Protocol::Valve(SteamApp::PZ)),
"quake1" => game!("Quake 1", 27500, Protocol::Quake(QuakeVersion::One)),
"quake2" => game!("Quake 2", 27910, Protocol::Quake(QuakeVersion::Two)),
"quake3a" => game!("Quake 3: Arena", 27960, Protocol::Quake(QuakeVersion::Three)),
"ror2" => game!("Risk of Rain 2", 27016, Protocol::Valve(SteamApp::ROR2)),
"rust" => game!("Rust", 27015, Protocol::Valve(SteamApp::RUST)),
"sc" => game!("Sven Co-op", 27015, Protocol::Valve(SteamApp::SC)),
"sdtd" => game!("7 Days To Die", 26900, Protocol::Valve(SteamApp::SDTD)),
"sof2" => game!("Soldier of Fortune 2", 20100, Protocol::Quake(QuakeVersion::Three)),
"ss" => game!("Serious Sam", 25601, Protocol::Gamespy(GameSpyVersion::One)),
"tf" => game!("The Forest", 27016, Protocol::Valve(SteamApp::TF)),
"tf2" => game!("Team Fortress 2", 27015, Protocol::Valve(SteamApp::TF2)),
"tfc" => game!("Team Fortress Classic", 27015, Protocol::Valve(SteamApp::TFC)),
"ts" => game!("The Ship", 27015, Protocol::TheShip),
"unturned" => game!("Unturned", 27015, Protocol::Valve(SteamApp::UNTURNED)),
"ut" => game!("Unreal Tournament", 7778, Protocol::Gamespy(GameSpyVersion::One)),
"vr" => game!("V Rising", 27016, Protocol::Valve(SteamApp::VR)),
};

View file

@ -1,5 +1,6 @@
use crate::protocols::types::TimeoutSettings;
use crate::protocols::types::{SpecificResponse, TimeoutSettings};
use crate::protocols::valve::{Engine, Environment, Server, ValveProtocol};
use crate::protocols::GenericResponse;
use crate::GDResult;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
@ -43,6 +44,53 @@ pub struct Response {
pub time_left: u16,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ExtraResponse {
/// Protocol used by the server.
pub protocol: u8,
/// Map name.
pub active_mod: String,
/// 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,
/// Current round index.
pub round: u8,
/// Maximum amount of rounds.
pub rounds_maximum: u8,
/// Time left for the current round in seconds.
pub time_left: u16,
}
impl From<Response> for GenericResponse {
fn from(r: Response) -> Self {
Self {
name: Some(r.name),
description: Some(r.description),
game: Some(r.game_mode),
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: Some(r.has_password),
inner: SpecificResponse::FFOW(ExtraResponse {
protocol: r.protocol,
active_mod: r.active_mod,
server_type: r.server_type,
environment_type: r.environment_type,
vac_secured: r.vac_secured,
round: r.round,
rounds_maximum: r.rounds_maximum,
time_left: r.time_left,
}),
}
}
}
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> {
query_with_timeout(address, port, TimeoutSettings::default())
}

View file

@ -1,5 +1,8 @@
//! Currently supported games.
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// Alien Swarm
pub mod aliens;
/// Age of Chivalry
@ -106,3 +109,61 @@ pub mod unturned;
pub mod ut;
/// V Rising
pub mod vr;
use crate::protocols::gamespy::GameSpyVersion;
use crate::protocols::quake::QuakeVersion;
use crate::protocols::{self, Protocol};
use crate::GDResult;
use std::net::{IpAddr, SocketAddr};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct Game {
pub name: &'static str,
pub default_port: u16,
pub protocol: Protocol,
}
#[cfg(feature = "game_defs")]
mod definitions;
#[cfg(feature = "game_defs")]
pub use definitions::GAMES;
pub fn query(game: &Game, address: &IpAddr, port: Option<u16>) -> GDResult<protocols::GenericResponse> {
let socket_addr = SocketAddr::new(*address, port.unwrap_or(game.default_port));
Ok(match &game.protocol {
Protocol::Valve(steam_app) => {
protocols::valve::query(&socket_addr, steam_app.as_engine(), None, None).map(|r| r.into())?
}
Protocol::Minecraft(version) => {
match version {
Some(protocols::minecraft::Server::Java) => {
protocols::minecraft::query_java(&socket_addr, None).map(|r| r.into())?
}
Some(protocols::minecraft::Server::Bedrock) => {
protocols::minecraft::query_bedrock(&socket_addr, None).map(|r| r.into())?
}
Some(protocols::minecraft::Server::Legacy(group)) => {
protocols::minecraft::query_legacy_specific(*group, &socket_addr, None).map(|r| r.into())?
}
None => protocols::minecraft::query(&socket_addr, None).map(|r| r.into())?,
}
}
Protocol::Gamespy(version) => {
match version {
GameSpyVersion::One => protocols::gamespy::one::query(&socket_addr, None).map(|r| r.into())?,
GameSpyVersion::Three => protocols::gamespy::three::query(&socket_addr, None).map(|r| r.into())?,
}
}
Protocol::Quake(version) => {
match version {
QuakeVersion::One => protocols::quake::one::query(&socket_addr, None).map(|r| r.into())?,
QuakeVersion::Two => protocols::quake::two::query(&socket_addr, None).map(|r| r.into())?,
QuakeVersion::Three => protocols::quake::three::query(&socket_addr, None).map(|r| r.into())?,
}
}
Protocol::TheShip => ts::query(address, port).map(|r| r.into())?,
Protocol::FFOW => ffow::query(address, port).map(|r| r.into())?,
})
}

View file

@ -1,5 +1,9 @@
use crate::{
protocols::valve::{self, get_optional_extracted_data, Server, ServerPlayer, SteamApp},
protocols::{
types::SpecificResponse,
valve::{self, get_optional_extracted_data, Server, ServerPlayer, SteamApp},
GenericResponse,
},
GDResult,
};
use std::net::{IpAddr, SocketAddr};
@ -57,6 +61,55 @@ pub struct Response {
pub duration: u8,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct ExtraResponse {
pub protocol: u8,
pub player_details: Vec<TheShipPlayer>,
pub server_type: Server,
pub vac_secured: bool,
pub port: Option<u16>,
pub steam_id: Option<u64>,
pub tv_port: Option<u16>,
pub tv_name: Option<String>,
pub keywords: Option<String>,
pub rules: HashMap<String, String>,
pub mode: u8,
pub witnesses: u8,
pub duration: u8,
}
impl From<Response> for GenericResponse {
fn from(r: Response) -> Self {
Self {
name: Some(r.name),
description: None,
game: Some(r.game),
game_version: Some(r.version),
map: Some(r.map),
players_maximum: r.max_players.into(),
players_online: r.players.into(),
players_bots: Some(r.bots.into()),
has_password: Some(r.has_password),
inner: SpecificResponse::TheShip(ExtraResponse {
protocol: r.protocol,
player_details: r.players_details,
server_type: r.server_type,
vac_secured: r.vac_secured,
steam_id: r.steam_id,
port: r.port,
tv_port: r.tv_port,
tv_name: r.tv_name,
keywords: r.keywords,
rules: r.rules,
mode: r.mode,
witnesses: r.witnesses,
duration: r.duration,
}),
}
}
}
impl Response {
pub fn new_from_valve_response(response: valve::Response) -> Self {
let (port, steam_id, tv_port, tv_name, keywords) = get_optional_extracted_data(response.info.extra_data);