rust-mc-status 3.0.0

High-performance asynchronous Rust library for querying Minecraft server status (Java & Bedrock)
Documentation
//! Java Edition data models.
//!
//! [`JavaStatus`] is the root model produced by the Java Modern protocol
//! handshake.  All fields are populated from the JSON payload returned in the
//! `Status Response` packet.
//!
//! Most users interact with these fields through [`JavaServerStatus`](crate::JavaServerStatus)
//! accessors rather than accessing `JavaStatus` directly.  Use `.raw()` or
//! match on [`ServerData::Java`](crate::ServerData::Java) when you need the
//! underlying struct.

use std::fmt;
use std::fs::File;
use std::io::Write;

use base64::{engine::general_purpose, Engine as _};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smallvec::SmallVec;

use crate::error::McError;

// ─── JavaStatus ───────────────────────────────────────────────────────────────

/// Full status payload returned by a Java Edition server.
///
/// Produced by parsing the JSON body of the `Status Response` packet (0x00).
/// Accessed via [`JavaServerStatus`](crate::JavaServerStatus) accessors or
/// directly through [`ServerData::Java`](crate::ServerData::Java).
///
/// # Serde behaviour
///
/// `favicon` is excluded from serialisation (`#[serde(skip_serializing)]`) to
/// avoid bloating JSON output — it is a base64 PNG that can be hundreds of KB.
/// `raw_data` is excluded entirely (`#[serde(skip)]`).
#[derive(Serialize, Deserialize, Clone)]
pub struct JavaStatus {
    /// Server version information (name string + protocol number).
    pub version: JavaVersion,
    /// Online / max player counts plus an optional sample list.
    pub players: JavaPlayers,
    /// Primary server description (MOTD). May contain `§`-color codes.
    /// Use [`JavaServerStatus::motd_clean`](crate::JavaServerStatus::motd_clean)
    /// to get a plain-text version.
    pub description: String,
    /// Base64-encoded 64×64 PNG favicon. `None` if the server does not
    /// send one. Excluded from JSON serialisation — use
    /// [`JavaServerStatus::favicon`](crate::JavaServerStatus::favicon) or
    /// [`save_favicon`](Self::save_favicon) to access it.
    #[serde(skip_serializing)]
    pub favicon: Option<String>,
    /// Current map/world name. Only present on servers that expose it
    /// (e.g. via a status plugin).
    pub map: Option<String>,
    /// Game mode string (e.g. `"Survival"`). Plugin-specific, not standard.
    pub gamemode: Option<String>,
    /// Server software name (e.g. `"Paper 1.21.4"`). Plugin-specific.
    pub software: Option<String>,
    /// Plugin list. `SmallVec<[_; 8]>` avoids a heap allocation for servers
    /// with 8 or fewer plugins, which covers most vanilla/lightly-modded setups.
    /// `None` when the server does not expose plugin information.
    pub plugins: Option<SmallVec<[JavaPlugin; 8]>>,
    /// Mod list (Forge/Fabric servers). Same allocation strategy as `plugins`.
    /// `None` when the server does not expose mod information.
    pub mods: Option<SmallVec<[JavaMod; 8]>>,
    /// Raw JSON response body. Excluded from serialisation.
    /// Useful for accessing non-standard fields added by server plugins.
    #[serde(skip)]
    pub raw_data: Value,
}

impl JavaStatus {
    /// Save the server favicon to a PNG file on disk.
    ///
    /// Strips the optional `data:image/png;base64,` URI prefix before decoding.
    ///
    /// # Errors
    ///
    /// - [`McError::Protocol`] — no favicon available (field is `None`).
    /// - [`McError::Protocol`] — base64 decoding failed (malformed data).
    /// - [`McError::Io`] — file could not be created or written.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// # use rust_mc_status::{McClient, ServerData};
    /// # #[tokio::main] async fn main() -> Result<(), rust_mc_status::McError> {
    /// let client = McClient::builder().build();
    /// if let Ok(s) = client.java("mc.hypixel.net").await {
    ///     s.save_favicon("hypixel_icon.png")?;
    /// }
    /// # Ok(()) }
    /// ```
    pub fn save_favicon(&self, filename: &str) -> Result<(), McError> {
        let favicon = self.favicon.as_deref()
            .ok_or_else(|| McError::invalid_response("No favicon available"))?;
        // Strip optional data-URI prefix ("data:image/png;base64,…")
        let b64 = favicon.split(',').next_back().unwrap_or(favicon);
        let bytes = general_purpose::STANDARD
            .decode(b64)
            .map_err(|e| McError::from(crate::error::ProtocolError::Base64(e)))?;
        let mut file = File::create(filename).map_err(McError::Io)?;
        file.write_all(&bytes).map_err(McError::Io)?;
        Ok(())
    }

    /// Test-only re-export of the internal `from_json` function.
    #[doc(hidden)]
    pub fn from_json_test(v: serde_json::Value) -> Result<Self, crate::McError> {
        Self::from_json(v)
    }

    /// Parse a `JavaStatus` from the raw JSON value in the Status Response packet.
    ///
    /// Handles two `description` shapes:
    /// - Plain string: `"description": "A Minecraft Server"`
    /// - Text object: `"description": { "text": "A Minecraft Server" }`
    pub(crate) fn from_json(v: Value) -> Result<Self, McError> {
        let desc = match &v["description"] {
            Value::String(s) => s.clone(),
            o if o.is_object() => o["text"].as_str().unwrap_or("").to_string(),
            _ => String::new(),
        };
        Ok(JavaStatus {
            version: serde_json::from_value(v["version"].clone())
                .map_err(|e| McError::from(crate::error::ProtocolError::Json(e)))?,
            players: serde_json::from_value(v["players"].clone())
                .map_err(|e| McError::from(crate::error::ProtocolError::Json(e)))?,
            description: desc,
            favicon:  serde_json::from_value(v["favicon"].clone()).ok(),
            map:      v["map"].as_str().map(str::to_string),
            gamemode: v["gamemode"].as_str().map(str::to_string),
            software: v["software"].as_str().map(str::to_string),
            plugins:  serde_json::from_value(v["plugins"].clone()).ok(),
            mods:     serde_json::from_value(v["mods"].clone()).ok(),
            raw_data: v,
        })
    }
}

impl fmt::Debug for JavaStatus {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("JavaStatus")
            .field("version",     &self.version)
            .field("players",     &self.players)
            .field("description", &self.description)
            .field("map",         &self.map)
            .field("gamemode",    &self.gamemode)
            .field("software",    &self.software)
            .field("plugins",     &self.plugins.as_ref().map(|p| p.len()))
            .field("mods",        &self.mods.as_ref().map(|m| m.len()))
            .field("favicon",     &self.favicon.as_ref().map(|_| "[Favicon data]"))
            .field("raw_data",    &"[Value]")
            .finish()
    }
}

// ─── JavaVersion ─────────────────────────────────────────────────────────────

/// Java server version information.
///
/// `name` is the human-readable version string shown in the multiplayer list.
/// `protocol` is the numeric Minecraft protocol version — useful for
/// compatibility checks without parsing the name string.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JavaVersion {
    /// Human-readable version name, e.g. `"1.21.4"` or `"Requires MC 1.8 / 1.21"`.
    pub name: String,
    /// Numeric protocol version, e.g. `769` for 1.21.4.
    /// See <https://wiki.vg/Protocol_version_number> for the full list.
    pub protocol: i64,
}

// ─── JavaPlayers ─────────────────────────────────────────────────────────────

/// Online and maximum player counts, plus an optional sample list.
///
/// Note that `online` and `max` are reported by the server and may not reflect
/// actual player counts on large networks that display custom numbers.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JavaPlayers {
    /// Number of players currently online (as reported by the server).
    pub online: i64,
    /// Server player capacity (as reported by the server).
    pub max: i64,
    /// Sample of online players. Typically 0–12 entries — servers truncate
    /// this list for performance. `None` when the server omits the field.
    /// `SmallVec<[_; 12]>` avoids heap allocation for lists of ≤ 12 players.
    pub sample: Option<SmallVec<[JavaPlayer; 12]>>,
}

// ─── JavaPlayer ──────────────────────────────────────────────────────────────

/// A single player entry in the status sample list.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JavaPlayer {
    /// Player's display name (may differ from their actual username on some servers).
    pub name: String,
    /// Player's UUID as a hyphenated string, e.g. `"069a79f4-44e9-4726-a5be-fca90e38aaf5"`.
    pub id: String,
}

// ─── JavaPlugin ──────────────────────────────────────────────────────────────

/// A single server plugin entry, reported by some server software.
///
/// Plugin information is not part of the vanilla protocol — it is added by
/// plugins like PluginList or exposed by Bukkit/Paper/Spigot servers.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JavaPlugin {
    /// Plugin name, e.g. `"EssentialsX"`.
    pub name: String,
    /// Plugin version string, e.g. `"2.21.0"`. `None` when not reported.
    pub version: Option<String>,
}

// ─── JavaMod ─────────────────────────────────────────────────────────────────

/// A single mod entry, reported by Forge and Fabric servers.
///
/// Mod lists are part of the Forge/Fabric handshake extension — they are not
/// present on vanilla or Bukkit-based servers.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JavaMod {
    /// Mod ID (internal identifier), e.g. `"create"`.
    pub modid: String,
    /// Mod version, e.g. `"0.5.1"`. `None` when not reported.
    pub version: Option<String>,
}