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}