feat: add Ark: Survival Ascended support (#197)

* feat: add initial epic client auth call

* fix: working client auth

* feat: unfinished initial EOS query

* first successful query

* first successful server query

* run fmt

* be a bit more detailed about servers

* properly run fmt for sure this time fr fr

* port of what node gamedig has done

* feat: remove query_raw_values to query_raw

* feat: add raw field to epic response

* feat: pass SocketAddr to epic

* feat: remove unused pub access to internal only struct

* feat: add initial generic impl

* fix: possibly conditional comp

* feat: add epic to the protocol list

* feat: add version and add epic to RESPONSES.md

* feat: add asa to definitions

* feat: add initial protocol macros

* feat: conditional serde ser and des

* fix: cfg serde stuff

* fix: epic macro warn dead code

* partial feature gate epic to tls

* fix: remove asa from game definitions
This commit is contained in:
CosminPerRam 2024-04-21 18:53:33 +03:00 committed by GitHub
parent 1620ba36b8
commit 45ffa53de3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 434 additions and 81 deletions

View file

@ -89,6 +89,7 @@ requirements/information.
| Myth of Empires | MOE | Valve | |
| Pirates, Vikings, and Knights II | PVAK2 | Valve | |
| PixARK | PIXARK | | |
| Ark: Survival Ascended | ASA | Epic | Available on the 'tls' feature |
## Planned to add support:

View file

@ -11,6 +11,7 @@ A protocol is defined as proprietary if it is being used only for a single scope
| Just Cause 2: Multiplayer | Games | Yes | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/jc2mp.js) |
| Unreal 2 | Games | No | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/unreal2.js) | Sometimes servers send strings that node-gamedig would treat as latin1 that are UTF-8 encoded, when this happens the remove color code breaks because latin1 decodes the colour sequences differently. Some games provide additional info at the end of the server info packet, this is not currently handled (see the node implementation). Some games use a bot player to denote the team names, this is not currently handled. |
| Savage 2 | Games | Yes | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/savage2.js) | |
| Epic | Games | No | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/epic.js) | Available only on the 'tls' feature. |
## Planned to add support:

View file

@ -6,57 +6,57 @@ annotated in brackets.
# Response table
| Field | Generic | GameSpy(1) | GameSpy(2) | GameSpy(3) | Minecraft(Java) | Minecraft(Bedrock) | Valve | Quake | Unreal2 | Proprietary: FFOW | Proprietary: TheShip | Proprietary: JC2MP | Proprietary: Savage 2 |
|----------------------|----------|------------|------------|------------|-----------------|--------------------|---------------|-----------|------------|-------------------|----------------------|--------------------|-----------------------|
| name | `Option` | `String` | `String` | `String` | | `String` | `String` | `String` | `String` | `String` | `String` | `String` | `String` |
| description | `Option` | | | | `String` | | | | | `String` | | `String` | |
| game_mode | `Option` | `String` | | `String` | | `Option` | `String` | | `String` | `String` | `String` | | `String` |
| game_version | `Option` | `String` | | `String` | `String` | | `String` | `String` | | `String` | `String` | `String` | |
| map | `Option` | `String` | `String` | `String` | | `Option` | `String` | `String` | `String` | `String` | `String` | | `String` |
| players_maxmimum | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u32` | `u8` | `u8` | `u32` | `u8` |
| players_online | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u32` | `u8` | `u8` | `u32` | `u8` |
| players_bots | `Option` | | | | | | `u8` | | | | `u8` | | |
| has_password | `Option` | `bool` | `bool` | `bool` | | | `bool` | | | `bool` | `bool` | `bool` | |
| players_minimum | | `Option` | `Option` | `Option` | | | | | | | | | `u8` |
| players | | `Vec` | `Vec` | `Vec` | `Option>` | | `Option>` | `Vec ` | `Vec` | | `Vec` | `Vec` | |
| tournament | | `bool` | | `bool` | | | | | | | | | |
| unused_entries | | `Hashmap` | | `HashMap` | | | | `HashMap` | | | | | |
| teams | | | `Vec` | `Vec` | | | | | | | | | |
| protocol_version | | | | | `i32` | `String` | `u8` | | | `u8` | `u8` | | `String` |
| server_type | | | | | `Server` | `Server` | `Server` | | | | `Server` | | |
| rules | | | | | | | `Option>` | | `HashMap>` | | `HashMap` | | |
| environment_type | | | | | | | `Environment` | | | `Environment` | | | |
| vac_secured | | | | | | | `bool` | | | `bool` | `bool` | | |
| map_title | | `Option` | | | | | | | | | | | |
| admin_contact | | `Option` | | | | | | | | | | | |
| admin_name | | `Option` | | | | | | | | | | | |
| favicon | | | | | `Option` | | | | | | | | |
| previews_chat | | | | | `Option` | | | | | | | | |
| enforces_secure_chat | | | | | `Option` | | | | | | | | |
| edition | | | | | | `String` | | | | | | | |
| id | | | | | | `String` | | | `String` | | | | |
| the_ship | | | | | | | `Option` | | | | | | |
| is_mod | | | | | | | `bool` | | | | | | |
| extra_data | | | | | | | `Option` | | | | | | |
| mod_data | | | | | | | `Option` | | | | | | |
| folder | | | | | | | `String` | | | | | | |
| appid | | | | | | | `u32` | | | | | | |
| active_mod | | | | | | | | | | `String` | | | |
| round | | | | | | | | | | `u8` | | | |
| rounds_maximum | | | | | | | | | | `u8` | | | |
| time_left | | | | | | | | | | `u16` | | | |
| port | | | | | | | | | `u32` | | `Option` | | |
| steam_id | | | | | | | | | | | `Option` | | |
| tv_port | | | | | | | | | | | `Option` | | |
| tv_name | | | | | | | | | | | `Option` | | |
| keywords | | | | | | | | | | | `Option` | | |
| mode | | | | | | | | | | | `u8` | | |
| witnesses | | | | | | | | | | | `u8` | | |
| duration | | | | | | | | | | | `u8` | | |
| query_port | | | | | | | | | `u32` | | | | |
| ip | | | | | | | | | `String` | | | | |
| mutators | | | | | | | | | `HashSet` | | | | |
| next_map | | | | | | | | | | | | | `String` |
| location | | | | | | | | | | | | | `String` |
| level_minimum | | | | | | | | | | | | | `String` |
| time | | | | | | | | | | | | | `String` |
| Field | Generic | GameSpy(1) | GameSpy(2) | GameSpy(3) | Minecraft(Java) | Minecraft(Bedrock) | Valve | Quake | Unreal2 | Epic | Proprietary: FFOW | Proprietary: TheShip | Proprietary: JC2MP | Proprietary: Savage 2 |
|----------------------|----------|------------|------------|------------|-----------------|--------------------|---------------|-----------|------------|----------|-------------------|----------------------|--------------------|-----------------------|
| name | `Option` | `String` | `String` | `String` | | `String` | `String` | `String` | `String` | `String` | `String` | `String` | `String` | `String` |
| description | `Option` | | | | `String` | | | | | | `String` | | `String` | |
| game_mode | `Option` | `String` | | `String` | | `Option` | `String` | | `String` | | `String` | `String` | | `String` |
| game_version | `Option` | `String` | | `String` | `String` | | `String` | `String` | | `String` | `String` | `String` | `String` | |
| map | `Option` | `String` | `String` | `String` | | `Option` | `String` | `String` | `String` | `String` | `String` | `String` | | `String` |
| players_maxmimum | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u32` | `u32` | `u8` | `u8` | `u32` | `u8` |
| players_online | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u32` | `u32` | `u8` | `u8` | `u32` | `u8` |
| players_bots | `Option` | | | | | | `u8` | | | | | `u8` | | |
| has_password | `Option` | `bool` | `bool` | `bool` | | | `bool` | | | `bool` | `bool` | `bool` | `bool` | |
| players_minimum | | `Option` | `Option` | `Option` | | | | | | | | | | `u8` |
| players | | `Vec` | `Vec` | `Vec` | `Option>` | | `Option>` | `Vec ` | `Vec` | `Vec` | | `Vec` | `Vec` | |
| tournament | | `bool` | | `bool` | | | | | | | | | | |
| unused_entries | | `Hashmap` | | `HashMap` | | | | `HashMap` | | | | | | |
| teams | | | `Vec` | `Vec` | | | | | | | | | | |
| protocol_version | | | | | `i32` | `String` | `u8` | | | | `u8` | `u8` | | `String` |
| server_type | | | | | `Server` | `Server` | `Server` | | | | | `Server` | | |
| rules | | | | | | | `Option>` | | `HashMap>` | | | `HashMap` | | |
| environment_type | | | | | | | `Environment` | | | | `Environment` | | | |
| vac_secured | | | | | | | `bool` | | | | `bool` | `bool` | | |
| map_title | | `Option` | | | | | | | | | | | | |
| admin_contact | | `Option` | | | | | | | | | | | | |
| admin_name | | `Option` | | | | | | | | | | | | |
| favicon | | | | | `Option` | | | | | | | | | |
| previews_chat | | | | | `Option` | | | | | | | | | |
| enforces_secure_chat | | | | | `Option` | | | | | | | | | |
| edition | | | | | | `String` | | | | | | | | |
| id | | | | | | `String` | | | `String` | | | | | |
| the_ship | | | | | | | `Option` | | | | | | | |
| is_mod | | | | | | | `bool` | | | | | | | |
| extra_data | | | | | | | `Option` | | | | | | | |
| mod_data | | | | | | | `Option` | | | | | | | |
| folder | | | | | | | `String` | | | | | | | |
| appid | | | | | | | `u32` | | | | | | | |
| active_mod | | | | | | | | | | | `String` | | | |
| round | | | | | | | | | | | `u8` | | | |
| rounds_maximum | | | | | | | | | | | `u8` | | | |
| time_left | | | | | | | | | | | `u16` | | | |
| port | | | | | | | | | `u32` | | | `Option` | | |
| steam_id | | | | | | | | | | | | `Option` | | |
| tv_port | | | | | | | | | | | | `Option` | | |
| tv_name | | | | | | | | | | | | `Option` | | |
| keywords | | | | | | | | | | | | `Option` | | |
| mode | | | | | | | | | | | | `u8` | | |
| witnesses | | | | | | | | | | | | `u8` | | |
| duration | | | | | | | | | | | | `u8` | | |
| query_port | | | | | | | | | `u32` | | | | | |
| ip | | | | | | | | | `String` | | | | | |
| mutators | | | | | | | | | `HashSet` | | | | | |
| next_map | | | | | | | | | | | | | | `String` |
| location | | | | | | | | | | | | | | `String` |
| level_minimum | | | | | | | | | | | | | | `String` |
| time | | | | | | | | | | | | | | `String` |

View file

@ -9,6 +9,12 @@ Games:
- [Myth of Empires](https://store.steampowered.com/app/1371580/Myth_of_Empires/) support.
- [Pirates, Vikings, and Knights II](https://store.steampowered.com/app/17570/Pirates_Vikings_and_Knights_II/) support.
- [PixARK](https://store.steampowered.com/app/593600/PixARK/) support.
- [Ark: Survival Ascended](https://store.steampowered.com/app/2399830/ARK_Survival_Ascended/) support, note: not yet in
the games definitions.
Protocols:
- Epic (EOS) support, available only on the `tls` feature.
# 0.5.0 - 15/03/2024

View file

@ -41,6 +41,7 @@ tls = ["ureq/tls"]
byteorder = "1.5"
bzip2-rs = "0.1"
crc32fast = "1.3"
base64 = "0.22.0"
encoding_rs = "0.8"
ureq = { version = "2.8", default-features = false, features = ["gzip", "json"] }

View file

@ -8,6 +8,9 @@ use crate::protocols::types::{GatherToggle, ProprietaryProtocol};
use crate::protocols::valve::GatheringSettings;
use phf::{phf_map, Map};
#[cfg(feature = "tls")]
use crate::protocols::epic::Credentials;
macro_rules! game {
($name: literal, $default_port: expr, $protocol: expr) => {
game!(

View file

@ -0,0 +1,15 @@
//! Unreal2 game query modules
use crate::protocols::epic::game_query_mod;
game_query_mod!(
asa,
"Ark: Survival Ascended",
7777,
Credentials {
deployment: "ad9a8feffb3b4b2ca315546f038c3ae2",
id: "xyza7891muomRmynIIHaJB9COBKkwj6n",
secret: "PP5UGxysEieNfSrEicaD1N2Bb3TdXuD7xHYcsdUHZ7s",
auth_by_external: false,
}
);

View file

@ -1,10 +1,14 @@
//! Currently supported games.
#[cfg(feature = "tls")]
pub mod epic;
pub mod gamespy;
pub mod quake;
pub mod unreal2;
pub mod valve;
#[cfg(feature = "tls")]
pub use epic::*;
pub use gamespy::*;
pub use quake::*;
pub use unreal2::*;

View file

@ -48,6 +48,10 @@ pub fn query_with_timeout_and_extra_settings(
)
.map(Box::new)?
}
#[cfg(feature = "tls")]
Protocol::Epic(credentials) => {
protocols::epic::query_with_timeout(credentials.clone(), &socket_addr, timeout_settings).map(Box::new)?
}
Protocol::Gamespy(version) => {
match version {
GameSpyVersion::One => protocols::gamespy::one::query(&socket_addr, timeout_settings).map(Box::new)?,

View file

@ -13,7 +13,7 @@ use crate::{GDResult, TimeoutSettings};
use std::io::Read;
use std::net::{SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs};
use ureq::{Agent, AgentBuilder};
use ureq::{Agent, AgentBuilder, Request};
use url::{Host, Url};
use serde::{de::DeserializeOwned, Serialize};
@ -250,14 +250,20 @@ impl HttpClient {
self.request_with_json_data("POST", path, headers, data)
}
/// Send a HTTP Post request with FORM data and parse a JSON response.
pub fn post_json_with_form<T: DeserializeOwned>(
&mut self,
path: &str,
headers: HttpHeaders,
data: &[(&str, &str)],
) -> GDResult<T> {
self.request_with_form_data("POST", path, headers, data)
}
// NOTE: More methods can be added here as required using the request_json or
// request_with_json methods
/// Internal request method, makes a request with an arbitrary HTTP method.
#[inline]
fn request(&mut self, method: &str, path: &str, headers: HttpHeaders) -> GDResult<Vec<u8>> {
// Append the path to the pre-parsed URL and create a request object.
self.address.set_path(path);
fn make_request(&self, method: &str, headers: HttpHeaders) -> Request {
let mut request = self.client.request_url(method, &self.address);
// Set the request headers.
@ -271,6 +277,16 @@ impl HttpClient {
}
}
request
}
/// Internal request method, makes a request with an arbitrary HTTP method.
#[inline]
fn request(&mut self, method: &str, path: &str, headers: HttpHeaders) -> GDResult<Vec<u8>> {
// Append the path to the pre-parsed URL and create a request object.
self.address.set_path(path);
let request = self.make_request(method, headers);
// Send the request.
let http_response = request.call().map_err(|e| PacketSend.context(e))?;
@ -299,17 +315,7 @@ impl HttpClient {
fn request_json<T: DeserializeOwned>(&mut self, method: &str, path: &str, headers: HttpHeaders) -> GDResult<T> {
// Append the path to the pre-parsed URL and create a request object.
self.address.set_path(path);
let mut request = self.client.request_url(method, &self.address);
// Set the request headers.
for (key, value) in self.headers.iter() {
request = request.set(key, value);
}
if let Some(headers) = headers {
for (key, value) in headers {
request = request.set(key, value);
}
}
let request = self.make_request(method, headers);
// Send the request and parse the response as JSON.
request
@ -329,16 +335,7 @@ impl HttpClient {
data: S,
) -> GDResult<T> {
self.address.set_path(path);
let mut request = self.client.request_url(method, &self.address);
for (key, value) in self.headers.iter() {
request = request.set(key, value);
}
if let Some(headers) = headers {
for (key, value) in headers {
request = request.set(key, value);
}
}
let request = self.make_request(method, headers);
request
.send_json(data)
@ -346,6 +343,25 @@ impl HttpClient {
.into_json::<T>()
.map_err(|e| ProtocolFormat.context(e))
}
/// Send a HTTP request with FORM data and parse the JSON response.
#[inline]
fn request_with_form_data<T: DeserializeOwned>(
&mut self,
method: &str,
path: &str,
headers: HttpHeaders,
data: &[(&str, &str)],
) -> GDResult<T> {
self.address.set_path(path);
let request = self.make_request(method, headers);
request
.send_form(data)
.map_err(|e| PacketSend.context(e))?
.into_json::<T>()
.map_err(|e| ProtocolFormat.context(e))
}
}
#[cfg(test)]

View file

@ -0,0 +1,58 @@
/// The implementation.
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;
/// Generate a module containing a query function for an epic (EOS) game.
///
/// * `mod_name` - The name to be given to the game module (see ID naming
/// conventions in CONTRIBUTING.md).
/// * `pretty_name` - The full name of the game, will be used as the
/// documentation for the created module.
/// * `steam_app`, `default_port` - Passed through to [game_query_fn].
#[cfg(feature = "games")]
macro_rules! game_query_mod {
($mod_name: ident, $pretty_name: expr, $default_port: literal, $credentials: expr) => {
#[doc = $pretty_name]
pub mod $mod_name {
use crate::protocols::epic::Credentials;
crate::protocols::epic::game_query_fn!($pretty_name, $default_port, $credentials);
}
};
}
#[cfg(feature = "games")]
pub(crate) use game_query_mod;
/// Generate a query function for an epic (EOS) game.
///
/// * `default_port` - The default port the game uses.
/// * `credentials` - Credentials to access EOS.
#[cfg(feature = "games")]
macro_rules! game_query_fn {
($pretty_name: expr, $default_port: literal, $credentials: expr) => {
crate::protocols::epic::game_query_fn! {@gen $default_port, concat!(
"Make a Epic query for ", $pretty_name, ".\n\n",
"If port is `None`, then the default port (", stringify!($default_port), ") will be used."), $credentials}
};
(@gen $default_port: literal, $doc: expr, $credentials: expr) => {
#[doc = $doc]
pub fn query(
address: &std::net::IpAddr,
port: Option<u16>,
) -> crate::GDResult<crate::protocols::epic::Response> {
crate::protocols::epic::query(
$credentials,
&std::net::SocketAddr::new(*address, port.unwrap_or($default_port)),
)
}
};
}
#[cfg(feature = "games")]
pub(crate) use game_query_fn;

View file

@ -0,0 +1,181 @@
use crate::http::HttpClient;
use crate::protocols::epic::Response;
use crate::GDErrorKind::{JsonParse, PacketBad};
use crate::{GDResult, TimeoutSettings};
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use serde::Deserialize;
#[cfg(feature = "serde")]
use serde::Serialize;
use serde_json::Value;
use std::net::SocketAddr;
const EPIC_API_ENDPOINT: &'static str = "https://api.epicgames.dev";
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Credentials {
#[cfg_attr(feature = "serde", serde(skip_deserializing, skip_serializing))]
pub deployment: &'static str,
#[cfg_attr(feature = "serde", serde(skip_deserializing, skip_serializing))]
pub id: &'static str,
#[cfg_attr(feature = "serde", serde(skip_deserializing, skip_serializing))]
pub secret: &'static str,
pub auth_by_external: bool,
}
pub struct EpicProtocol {
client: HttpClient,
credentials: Credentials,
}
#[derive(Deserialize)]
struct ClientTokenResponse {
access_token: String,
}
#[derive(Deserialize)]
struct QueryResponse {
sessions: Value,
}
macro_rules! extract_optional_field {
($value:expr, $fields:expr, $map_func:expr) => {
$fields
.iter()
.fold(Some(&$value), |acc, &key| acc.and_then(|val| val.get(key)))
.map($map_func)
.flatten()
};
}
macro_rules! extract_field {
($value:expr, $fields:expr, $map_func:expr) => {
extract_optional_field!($value, $fields, $map_func)
.ok_or(PacketBad.context("Field is missing or is not parsable."))?
};
}
impl EpicProtocol {
pub fn new(credentials: Credentials, timeout_settings: TimeoutSettings) -> GDResult<Self> {
Ok(Self {
client: HttpClient::from_url(EPIC_API_ENDPOINT, &Some(timeout_settings), None)?,
credentials,
})
}
pub fn auth_by_external(&self) -> GDResult<String> { Ok(String::new()) }
pub fn auth_by_client(&mut self) -> GDResult<String> {
let body = [
("grant_type", "client_credentials"),
("deployment_id", &self.credentials.deployment),
];
let auth_format = format!("{}:{}", self.credentials.id, self.credentials.secret);
let auth_base = BASE64_STANDARD.encode(auth_format);
let auth = format!("Basic {}", auth_base.as_str());
let authorization = auth.as_str();
let headers = [
("Authorization", authorization),
("Content-Type", "application/x-www-form-urlencoded"),
];
let response =
self.client
.post_json_with_form::<ClientTokenResponse>("/auth/v1/oauth/token", Some(&headers), &body)?;
Ok(response.access_token)
}
pub fn query_raw(&mut self, address: &SocketAddr) -> GDResult<Value> {
let port = address.port();
let address = address.ip().to_string();
let body = format!(
"{{\"criteria\":[{{\"key\":\"attributes.ADDRESS_s\",\"op\":\"EQUAL\",\"value\":\"{}\"}}]}}",
address
);
let body = serde_json::from_str::<Value>(body.as_str()).map_err(|e| JsonParse.context(e))?;
let token = if self.credentials.auth_by_external {
self.auth_by_external()?
} else {
self.auth_by_client()?
};
let authorization = format!("Bearer {}", token);
let headers = [
("Content-Type", "application/json"),
("Accept", "application/json"),
("Authorization", authorization.as_str()),
];
let url = format!("/matchmaking/v1/{}/filter", self.credentials.deployment);
let response: QueryResponse = self.client.post_json(url.as_str(), Some(&headers), body)?;
if let Value::Array(sessions) = response.sessions {
if sessions.is_empty() {
return Err(PacketBad.context("No servers provided."));
}
for session in sessions.into_iter() {
let attributes = session
.get("attributes")
.ok_or(PacketBad.context("Expected attributes field missing in sessions."))?;
if attributes
.get("ADDRESSBOUND_s")
.and_then(Value::as_str)
.map_or(false, |v| {
v.contains(&address) || v.contains(&port.to_string())
})
|| attributes
.get("ADDRESS_s")
.and_then(Value::as_str)
.map_or(false, |v| v.contains(&address))
{
return Ok(session);
}
}
return Err(PacketBad.context("Servers were provided but the specified one couldn't be find amonst them."));
}
Err(PacketBad.context("Expected session field to be an array."))
}
pub fn query(&mut self, address: &SocketAddr) -> GDResult<Response> {
let value = self.query_raw(address)?;
let build_version = extract_optional_field!(value, ["attributes", "BUILDID_s"], Value::as_str);
let minor_version = extract_optional_field!(value, ["attributes", "MINORBUILDID_s"], Value::as_str);
let game_version = match (build_version, minor_version) {
(Some(b), Some(m)) => Some(format!("{b}.{m}")),
_ => None,
};
Ok(Response {
name: extract_field!(value, ["attributes", "CUSTOMSERVERNAME_s"], Value::as_str).to_string(),
map: extract_field!(value, ["attributes", "MAPNAME_s"], Value::as_str).to_string(),
has_password: extract_field!(value, ["attributes", "SERVERPASSWORD_b"], Value::as_bool),
players_online: extract_field!(value, ["totalPlayers"], Value::as_u64) as u32,
players_maxmimum: extract_field!(value, ["settings", "maxPublicPlayers"], Value::as_u64) as u32,
players: vec![],
game_version,
raw: value,
})
}
}
pub fn query(credentials: Credentials, address: &SocketAddr) -> GDResult<Response> {
query_with_timeout(credentials, address, None)
}
pub fn query_with_timeout(
credentials: Credentials,
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let mut client = EpicProtocol::new(credentials, timeout_settings.unwrap_or_default())?;
client.query(address)
}

View file

@ -0,0 +1,52 @@
use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer};
use crate::protocols::GenericResponse;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct Response {
pub name: String,
pub map: String,
pub has_password: bool,
pub players_online: u32,
pub players_maxmimum: u32,
pub players: Vec<Player>,
pub game_version: Option<String>,
pub raw: Value,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse { GenericResponse::Epic(self) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn players_maximum(&self) -> u32 { self.players_maxmimum }
fn players_online(&self) -> u32 { self.players_online }
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 &dyn CommonPlayer)
.collect(),
)
}
fn game_version(&self) -> Option<&str> { self.game_version.as_deref() }
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct Player {
pub name: String,
}
impl CommonPlayer for Player {
fn as_original(&self) -> GenericPlayer { GenericPlayer::Epic(self) }
fn name(&self) -> &str { &self.name }
}

View file

@ -4,6 +4,9 @@
//! implementation will be in that specific needed place, a protocol can be
//! independently queried.
#[cfg(feature = "tls")]
/// Reference: [node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/epic.js)
pub mod epic;
/// Reference: [node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy1.js)
pub mod gamespy;
/// Reference: [node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake1.js)

View file

@ -1,5 +1,7 @@
#[cfg(feature = "games")]
use crate::games::minecraft;
#[cfg(feature = "tls")]
use crate::protocols::epic;
use crate::protocols::{gamespy, quake, unreal2, valve};
use crate::GDErrorKind::InvalidInput;
use crate::GDResult;
@ -31,6 +33,8 @@ pub enum Protocol {
Quake(quake::QuakeVersion),
Valve(valve::Engine),
Unreal2,
#[cfg(feature = "tls")]
Epic(epic::Credentials),
#[cfg(feature = "games")]
PROPRIETARY(ProprietaryProtocol),
}
@ -43,6 +47,8 @@ pub enum GenericResponse<'a> {
Quake(quake::VersionedResponse<'a>),
Valve(&'a valve::Response),
Unreal2(&'a unreal2::Response),
#[cfg(feature = "tls")]
Epic(&'a epic::Response),
#[cfg(feature = "games")]
Mindustry(&'a crate::games::mindustry::types::ServerData),
#[cfg(feature = "games")]
@ -68,6 +74,8 @@ pub enum GenericPlayer<'a> {
QuakeTwo(&'a quake::two::Player),
Gamespy(gamespy::VersionedPlayer<'a>),
Unreal2(&'a unreal2::Player),
#[cfg(feature = "tls")]
Epic(&'a epic::Player),
#[cfg(feature = "games")]
Minecraft(&'a minecraft::Player),
#[cfg(feature = "games")]