Skip to main content

rust_mc_status/models/
common.rs

1//! Shared top-level data models used across all protocol implementations.
2
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7use crate::error::McError;
8use super::{JavaStatus, BedrockStatus};
9
10// ─── PingResult ───────────────────────────────────────────────────────────────
11
12/// Raw result returned by a [`PingProtocol`](crate::protocol::PingProtocol)
13/// before it is assembled into a [`ServerStatus`].
14///
15/// Separating `latency` from `data` keeps the trait signature clean — protocol
16/// implementations don't need to embed timing inside their own models.
17///
18/// `meta` is a forward-compatibility escape hatch: protocol-specific fields
19/// that don't yet have a typed [`ServerData`] variant go here as key-value
20/// strings. Once a field is stable enough to model properly, it graduates into
21/// the appropriate `ServerData` variant and is removed from `meta`.
22///
23/// # Implementing a new protocol
24///
25/// ```rust,ignore
26/// use rust_mc_status::{PingResult, models::ServerData};
27/// use rust_mc_status::protocol::{PingProtocol, ResolvedTarget};
28/// use rust_mc_status::core::time::{start_timer, elapsed_ms};
29/// use rust_mc_status::McError;
30/// use std::time::Duration;
31///
32/// #[derive(Debug, Clone, Copy, Default)]
33/// pub struct QueryProtocol;
34///
35/// impl PingProtocol for QueryProtocol {
36///     fn name(&self) -> &'static str { "query" }
37///
38///     async fn ping(
39///         &self,
40///         target: &ResolvedTarget,
41///         timeout: Duration,
42///         proxy: Option<&rust_mc_status::ProxyConfig>,
43///     ) -> Result<PingResult, McError> {
44///         let start = start_timer();
45///         // … UDP handshake, stat request, parse response …
46///         Ok(PingResult::new(
47///             ServerData::Query(Default::default()),
48///             elapsed_ms(start),
49///         ))
50///     }
51/// }
52/// ```
53#[derive(Debug, Clone)]
54pub struct PingResult {
55    /// Edition-specific server data — Java, Bedrock, Legacy, or Query.
56    pub data: ServerData,
57    /// Round-trip latency in milliseconds. Always ≥ 0 (measured with a
58    /// monotonic clock via [`core::time::elapsed_ms`](crate::core::time::elapsed_ms)).
59    pub latency: f64,
60    /// Protocol-specific extra fields not yet modelled in [`ServerData`].
61    ///
62    /// Keys should be short lowercase strings, e.g. `"forge_version"` or
63    /// `"netty_channel_id"`. Forwarded verbatim into [`ServerStatus::meta`].
64    pub meta: HashMap<String, String>,
65}
66
67impl PingResult {
68    /// Create a `PingResult` with an empty `meta` map.
69    ///
70    /// Use this for all current protocols — Java Modern and Bedrock expose
71    /// everything through typed [`ServerData`] variants.
72    pub fn new(data: ServerData, latency: f64) -> Self {
73        Self { data, latency, meta: HashMap::new() }
74    }
75
76    /// Create a `PingResult` with protocol-specific metadata.
77    ///
78    /// Use this when a new protocol exposes fields that don't yet have a
79    /// typed [`ServerData`] variant.
80    pub fn with_meta(
81        data:    ServerData,
82        latency: f64,
83        meta:    impl Into<HashMap<String, String>>,
84    ) -> Self {
85        Self { data, latency, meta: meta.into() }
86    }
87}
88
89// ─── ServerStatus ─────────────────────────────────────────────────────────────
90
91/// Full server status returned to callers after a successful ping.
92///
93/// Assembled by [`McClient`](crate::McClient) from a [`PingResult`] (produced
94/// by the wire protocol) plus resolved network metadata (IP, port, DNS info).
95///
96/// Most callers access this through the typed wrappers
97/// [`JavaServerStatus`](crate::JavaServerStatus) and
98/// [`BedrockServerStatus`](crate::BedrockServerStatus) which provide
99/// ergonomic accessors. Use `.raw()` on those wrappers or match on
100/// [`ServerData`] directly when you need fields not surfaced by the accessors.
101///
102/// # Serde
103///
104/// `ServerStatus` implements `Serialize` and `Deserialize`.
105/// The `meta` field is skipped in serialisation when empty.
106#[derive(Debug, Serialize, Deserialize, Clone)]
107pub struct ServerStatus {
108    /// `true` if the server responded successfully within the timeout.
109    pub online: bool,
110    /// Resolved IP address of the server, e.g. `"172.65.197.160"`.
111    pub ip: String,
112    /// Port that was connected to.
113    pub port: u16,
114    /// Original hostname as provided to the ping call, e.g. `"mc.hypixel.net"`.
115    /// This is the value sent in the Minecraft handshake packet.
116    pub hostname: String,
117    /// Round-trip latency in milliseconds. `0.0` for cached responses.
118    pub latency: f64,
119    /// DNS resolution metadata. `None` only in unusual error paths.
120    pub dns: Option<DnsInfo>,
121    /// Edition-specific payload — match on [`ServerData`] to access fields.
122    pub data: ServerData,
123    /// `true` when this result was served from the response cache without
124    /// a network request. `latency` will be `0.0` in this case.
125    ///
126    /// Prefer [`JavaServerStatus::is_cached`](crate::JavaServerStatus::is_cached)
127    /// or [`BedrockServerStatus::is_cached`](crate::BedrockServerStatus::is_cached)
128    /// over accessing this field directly.
129    #[serde(default)]
130    pub cached: bool,
131    /// Protocol-specific extra fields forwarded from [`PingResult::meta`].
132    ///
133    /// Empty for Java Modern and Bedrock (they have fully-typed models).
134    /// Skipped in serialisation when empty.
135    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
136    pub meta: HashMap<String, String>,
137}
138
139impl ServerStatus {
140    /// Returns `(online_players, max_players)` regardless of edition.
141    ///
142    /// Returns `None` only if `data` is a future variant with no player count.
143    pub fn players(&self) -> Option<(i64, i64)> {
144        self.data.players()
145    }
146}
147
148// ─── ServerData ───────────────────────────────────────────────────────────────
149
150/// Edition-specific server data, stored inside [`ServerStatus`].
151///
152/// Match on this enum to access edition-specific fields:
153///
154/// ```rust,no_run
155/// use rust_mc_status::{McClient, ServerData};
156///
157/// # #[tokio::main] async fn main() -> Result<(), rust_mc_status::McError> {
158/// let client = McClient::builder().build();
159/// let status = client.java("mc.hypixel.net").await?;
160/// match &status.raw().data {
161///     ServerData::Java(j) => println!("Protocol: {}", j.version.protocol),
162///     ServerData::Bedrock(b) => println!("Edition: {}", b.edition),
163///     _ => {}
164/// }
165/// # Ok(()) }
166/// ```
167///
168/// # Future variants
169///
170/// `Legacy` and `Query` are placeholder variants with stub models.
171/// They will be populated in a future release when those protocols are
172/// implemented. Both are serialised with `#[serde(skip)]` until then.
173#[derive(Debug, Serialize, Deserialize, Clone)]
174#[serde(tag = "edition", rename_all = "snake_case")]
175pub enum ServerData {
176    /// Java Edition — modern handshake (protocol 1.7 / version 47+).
177    Java(JavaStatus),
178    /// Bedrock Edition — RakNet UDP Unconnected Ping/Pong.
179    Bedrock(BedrockStatus),
180    /// Java Edition — legacy ping (`0xFE 0x01`, pre-1.7).
181    ///
182    /// **Not yet implemented.** Skipped in serialisation.
183    #[serde(skip)]
184    Legacy(LegacyData),
185    /// Java Edition — UDP Query API (GameSpy4 protocol).
186    ///
187    /// **Not yet implemented.** Skipped in serialisation.
188    #[serde(skip)]
189    Query(QueryData),
190}
191
192impl ServerData {
193    /// Returns `(online_players, max_players)` for any implemented variant.
194    pub fn players(&self) -> Option<(i64, i64)> {
195        match self {
196            ServerData::Java(j)    => Some((j.players.online, j.players.max)),
197            ServerData::Bedrock(b) => Some((b.online_players as i64, b.max_players as i64)),
198            ServerData::Legacy(l)  => Some((l.online_players as i64, l.max_players as i64)),
199            ServerData::Query(q)   => Some((q.online_players as i64, q.max_players as i64)),
200        }
201    }
202
203    /// Returns `true` for any Java Edition variant (Modern, Legacy, Query).
204    pub fn is_java(&self) -> bool {
205        matches!(self, ServerData::Java(_) | ServerData::Legacy(_) | ServerData::Query(_))
206    }
207
208    /// Returns `true` for the Bedrock Edition variant.
209    pub fn is_bedrock(&self) -> bool {
210        matches!(self, ServerData::Bedrock(_))
211    }
212}
213
214// ─── Placeholder models ───────────────────────────────────────────────────────
215
216/// Data returned by the pre-1.7 Java legacy ping (`0xFE 0x01`).
217///
218/// **Placeholder** — will be populated when `src/protocol/java_legacy.rs` is
219/// implemented in a future release. All fields currently default to zero/empty.
220#[derive(Debug, Serialize, Deserialize, Clone, Default)]
221pub struct LegacyData {
222    /// Numeric Minecraft protocol version.
223    pub protocol_version: i32,
224    /// Game version string, e.g. `"1.6.4"`.
225    pub minecraft_version: String,
226    /// Server MOTD / description.
227    pub motd: String,
228    /// Number of players currently online.
229    pub online_players: u32,
230    /// Server player capacity.
231    pub max_players: u32,
232}
233
234/// Data returned by the UDP Query API (GameSpy4 / `enable-query` in server.properties).
235///
236/// **Placeholder** — will be populated when `src/protocol/query.rs` is
237/// implemented in a future release. The Query API provides a richer view than
238/// a regular ping: full player list, exact plugin list, and more.
239///
240/// Requires `enable-query=true` in `server.properties` on the target server.
241#[derive(Debug, Serialize, Deserialize, Clone, Default)]
242pub struct QueryData {
243    /// Server MOTD.
244    pub motd: String,
245    /// Game type, typically `"SMP"`.
246    pub game_type: String,
247    /// Current map/world name.
248    pub map: String,
249    /// Number of players currently online.
250    pub online_players: u32,
251    /// Server player capacity.
252    pub max_players: u32,
253    /// Port the server listens on.
254    pub host_port: u16,
255    /// Server IP as reported by the Query response.
256    pub host_ip: String,
257    /// Full list of online player names (not truncated like the status sample).
258    pub players: Vec<String>,
259    /// Server plugins as reported by the Query API.
260    pub plugins: Vec<String>,
261}
262
263// ─── DnsInfo ──────────────────────────────────────────────────────────────────
264
265/// DNS resolution metadata attached to every [`ServerStatus`].
266#[derive(Debug, Serialize, Deserialize, Clone)]
267pub struct DnsInfo {
268    /// All resolved A/AAAA records for the hostname, as IP strings.
269    /// Usually a single entry; load-balanced servers may return several.
270    pub a_records: Vec<String>,
271    /// CNAME target if the hostname is an alias. `None` for direct A records.
272    pub cname: Option<String>,
273    /// Cache TTL in seconds (reflects the library's internal DNS cache TTL,
274    /// not necessarily the upstream DNS TTL).
275    pub ttl: u32,
276}
277
278// ─── ServerInfo ───────────────────────────────────────────────────────────────
279
280/// Address + edition pair — used as input to [`McClient::ping_many`](crate::McClient::ping_many).
281#[derive(Debug, Serialize, Deserialize, Clone)]
282pub struct ServerInfo {
283    /// Server address, e.g. `"mc.hypixel.net"` or `"192.168.1.1:25565"`.
284    pub address: String,
285    /// Which Minecraft edition to ping.
286    pub edition: ServerEdition,
287}
288
289// ─── ServerEdition ────────────────────────────────────────────────────────────
290
291/// Which Minecraft edition to ping.
292///
293/// Parses from the strings `"java"` and `"bedrock"` (case-insensitive):
294///
295/// ```rust
296/// use rust_mc_status::ServerEdition;
297///
298/// let e: ServerEdition = "java".parse().unwrap();
299/// assert_eq!(e, ServerEdition::Java);
300///
301/// let e: ServerEdition = "BEDROCK".parse().unwrap();
302/// assert_eq!(e, ServerEdition::Bedrock);
303/// ```
304#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
305pub enum ServerEdition {
306    /// Java Edition — TCP, port 25565 by default.
307    Java,
308    /// Bedrock Edition — UDP, port 19132 by default.
309    Bedrock,
310}
311
312impl std::str::FromStr for ServerEdition {
313    type Err = McError;
314    fn from_str(s: &str) -> Result<Self, Self::Err> {
315        match s.to_lowercase().as_str() {
316            "java"    => Ok(ServerEdition::Java),
317            "bedrock" => Ok(ServerEdition::Bedrock),
318            _         => Err(McError::invalid_edition(s.to_string())),
319        }
320    }
321}
322
323// ─── CacheStats ───────────────────────────────────────────────────────────────
324
325/// Snapshot of the current cache entry counts returned by
326/// [`McClient::cache_stats`](crate::McClient::cache_stats).
327#[derive(Debug, Clone, Copy)]
328pub struct CacheStats {
329    /// Number of DNS A/AAAA entries currently in the LRU cache.
330    pub dns_entries: usize,
331    /// Number of SRV entries currently in the LRU cache.
332    pub srv_entries: usize,
333    /// Number of full response entries in the response cache.
334    /// Always `0` when the response cache is disabled.
335    pub response_entries: usize,
336}