mirror of
https://github.com/tribufu/rust-gamedig
synced 2026-06-01 09:42:41 +00:00
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:
parent
1620ba36b8
commit
45ffa53de3
15 changed files with 434 additions and 81 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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!(
|
||||
|
|
|
|||
15
crates/lib/src/games/epic.rs
Normal file
15
crates/lib/src/games/epic.rs
Normal 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,
|
||||
}
|
||||
);
|
||||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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)?,
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
58
crates/lib/src/protocols/epic/mod.rs
Normal file
58
crates/lib/src/protocols/epic/mod.rs
Normal 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;
|
||||
181
crates/lib/src/protocols/epic/protocol.rs
Normal file
181
crates/lib/src/protocols/epic/protocol.rs
Normal 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)
|
||||
}
|
||||
52
crates/lib/src/protocols/epic/types.rs
Normal file
52
crates/lib/src/protocols/epic/types.rs
Normal 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 }
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue