rust-mc-status 3.0.0

High-performance asynchronous Rust library for querying Minecraft server status (Java & Bedrock)
Documentation
//! Bedrock Edition data models.
//!
//! [`BedrockStatus`] is parsed from the RakNet **Unconnected Pong** packet
//! (`0x1C`).  The payload is a semicolon-delimited string with a fixed field
//! order defined by the Bedrock protocol:
//!
//! ```text
//! edition;motd;protocol;version;online;max;uid;motd2;gamemode;gamemode_num;port_ipv4;port_ipv6
//! ```
//!
//! Fields after `max` are optional — servers may omit trailing fields.
//! Most users interact with these fields through
//! [`BedrockServerStatus`](crate::BedrockServerStatus) accessors.

use std::fmt;

use serde::{Deserialize, Serialize};

use crate::error::McError;

// ─── BedrockStatus ───────────────────────────────────────────────────────────

/// Full status payload returned by a Bedrock Edition server.
///
/// Parsed from the semicolon-delimited MOTD string inside the RakNet
/// Unconnected Pong packet.  Fields 0–5 are required; fields 6+ are optional
/// and default to empty strings / `None` when absent.
///
/// Most users access these fields through
/// [`BedrockServerStatus`](crate::BedrockServerStatus) which provides
/// typed accessors and MOTD cleaning.
#[derive(Serialize, Deserialize, Clone)]
pub struct BedrockStatus {
    /// Edition identifier — typically `"MCPE"` (phone/console) or
    /// `"MCEE"` (Education Edition).
    pub edition: String,
    /// Primary MOTD / server name. May contain `§`-color codes.
    /// Use [`BedrockServerStatus::motd_clean`](crate::BedrockServerStatus::motd_clean)
    /// for a plain-text version.
    pub motd: String,
    /// Numeric protocol version string, e.g. `"748"`.
    pub protocol_version: String,
    /// Human-readable game version string, e.g. `"1.21.50"`.
    pub version: String,
    /// Number of players currently online.
    pub online_players: u32,
    /// Server player capacity.
    pub max_players: u32,
    /// Server unique identifier (a large numeric string).
    pub server_uid: String,
    /// Secondary MOTD / subtitle shown below the server name.
    /// May contain `§`-color codes; empty string if absent.
    pub motd2: String,
    /// Game mode name, e.g. `"Survival"`, `"Creative"`.
    pub game_mode: String,
    /// Numeric game mode, e.g. `"0"` for Survival. Corresponds to the
    /// `LevelSettings::GameType` enum in the Bedrock protocol.
    pub game_mode_numeric: String,
    /// IPv4 port hint sent by the server. `None` if field is absent.
    pub port_ipv4: Option<u16>,
    /// IPv6 port hint sent by the server. `None` if field is absent.
    pub port_ipv6: Option<u16>,
    /// Map/world name. Not part of the standard protocol — plugin-specific.
    pub map: Option<String>,
    /// Server software name. Not part of the standard protocol.
    pub software: Option<String>,
    /// Raw semicolon-delimited pong string as received from the server.
    /// Useful for accessing non-standard trailing fields.
    pub raw_data: String,
}

impl BedrockStatus {
    /// Test-only re-export of the internal `parse` function.
    #[doc(hidden)]
    pub fn parse_test(data: &str) -> Result<Self, crate::McError> {
        Self::parse(data)
    }

    /// Parse a `BedrockStatus` from the semicolon-delimited pong string.
    ///
    /// Requires at least 6 fields (edition through max_players).
    /// Extra trailing fields are silently ignored.
    ///
    /// The literal string `"unlimited"` in the `max_players` field — sent by
    /// some large servers (e.g. The Hive) — is mapped to [`u32::MAX`]. Code
    /// that computes ratios like `online / max` should special-case
    /// `max == u32::MAX` to avoid surprising results.
    ///
    /// # Errors
    ///
    /// Returns [`McError::Protocol`] when:
    /// - Fewer than 6 semicolon-separated fields are present.
    /// - `online_players` cannot be parsed as `u32`.
    /// - `max_players` is neither `"unlimited"` nor a valid `u32`.
    pub(crate) fn parse(data: &str) -> Result<Self, McError> {
        let parts: Vec<&str> = data.split(';').collect();
        if parts.len() < 6 {
            return Err(McError::invalid_response(format!(
                "bedrock response has {} fields, need ≥ 6",
                parts.len()
            )));
        }
        Ok(BedrockStatus {
            edition:           parts[0].into(),
            motd:              parts[1].into(),
            protocol_version:  parts[2].into(),
            version:           parts[3].into(),
            online_players:    parts[4]
                .parse()
                .map_err(|_| McError::invalid_response("bad online_players"))?,
            max_players:       match parts[5] {
                "unlimited" => u32::MAX,
                s => s.parse()
                    .map_err(|_| McError::invalid_response("bad max_players"))?,
            },
            server_uid:        parts.get(6).copied().unwrap_or("").into(),
            motd2:             parts.get(7).copied().unwrap_or("").into(),
            game_mode:         parts.get(8).copied().unwrap_or("").into(),
            game_mode_numeric: parts.get(9).copied().unwrap_or("").into(),
            port_ipv4:         parts.get(10).and_then(|s| s.parse().ok()),
            port_ipv6:         parts.get(11).and_then(|s| s.parse().ok()),
            map:               parts.get(12).map(|s| s.to_string()),
            software:          parts.get(13).map(|s| s.to_string()),
            raw_data:          data.into(),
        })
    }
}

impl fmt::Debug for BedrockStatus {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("BedrockStatus")
            .field("edition",           &self.edition)
            .field("motd",              &self.motd)
            .field("protocol_version",  &self.protocol_version)
            .field("version",           &self.version)
            .field("online_players",    &self.online_players)
            .field("max_players",       &self.max_players)
            .field("server_uid",        &self.server_uid)
            .field("motd2",             &self.motd2)
            .field("game_mode",         &self.game_mode)
            .field("game_mode_numeric", &self.game_mode_numeric)
            .field("port_ipv4",         &self.port_ipv4)
            .field("port_ipv6",         &self.port_ipv6)
            .field("map",               &self.map)
            .field("software",          &self.software)
            .field("raw_data_len",      &self.raw_data.len())
            .finish()
    }
}