Skip to main content

rust_mc_status/models/
java.rs

1//! Java Edition data models.
2//!
3//! [`JavaStatus`] is the root model produced by the Java Modern protocol
4//! handshake.  All fields are populated from the JSON payload returned in the
5//! `Status Response` packet.
6//!
7//! Most users interact with these fields through [`JavaServerStatus`](crate::JavaServerStatus)
8//! accessors rather than accessing `JavaStatus` directly.  Use `.raw()` or
9//! match on [`ServerData::Java`](crate::ServerData::Java) when you need the
10//! underlying struct.
11
12use std::fmt;
13use std::fs::File;
14use std::io::Write;
15
16use base64::{engine::general_purpose, Engine as _};
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19use smallvec::SmallVec;
20
21use crate::error::McError;
22
23// ─── JavaStatus ───────────────────────────────────────────────────────────────
24
25/// Full status payload returned by a Java Edition server.
26///
27/// Produced by parsing the JSON body of the `Status Response` packet (0x00).
28/// Accessed via [`JavaServerStatus`](crate::JavaServerStatus) accessors or
29/// directly through [`ServerData::Java`](crate::ServerData::Java).
30///
31/// # Serde behaviour
32///
33/// `favicon` is excluded from serialisation (`#[serde(skip_serializing)]`) to
34/// avoid bloating JSON output — it is a base64 PNG that can be hundreds of KB.
35/// `raw_data` is excluded entirely (`#[serde(skip)]`).
36#[derive(Serialize, Deserialize, Clone)]
37pub struct JavaStatus {
38    /// Server version information (name string + protocol number).
39    pub version: JavaVersion,
40    /// Online / max player counts plus an optional sample list.
41    pub players: JavaPlayers,
42    /// Primary server description (MOTD). May contain `§`-color codes.
43    /// Use [`JavaServerStatus::motd_clean`](crate::JavaServerStatus::motd_clean)
44    /// to get a plain-text version.
45    pub description: String,
46    /// Base64-encoded 64×64 PNG favicon. `None` if the server does not
47    /// send one. Excluded from JSON serialisation — use
48    /// [`JavaServerStatus::favicon`](crate::JavaServerStatus::favicon) or
49    /// [`save_favicon`](Self::save_favicon) to access it.
50    #[serde(skip_serializing)]
51    pub favicon: Option<String>,
52    /// Current map/world name. Only present on servers that expose it
53    /// (e.g. via a status plugin).
54    pub map: Option<String>,
55    /// Game mode string (e.g. `"Survival"`). Plugin-specific, not standard.
56    pub gamemode: Option<String>,
57    /// Server software name (e.g. `"Paper 1.21.4"`). Plugin-specific.
58    pub software: Option<String>,
59    /// Plugin list. `SmallVec<[_; 8]>` avoids a heap allocation for servers
60    /// with 8 or fewer plugins, which covers most vanilla/lightly-modded setups.
61    /// `None` when the server does not expose plugin information.
62    pub plugins: Option<SmallVec<[JavaPlugin; 8]>>,
63    /// Mod list (Forge/Fabric servers). Same allocation strategy as `plugins`.
64    /// `None` when the server does not expose mod information.
65    pub mods: Option<SmallVec<[JavaMod; 8]>>,
66    /// Raw JSON response body. Excluded from serialisation.
67    /// Useful for accessing non-standard fields added by server plugins.
68    #[serde(skip)]
69    pub raw_data: Value,
70}
71
72impl JavaStatus {
73    /// Save the server favicon to a PNG file on disk.
74    ///
75    /// Strips the optional `data:image/png;base64,` URI prefix before decoding.
76    ///
77    /// # Errors
78    ///
79    /// - [`McError::Protocol`] — no favicon available (field is `None`).
80    /// - [`McError::Protocol`] — base64 decoding failed (malformed data).
81    /// - [`McError::Io`] — file could not be created or written.
82    ///
83    /// # Example
84    ///
85    /// ```rust,no_run
86    /// # use rust_mc_status::{McClient, ServerData};
87    /// # #[tokio::main] async fn main() -> Result<(), rust_mc_status::McError> {
88    /// let client = McClient::builder().build();
89    /// if let Ok(s) = client.java("mc.hypixel.net").await {
90    ///     s.save_favicon("hypixel_icon.png")?;
91    /// }
92    /// # Ok(()) }
93    /// ```
94    pub fn save_favicon(&self, filename: &str) -> Result<(), McError> {
95        let favicon = self.favicon.as_deref()
96            .ok_or_else(|| McError::invalid_response("No favicon available"))?;
97        // Strip optional data-URI prefix ("data:image/png;base64,…")
98        let b64 = favicon.split(',').next_back().unwrap_or(favicon);
99        let bytes = general_purpose::STANDARD
100            .decode(b64)
101            .map_err(|e| McError::from(crate::error::ProtocolError::Base64(e)))?;
102        let mut file = File::create(filename).map_err(McError::Io)?;
103        file.write_all(&bytes).map_err(McError::Io)?;
104        Ok(())
105    }
106
107    /// Test-only re-export of the internal `from_json` function.
108    #[doc(hidden)]
109    pub fn from_json_test(v: serde_json::Value) -> Result<Self, crate::McError> {
110        Self::from_json(v)
111    }
112
113    /// Parse a `JavaStatus` from the raw JSON value in the Status Response packet.
114    ///
115    /// Handles two `description` shapes:
116    /// - Plain string: `"description": "A Minecraft Server"`
117    /// - Text object: `"description": { "text": "A Minecraft Server" }`
118    pub(crate) fn from_json(v: Value) -> Result<Self, McError> {
119        let desc = match &v["description"] {
120            Value::String(s) => s.clone(),
121            o if o.is_object() => o["text"].as_str().unwrap_or("").to_string(),
122            _ => String::new(),
123        };
124        Ok(JavaStatus {
125            version: serde_json::from_value(v["version"].clone())
126                .map_err(|e| McError::from(crate::error::ProtocolError::Json(e)))?,
127            players: serde_json::from_value(v["players"].clone())
128                .map_err(|e| McError::from(crate::error::ProtocolError::Json(e)))?,
129            description: desc,
130            favicon:  serde_json::from_value(v["favicon"].clone()).ok(),
131            map:      v["map"].as_str().map(str::to_string),
132            gamemode: v["gamemode"].as_str().map(str::to_string),
133            software: v["software"].as_str().map(str::to_string),
134            plugins:  serde_json::from_value(v["plugins"].clone()).ok(),
135            mods:     serde_json::from_value(v["mods"].clone()).ok(),
136            raw_data: v,
137        })
138    }
139}
140
141impl fmt::Debug for JavaStatus {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        f.debug_struct("JavaStatus")
144            .field("version",     &self.version)
145            .field("players",     &self.players)
146            .field("description", &self.description)
147            .field("map",         &self.map)
148            .field("gamemode",    &self.gamemode)
149            .field("software",    &self.software)
150            .field("plugins",     &self.plugins.as_ref().map(|p| p.len()))
151            .field("mods",        &self.mods.as_ref().map(|m| m.len()))
152            .field("favicon",     &self.favicon.as_ref().map(|_| "[Favicon data]"))
153            .field("raw_data",    &"[Value]")
154            .finish()
155    }
156}
157
158// ─── JavaVersion ─────────────────────────────────────────────────────────────
159
160/// Java server version information.
161///
162/// `name` is the human-readable version string shown in the multiplayer list.
163/// `protocol` is the numeric Minecraft protocol version — useful for
164/// compatibility checks without parsing the name string.
165#[derive(Debug, Serialize, Deserialize, Clone)]
166pub struct JavaVersion {
167    /// Human-readable version name, e.g. `"1.21.4"` or `"Requires MC 1.8 / 1.21"`.
168    pub name: String,
169    /// Numeric protocol version, e.g. `769` for 1.21.4.
170    /// See <https://wiki.vg/Protocol_version_number> for the full list.
171    pub protocol: i64,
172}
173
174// ─── JavaPlayers ─────────────────────────────────────────────────────────────
175
176/// Online and maximum player counts, plus an optional sample list.
177///
178/// Note that `online` and `max` are reported by the server and may not reflect
179/// actual player counts on large networks that display custom numbers.
180#[derive(Debug, Serialize, Deserialize, Clone)]
181pub struct JavaPlayers {
182    /// Number of players currently online (as reported by the server).
183    pub online: i64,
184    /// Server player capacity (as reported by the server).
185    pub max: i64,
186    /// Sample of online players. Typically 0–12 entries — servers truncate
187    /// this list for performance. `None` when the server omits the field.
188    /// `SmallVec<[_; 12]>` avoids heap allocation for lists of ≤ 12 players.
189    pub sample: Option<SmallVec<[JavaPlayer; 12]>>,
190}
191
192// ─── JavaPlayer ──────────────────────────────────────────────────────────────
193
194/// A single player entry in the status sample list.
195#[derive(Debug, Serialize, Deserialize, Clone)]
196pub struct JavaPlayer {
197    /// Player's display name (may differ from their actual username on some servers).
198    pub name: String,
199    /// Player's UUID as a hyphenated string, e.g. `"069a79f4-44e9-4726-a5be-fca90e38aaf5"`.
200    pub id: String,
201}
202
203// ─── JavaPlugin ──────────────────────────────────────────────────────────────
204
205/// A single server plugin entry, reported by some server software.
206///
207/// Plugin information is not part of the vanilla protocol — it is added by
208/// plugins like PluginList or exposed by Bukkit/Paper/Spigot servers.
209#[derive(Debug, Serialize, Deserialize, Clone)]
210pub struct JavaPlugin {
211    /// Plugin name, e.g. `"EssentialsX"`.
212    pub name: String,
213    /// Plugin version string, e.g. `"2.21.0"`. `None` when not reported.
214    pub version: Option<String>,
215}
216
217// ─── JavaMod ─────────────────────────────────────────────────────────────────
218
219/// A single mod entry, reported by Forge and Fabric servers.
220///
221/// Mod lists are part of the Forge/Fabric handshake extension — they are not
222/// present on vanilla or Bukkit-based servers.
223#[derive(Debug, Serialize, Deserialize, Clone)]
224pub struct JavaMod {
225    /// Mod ID (internal identifier), e.g. `"create"`.
226    pub modid: String,
227    /// Mod version, e.g. `"0.5.1"`. `None` when not reported.
228    pub version: Option<String>,
229}