mirror of
https://github.com/tribufu/rust-gamedig
synced 2026-06-01 09:42:41 +00:00
feat: http client and eco support (#175)
* feat: initial http and eco support * http: Replace reqwest with ureq and add HTTPS support ureq markets itself as a lightweight blocking HTTP client which might be a good choice for rust-gamedig at the moment. However the main reason for changing to ureq is that it allows setting a "resolver" function which overrides the IP address to connect to. This is useful because it allows us to pass a URL with the desired hostname without the HTTP library doing an extra DNS lookup (this allows HTTPS to work when we specify the exact IP and port to connect to external to the URL). Other changes in this commit are: - Feature gated things that depend on serde: this means that the eco game won't be available if the library is compiled without serde - Added the TLS feature to enable TLS support in the HTTP library - Added HTTPSettings to set the protocol (HTTP/HTTPS) and the hostname - Setting a user-agent string on HTTP requests (allows the server to see what program is being used to query them) - Store the address as a parsed Url so we don't re-parse it on every request - Add a method to POST JSON data and parse response - Renamed the request() method to get_json() in anticipation of a future method that will send a GET request and handle the raw bytes instead of using serde - Improved documentation * eco: Add generic impls * eco: fixes * http: Add headers to HttpSettings and rename from HTTPSettings * eco: Add extra request settings * http: Add support for querying raw bytes * http: Add unit-tests * http: Rename HttpProtocol * crate: Make serde dependency non-optional The serde feature now only enable serde derivations for our types that don't need it for the library to function. * http: Add helper for creating HttpClients to query APIs Adds the from_url helper that should make working with master server web APIs easier. * Add/Update badge * crate: Require games feature for eco example * docs: Update changelog --------- Co-authored-by: Douile <douile@douile.com>
This commit is contained in:
parent
2a65c39cb6
commit
310b62664c
13 changed files with 800 additions and 10 deletions
|
|
@ -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
crates/lib/src/games/eco/mod.rs
Normal file
8
crates/lib/src/games/eco/mod.rs
Normal file
|
|
@ -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
crates/lib/src/games/eco/protocol.rs
Normal file
37
crates/lib/src/games/eco/protocol.rs
Normal file
|
|
@ -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
crates/lib/src/games/eco/types.rs
Normal file
241
crates/lib/src/games/eco/types.rs
Normal file
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
use crate::games::types::Game;
|
||||
use crate::games::{ffow, jc2m, mindustry, minecraft, savage2, theship};
|
||||
use crate::games::{eco, ffow, jc2m, mindustry, minecraft, savage2, theship};
|
||||
use crate::protocols;
|
||||
use crate::protocols::gamespy::GameSpyVersion;
|
||||
use crate::protocols::quake::QuakeVersion;
|
||||
|
|
@ -112,6 +112,15 @@ pub fn query_with_timeout_and_extra_settings(
|
|||
}
|
||||
}
|
||||
}
|
||||
ProprietaryProtocol::Eco => {
|
||||
eco::query_with_timeout_and_extra_settings(
|
||||
address,
|
||||
port,
|
||||
&timeout_settings,
|
||||
extra_settings.map(ExtraRequestSettings::into),
|
||||
)
|
||||
.map(Box::new)?
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue