diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 59cdbc86..34c030bb 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -29,7 +29,9 @@ byteorder = "1.5" bzip2-rs = "0.1" crc32fast = "1.3" serde_json = "1.0" +serde_derive = "1.0" encoding_rs = "0.8" +reqwest = { version = "0.11", features = ["blocking", "json"] } serde = { version = "1.0", optional = true } diff --git a/crates/lib/examples/test_eco.rs b/crates/lib/examples/test_eco.rs new file mode 100644 index 00000000..735fe9c1 --- /dev/null +++ b/crates/lib/examples/test_eco.rs @@ -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); +} diff --git a/crates/lib/src/games/eco/mod.rs b/crates/lib/src/games/eco/mod.rs new file mode 100644 index 00000000..e22cf53d --- /dev/null +++ b/crates/lib/src/games/eco/mod.rs @@ -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::*; diff --git a/crates/lib/src/games/eco/protocol.rs b/crates/lib/src/games/eco/protocol.rs new file mode 100644 index 00000000..9bb107fc --- /dev/null +++ b/crates/lib/src/games/eco/protocol.rs @@ -0,0 +1,13 @@ +use crate::eco::{Response, Root}; +use crate::http::HttpClient; +use crate::{GDResult, TimeoutSettings}; +use std::net::{IpAddr, SocketAddr}; + +pub fn query(address: &IpAddr, port: Option) -> GDResult { + let address = &SocketAddr::new(*address, port.unwrap_or(3001)); + let mut client = HttpClient::new(address, &Some(TimeoutSettings::default()))?; + + let response = client.request::("/frontpage")?; + + Ok(response.into()) +} diff --git a/crates/lib/src/games/eco/types.rs b/crates/lib/src/games/eco/types.rs new file mode 100644 index 00000000..043c3826 --- /dev/null +++ b/crates/lib/src/games/eco/types.rs @@ -0,0 +1,190 @@ +use serde_derive::Deserialize; +use serde_derive::Serialize; +use std::collections::HashMap; + +#[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, + #[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, + #[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, +} + +#[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, + 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, + pub relay_address: String, + pub access: String, + pub connect: String, +} + +impl From 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, + } + } +} diff --git a/crates/lib/src/games/mod.rs b/crates/lib/src/games/mod.rs index 32467b5c..ea063fcf 100644 --- a/crates/lib/src/games/mod.rs +++ b/crates/lib/src/games/mod.rs @@ -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 diff --git a/crates/lib/src/http.rs b/crates/lib/src/http.rs new file mode 100644 index 00000000..9071e589 --- /dev/null +++ b/crates/lib/src/http.rs @@ -0,0 +1,37 @@ +use crate::GDErrorKind::{PacketSend, ProtocolFormat, SocketConnect}; +use crate::{GDResult, TimeoutSettings}; +use reqwest::blocking::*; +use serde::de::DeserializeOwned; +use std::net::SocketAddr; + +pub struct HttpClient { + client: Client, + address: String, +} + +impl HttpClient { + pub fn new(address: &SocketAddr, timeout_settings: &Option) -> GDResult + where Self: Sized { + let client = Client::builder() + .connect_timeout(TimeoutSettings::get_connect_or_default(timeout_settings)) + .timeout(TimeoutSettings::get_connect_or_default(timeout_settings)) + .build() + .map_err(|e| SocketConnect.context(e))?; + + Ok(Self { + client, + address: format!("http://{}:{}", address.ip(), address.port()), + }) + } + + pub fn concat_path(&self, path: &str) -> String { format!("{}{}", self.address, path) } + + pub fn request(&mut self, path: &str) -> GDResult { + self.client + .get(self.concat_path(path)) + .send() + .map_err(|e| PacketSend.context(e))? + .json::() + .map_err(|e| ProtocolFormat.context(e)) + } +} diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index b17777c6..e182d03d 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -42,6 +42,7 @@ pub mod protocols; pub mod services; mod buffer; +mod http; mod socket; mod utils;