Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: http client and eco support #175

Merged
merged 15 commits into from
Feb 10, 2024
Merged
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ Games:
- Added a valve protocol query example.
- Made all of Just Cause 2: Multiplayer Response and Player fields public.
- [Mindustry](https://mindustrygame.github.io/) support.
- Eco support (by @CosminPerRam).

Crate:
- Changed the serde feature to only enable serde derive for some types: serde and serde_json is now a dependecy by default.

Protocols:
- Added the unreal2 protocol and its associated games: Darkest Hour, Devastation, Killing Floor, Red Orchestra, Unreal Tournament 2003, Unreal Tournament 2004 (by @Douile).
- Added HTTPClient to allow use of HTTP(S) (and JSON) APIs (by @CosminPerRam & @Douile).

CLI:
- Added a CLI (by @cainthebest).
Expand Down
27 changes: 23 additions & 4 deletions crates/lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,36 @@ categories = ["parser-implementations", "parsing", "network-programming", "encod

[features]
default = ["games", "services", "game_defs"]

# Enable query functions for specific games
games = []
services = []
# Enable game definitions for use with the generic query functions
game_defs = ["dep:phf", "games"]
serde = ["dep:serde", "serde/derive"]

# Enable service querying
services = []

# Enable serde derivations for our types
serde = []

# Enable clap derivations for our types
clap = ["dep:clap"]
packet_capture = ["dep:pcap-file", "dep:pnet_packet", "dep:lazy_static"]

# Enable TLS for HTTP Client
tls = ["ureq/tls"]

[dependencies]
byteorder = "1.5"
bzip2-rs = "0.1"
crc32fast = "1.3"
serde_json = "1.0"

encoding_rs = "0.8"
ureq = { version = "2.8", default-features = false, features = ["gzip", "json"] }
url = "2"

serde = { version = "1.0", optional = true }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0" }

phf = { version = "0.11", optional = true, features = ["macros"] }

Expand All @@ -58,6 +73,10 @@ required-features = ["games"]
name = "valve_master_server_query"
required-features = ["services"]

[[example]]
name = "test_eco"
required-features = ["games"]

[[example]]
name = "generic"
required-features = ["games", "game_defs"]
10 changes: 10 additions & 0 deletions crates/lib/examples/test_eco.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use gamedig::games::eco;
use std::net::IpAddr;
use std::str::FromStr;

fn main() {
let ip = IpAddr::from_str("142.132.154.69").unwrap();
let port = 31111;
let r = eco::query(&ip, Some(port));
println!("{:#?}", r);
}
2 changes: 2 additions & 0 deletions crates/lib/src/errors/kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ pub enum GDErrorKind {
JsonParse,
/// Couldn't parse a value.
TypeParse,
/// Couldn't find the host specified.
HostLookup,
}

impl GDErrorKind {
Expand Down
1 change: 1 addition & 0 deletions crates/lib/src/games/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ pub static GAMES: Map<&'static str, Game> = phf_map! {
"redorchestra" => game!("Red Orchestra", 7759, Protocol::Unreal2),
"unrealtournament2003" => game!("Unreal Tournament 2003", 7758, Protocol::Unreal2),
"unrealtournament2004" => game!("Unreal Tournament 2004", 7778, Protocol::Unreal2),
"eco" => game!("Eco", 3000, Protocol::PROPRIETARY(ProprietaryProtocol::Eco)),
"zps" => game!("Zombie Panic: Source", 27015, Protocol::Valve(Engine::new(17_500))),
"mindustry" => game!("Mindustry", crate::games::mindustry::DEFAULT_PORT, Protocol::PROPRIETARY(ProprietaryProtocol::Mindustry)),
};
8 changes: 8 additions & 0 deletions crates/lib/src/games/eco/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/// The implementation.
/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/ffow.js)
pub mod protocol;
/// All types used by the implementation.
pub mod types;

pub use protocol::*;
pub use types::*;
37 changes: 37 additions & 0 deletions crates/lib/src/games/eco/protocol.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use crate::eco::{EcoRequestSettings, Response, Root};
use crate::http::HttpClient;
use crate::{GDResult, TimeoutSettings};
use std::net::{IpAddr, SocketAddr};

/// Query a eco server.
#[inline]
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, &None) }

/// Query a eco server.
#[inline]
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: &Option<TimeoutSettings>,
) -> GDResult<Response> {
query_with_timeout_and_extra_settings(address, port, timeout_settings, None)
}

/// Query a eco server.
pub fn query_with_timeout_and_extra_settings(
address: &IpAddr,
port: Option<u16>,
timeout_settings: &Option<TimeoutSettings>,
extra_settings: Option<EcoRequestSettings>,
) -> GDResult<Response> {
let address = &SocketAddr::new(*address, port.unwrap_or(3001));
let mut client = HttpClient::new(
address,
timeout_settings,
extra_settings.unwrap_or_default().into(),
)?;

let response = client.get_json::<Root>("/frontpage")?;

Ok(response.into())
}
241 changes: 241 additions & 0 deletions crates/lib/src/games/eco/types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

use crate::http::{HttpProtocol, HttpSettings};
use crate::protocols::types::{CommonPlayer, CommonResponse};
use crate::ExtraRequestSettings;

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Root {
#[serde(rename = "Info")]
pub info: Info,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Info {
#[serde(rename = "External")]
pub external: bool,
#[serde(rename = "GamePort")]
pub game_port: u32,
#[serde(rename = "WebPort")]
pub web_port: u32,
#[serde(rename = "IsLAN")]
pub is_lan: bool,
#[serde(rename = "Description")]
pub description: String,
#[serde(rename = "DetailedDescription")]
pub detailed_description: String,
#[serde(rename = "Category")]
pub category: String,
#[serde(rename = "OnlinePlayers")]
pub online_players: u32,
#[serde(rename = "TotalPlayers")]
pub total_players: u32,
#[serde(rename = "OnlinePlayersNames")]
pub online_players_names: Vec<String>,
#[serde(rename = "AdminOnline")]
pub admin_online: bool,
#[serde(rename = "TimeSinceStart")]
pub time_since_start: f64,
#[serde(rename = "TimeLeft")]
pub time_left: f64,
#[serde(rename = "Animals")]
pub animals: u32,
#[serde(rename = "Plants")]
pub plants: u32,
#[serde(rename = "Laws")]
pub laws: u32,
#[serde(rename = "WorldSize")]
pub world_size: String,
#[serde(rename = "Version")]
pub version: String,
#[serde(rename = "EconomyDesc")]
pub economy_desc: String,
#[serde(rename = "SkillSpecializationSetting")]
pub skill_specialization_setting: String,
#[serde(rename = "Language")]
pub language: String,
#[serde(rename = "HasPassword")]
pub has_password: bool,
#[serde(rename = "HasMeteor")]
pub has_meteor: bool,
#[serde(rename = "DistributionStationItems")]
pub distribution_station_items: String,
#[serde(rename = "Playtimes")]
pub playtimes: String,
#[serde(rename = "DiscordAddress")]
pub discord_address: String,
#[serde(rename = "IsPaused")]
pub is_paused: bool,
#[serde(rename = "ActiveAndOnlinePlayers")]
pub active_and_online_players: u32,
#[serde(rename = "PeakActivePlayers")]
pub peak_active_players: u32,
#[serde(rename = "MaxActivePlayers")]
pub max_active_players: u32,
#[serde(rename = "ShelfLifeMultiplier")]
pub shelf_life_multiplier: f64,
#[serde(rename = "ExhaustionAfterHours")]
pub exhaustion_after_hours: f64,
#[serde(rename = "IsLimitingHours")]
pub is_limiting_hours: bool,
#[serde(rename = "ServerAchievementsDict")]
pub server_achievements_dict: HashMap<String, String>,
#[serde(rename = "RelayAddress")]
pub relay_address: String,
#[serde(rename = "Access")]
pub access: String,
#[serde(rename = "JoinUrl")]
pub join_url: String,
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
pub name: String,
}

impl CommonPlayer for Player {
fn as_original(&self) -> crate::protocols::types::GenericPlayer {
crate::protocols::types::GenericPlayer::Eco(self)
}

fn name(&self) -> &str { &self.name }
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct Response {
pub external: bool,
pub port: u32,
pub query_port: u32,
pub is_lan: bool,
pub description: String, // this and other fields require some text filtering
pub description_detailed: String,
pub description_economy: String,
pub category: String,
pub players_online: u32,
pub players_maximum: u32,
pub players: Vec<Player>,
pub admin_online: bool,
pub time_since_start: f64,
pub time_left: f64,
pub animals: u32,
pub plants: u32,
pub laws: u32,
pub world_size: String,
pub game_version: String,
pub skill_specialization_setting: String,
pub language: String,
pub has_password: bool,
pub has_meteor: bool,
pub distribution_station_items: String,
pub playtimes: String,
pub discord_address: String,
pub is_paused: bool,
pub active_and_online_players: u32,
pub peak_active_players: u32,
pub max_active_players: u32,
pub shelf_life_multiplier: f64,
pub exhaustion_after_hours: f64,
pub is_limiting_hours: bool,
pub server_achievements_dict: HashMap<String, String>,
pub relay_address: String,
pub access: String,
pub connect: String,
}

impl From<Root> for Response {
fn from(root: Root) -> Self {
let value = root.info;
Self {
external: value.external,
port: value.game_port,
query_port: value.web_port,
is_lan: value.is_lan,
description: value.description,
description_detailed: value.detailed_description,
description_economy: value.economy_desc,
category: value.category,
players_online: value.online_players,
players_maximum: value.total_players,
players: value
.online_players_names
.iter()
.map(|player| {
Player {
name: player.clone(),
}
})
.collect(),
admin_online: value.admin_online,
time_since_start: value.time_since_start,
time_left: value.time_left,
animals: value.animals,
plants: value.plants,
laws: value.laws,
world_size: value.world_size,
game_version: value.version,
skill_specialization_setting: value.skill_specialization_setting,
language: value.language,
has_password: value.has_password,
has_meteor: value.has_meteor,
distribution_station_items: value.distribution_station_items,
playtimes: value.playtimes,
discord_address: value.discord_address,
is_paused: value.is_paused,
active_and_online_players: value.active_and_online_players,
peak_active_players: value.peak_active_players,
max_active_players: value.max_active_players,
shelf_life_multiplier: value.shelf_life_multiplier,
exhaustion_after_hours: value.exhaustion_after_hours,
is_limiting_hours: value.is_limiting_hours,
server_achievements_dict: value.server_achievements_dict,
relay_address: value.relay_address,
access: value.access,
connect: value.join_url,
}
}
}

impl CommonResponse for Response {
fn as_original(&self) -> crate::protocols::GenericResponse { crate::protocols::GenericResponse::Eco(self) }

fn players_online(&self) -> u32 { self.players_online }

fn players_maximum(&self) -> u32 { self.players_maximum }

fn description(&self) -> Option<&str> { Some(&self.description) }

fn game_version(&self) -> Option<&str> { Some(&self.game_version) }

fn has_password(&self) -> Option<bool> { Some(self.has_password) }

fn players(&self) -> Option<Vec<&dyn CommonPlayer>> { Some(self.players.iter().map(|p| p as _).collect()) }
}

/// Extra request settings for eco queries.
#[derive(Debug, Default, Clone, PartialEq)]
pub struct EcoRequestSettings {
hostname: Option<String>,
}

impl From<ExtraRequestSettings> for EcoRequestSettings {
fn from(value: ExtraRequestSettings) -> Self {
EcoRequestSettings {
hostname: value.hostname,
}
}
}

impl From<EcoRequestSettings> for HttpSettings<String> {
fn from(value: EcoRequestSettings) -> Self {
HttpSettings {
protocol: HttpProtocol::Http,
hostname: value.hostname,
headers: Vec::with_capacity(0),
}
}
}
2 changes: 2 additions & 0 deletions crates/lib/src/games/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub use valve::*;

/// Battalion 1944
pub mod battalion1944;
/// Eco
pub mod eco;
/// Frontlines: Fuel of War
pub mod ffow;
/// Just Cause 2: Multiplayer
Expand Down
Loading
Loading