rust-mc-status 3.0.0

High-performance asynchronous Rust library for querying Minecraft server status (Java & Bedrock)
Documentation
//! Shared top-level data models used across all protocol implementations.

use std::collections::HashMap;

use serde::{Deserialize, Serialize};

use crate::error::McError;
use super::{JavaStatus, BedrockStatus};

// ─── PingResult ───────────────────────────────────────────────────────────────

/// Raw result returned by a [`PingProtocol`](crate::protocol::PingProtocol)
/// before it is assembled into a [`ServerStatus`].
///
/// Separating `latency` from `data` keeps the trait signature clean — protocol
/// implementations don't need to embed timing inside their own models.
///
/// `meta` is a forward-compatibility escape hatch: protocol-specific fields
/// that don't yet have a typed [`ServerData`] variant go here as key-value
/// strings. Once a field is stable enough to model properly, it graduates into
/// the appropriate `ServerData` variant and is removed from `meta`.
///
/// # Implementing a new protocol
///
/// ```rust,ignore
/// use rust_mc_status::{PingResult, models::ServerData};
/// use rust_mc_status::protocol::{PingProtocol, ResolvedTarget};
/// use rust_mc_status::core::time::{start_timer, elapsed_ms};
/// use rust_mc_status::McError;
/// use std::time::Duration;
///
/// #[derive(Debug, Clone, Copy, Default)]
/// pub struct QueryProtocol;
///
/// impl PingProtocol for QueryProtocol {
///     fn name(&self) -> &'static str { "query" }
///
///     async fn ping(
///         &self,
///         target: &ResolvedTarget,
///         timeout: Duration,
///         proxy: Option<&rust_mc_status::ProxyConfig>,
///     ) -> Result<PingResult, McError> {
///         let start = start_timer();
///         // … UDP handshake, stat request, parse response …
///         Ok(PingResult::new(
///             ServerData::Query(Default::default()),
///             elapsed_ms(start),
///         ))
///     }
/// }
/// ```
#[derive(Debug, Clone)]
pub struct PingResult {
    /// Edition-specific server data — Java, Bedrock, Legacy, or Query.
    pub data: ServerData,
    /// Round-trip latency in milliseconds. Always ≥ 0 (measured with a
    /// monotonic clock via [`core::time::elapsed_ms`](crate::core::time::elapsed_ms)).
    pub latency: f64,
    /// Protocol-specific extra fields not yet modelled in [`ServerData`].
    ///
    /// Keys should be short lowercase strings, e.g. `"forge_version"` or
    /// `"netty_channel_id"`. Forwarded verbatim into [`ServerStatus::meta`].
    pub meta: HashMap<String, String>,
}

impl PingResult {
    /// Create a `PingResult` with an empty `meta` map.
    ///
    /// Use this for all current protocols — Java Modern and Bedrock expose
    /// everything through typed [`ServerData`] variants.
    pub fn new(data: ServerData, latency: f64) -> Self {
        Self { data, latency, meta: HashMap::new() }
    }

    /// Create a `PingResult` with protocol-specific metadata.
    ///
    /// Use this when a new protocol exposes fields that don't yet have a
    /// typed [`ServerData`] variant.
    pub fn with_meta(
        data:    ServerData,
        latency: f64,
        meta:    impl Into<HashMap<String, String>>,
    ) -> Self {
        Self { data, latency, meta: meta.into() }
    }
}

// ─── ServerStatus ─────────────────────────────────────────────────────────────

/// Full server status returned to callers after a successful ping.
///
/// Assembled by [`McClient`](crate::McClient) from a [`PingResult`] (produced
/// by the wire protocol) plus resolved network metadata (IP, port, DNS info).
///
/// Most callers access this through the typed wrappers
/// [`JavaServerStatus`](crate::JavaServerStatus) and
/// [`BedrockServerStatus`](crate::BedrockServerStatus) which provide
/// ergonomic accessors. Use `.raw()` on those wrappers or match on
/// [`ServerData`] directly when you need fields not surfaced by the accessors.
///
/// # Serde
///
/// `ServerStatus` implements `Serialize` and `Deserialize`.
/// The `meta` field is skipped in serialisation when empty.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ServerStatus {
    /// `true` if the server responded successfully within the timeout.
    pub online: bool,
    /// Resolved IP address of the server, e.g. `"172.65.197.160"`.
    pub ip: String,
    /// Port that was connected to.
    pub port: u16,
    /// Original hostname as provided to the ping call, e.g. `"mc.hypixel.net"`.
    /// This is the value sent in the Minecraft handshake packet.
    pub hostname: String,
    /// Round-trip latency in milliseconds. `0.0` for cached responses.
    pub latency: f64,
    /// DNS resolution metadata. `None` only in unusual error paths.
    pub dns: Option<DnsInfo>,
    /// Edition-specific payload — match on [`ServerData`] to access fields.
    pub data: ServerData,
    /// `true` when this result was served from the response cache without
    /// a network request. `latency` will be `0.0` in this case.
    ///
    /// Prefer [`JavaServerStatus::is_cached`](crate::JavaServerStatus::is_cached)
    /// or [`BedrockServerStatus::is_cached`](crate::BedrockServerStatus::is_cached)
    /// over accessing this field directly.
    #[serde(default)]
    pub cached: bool,
    /// Protocol-specific extra fields forwarded from [`PingResult::meta`].
    ///
    /// Empty for Java Modern and Bedrock (they have fully-typed models).
    /// Skipped in serialisation when empty.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub meta: HashMap<String, String>,
}

impl ServerStatus {
    /// Returns `(online_players, max_players)` regardless of edition.
    ///
    /// Returns `None` only if `data` is a future variant with no player count.
    pub fn players(&self) -> Option<(i64, i64)> {
        self.data.players()
    }
}

// ─── ServerData ───────────────────────────────────────────────────────────────

/// Edition-specific server data, stored inside [`ServerStatus`].
///
/// Match on this enum to access edition-specific fields:
///
/// ```rust,no_run
/// use rust_mc_status::{McClient, ServerData};
///
/// # #[tokio::main] async fn main() -> Result<(), rust_mc_status::McError> {
/// let client = McClient::builder().build();
/// let status = client.java("mc.hypixel.net").await?;
/// match &status.raw().data {
///     ServerData::Java(j) => println!("Protocol: {}", j.version.protocol),
///     ServerData::Bedrock(b) => println!("Edition: {}", b.edition),
///     _ => {}
/// }
/// # Ok(()) }
/// ```
///
/// # Future variants
///
/// `Legacy` and `Query` are placeholder variants with stub models.
/// They will be populated in a future release when those protocols are
/// implemented. Both are serialised with `#[serde(skip)]` until then.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "edition", rename_all = "snake_case")]
pub enum ServerData {
    /// Java Edition — modern handshake (protocol 1.7 / version 47+).
    Java(JavaStatus),
    /// Bedrock Edition — RakNet UDP Unconnected Ping/Pong.
    Bedrock(BedrockStatus),
    /// Java Edition — legacy ping (`0xFE 0x01`, pre-1.7).
    ///
    /// **Not yet implemented.** Skipped in serialisation.
    #[serde(skip)]
    Legacy(LegacyData),
    /// Java Edition — UDP Query API (GameSpy4 protocol).
    ///
    /// **Not yet implemented.** Skipped in serialisation.
    #[serde(skip)]
    Query(QueryData),
}

impl ServerData {
    /// Returns `(online_players, max_players)` for any implemented variant.
    pub fn players(&self) -> Option<(i64, i64)> {
        match self {
            ServerData::Java(j)    => Some((j.players.online, j.players.max)),
            ServerData::Bedrock(b) => Some((b.online_players as i64, b.max_players as i64)),
            ServerData::Legacy(l)  => Some((l.online_players as i64, l.max_players as i64)),
            ServerData::Query(q)   => Some((q.online_players as i64, q.max_players as i64)),
        }
    }

    /// Returns `true` for any Java Edition variant (Modern, Legacy, Query).
    pub fn is_java(&self) -> bool {
        matches!(self, ServerData::Java(_) | ServerData::Legacy(_) | ServerData::Query(_))
    }

    /// Returns `true` for the Bedrock Edition variant.
    pub fn is_bedrock(&self) -> bool {
        matches!(self, ServerData::Bedrock(_))
    }
}

// ─── Placeholder models ───────────────────────────────────────────────────────

/// Data returned by the pre-1.7 Java legacy ping (`0xFE 0x01`).
///
/// **Placeholder** — will be populated when `src/protocol/java_legacy.rs` is
/// implemented in a future release. All fields currently default to zero/empty.
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct LegacyData {
    /// Numeric Minecraft protocol version.
    pub protocol_version: i32,
    /// Game version string, e.g. `"1.6.4"`.
    pub minecraft_version: String,
    /// Server MOTD / description.
    pub motd: String,
    /// Number of players currently online.
    pub online_players: u32,
    /// Server player capacity.
    pub max_players: u32,
}

/// Data returned by the UDP Query API (GameSpy4 / `enable-query` in server.properties).
///
/// **Placeholder** — will be populated when `src/protocol/query.rs` is
/// implemented in a future release. The Query API provides a richer view than
/// a regular ping: full player list, exact plugin list, and more.
///
/// Requires `enable-query=true` in `server.properties` on the target server.
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct QueryData {
    /// Server MOTD.
    pub motd: String,
    /// Game type, typically `"SMP"`.
    pub game_type: String,
    /// Current map/world name.
    pub map: String,
    /// Number of players currently online.
    pub online_players: u32,
    /// Server player capacity.
    pub max_players: u32,
    /// Port the server listens on.
    pub host_port: u16,
    /// Server IP as reported by the Query response.
    pub host_ip: String,
    /// Full list of online player names (not truncated like the status sample).
    pub players: Vec<String>,
    /// Server plugins as reported by the Query API.
    pub plugins: Vec<String>,
}

// ─── DnsInfo ──────────────────────────────────────────────────────────────────

/// DNS resolution metadata attached to every [`ServerStatus`].
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DnsInfo {
    /// All resolved A/AAAA records for the hostname, as IP strings.
    /// Usually a single entry; load-balanced servers may return several.
    pub a_records: Vec<String>,
    /// CNAME target if the hostname is an alias. `None` for direct A records.
    pub cname: Option<String>,
    /// Cache TTL in seconds (reflects the library's internal DNS cache TTL,
    /// not necessarily the upstream DNS TTL).
    pub ttl: u32,
}

// ─── ServerInfo ───────────────────────────────────────────────────────────────

/// Address + edition pair — used as input to [`McClient::ping_many`](crate::McClient::ping_many).
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ServerInfo {
    /// Server address, e.g. `"mc.hypixel.net"` or `"192.168.1.1:25565"`.
    pub address: String,
    /// Which Minecraft edition to ping.
    pub edition: ServerEdition,
}

// ─── ServerEdition ────────────────────────────────────────────────────────────

/// Which Minecraft edition to ping.
///
/// Parses from the strings `"java"` and `"bedrock"` (case-insensitive):
///
/// ```rust
/// use rust_mc_status::ServerEdition;
///
/// let e: ServerEdition = "java".parse().unwrap();
/// assert_eq!(e, ServerEdition::Java);
///
/// let e: ServerEdition = "BEDROCK".parse().unwrap();
/// assert_eq!(e, ServerEdition::Bedrock);
/// ```
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
pub enum ServerEdition {
    /// Java Edition — TCP, port 25565 by default.
    Java,
    /// Bedrock Edition — UDP, port 19132 by default.
    Bedrock,
}

impl std::str::FromStr for ServerEdition {
    type Err = McError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "java"    => Ok(ServerEdition::Java),
            "bedrock" => Ok(ServerEdition::Bedrock),
            _         => Err(McError::invalid_edition(s.to_string())),
        }
    }
}

// ─── CacheStats ───────────────────────────────────────────────────────────────

/// Snapshot of the current cache entry counts returned by
/// [`McClient::cache_stats`](crate::McClient::cache_stats).
#[derive(Debug, Clone, Copy)]
pub struct CacheStats {
    /// Number of DNS A/AAAA entries currently in the LRU cache.
    pub dns_entries: usize,
    /// Number of SRV entries currently in the LRU cache.
    pub srv_entries: usize,
    /// Number of full response entries in the response cache.
    /// Always `0` when the response cache is disabled.
    pub response_entries: usize,
}