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}