diff --git a/Cargo.lock b/Cargo.lock index a77715246..639c608a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10189,7 +10189,6 @@ dependencies = [ "either", "encoding_rs", "enumset", - "eyre", "flate2", "fs4", "futures", diff --git a/apps/labrinth/src/models/exp/minecraft.rs b/apps/labrinth/src/models/exp/minecraft.rs index 88e337a6c..3b8fb5508 100644 --- a/apps/labrinth/src/models/exp/minecraft.rs +++ b/apps/labrinth/src/models/exp/minecraft.rs @@ -423,11 +423,11 @@ pub struct JavaServerPingData { /// Reported version protocol number of the server. pub version_protocol: i32, /// Description/MOTD of the server as shown in the server list. - pub description: Option, + pub description: String, /// Number of players online at the time. - pub players_online: Option, + pub players_online: i32, /// Maximum number of players allowed on the server. - pub players_max: Option, + pub players_max: i32, } component::relations! { diff --git a/apps/labrinth/src/queue/server_ping.rs b/apps/labrinth/src/queue/server_ping.rs index d3f6d5315..c65ceb80b 100644 --- a/apps/labrinth/src/queue/server_ping.rs +++ b/apps/labrinth/src/queue/server_ping.rs @@ -6,6 +6,7 @@ use crate::models::exp; use crate::models::ids::ProjectId; use crate::models::projects::ProjectStatus; use crate::{database::PgPool, util::error::Context}; +use async_minecraft_ping::ServerDescription; use chrono::{TimeDelta, Utc}; use clickhouse::{Client, Row}; use serde::Serialize; @@ -106,16 +107,11 @@ impl ServerPingQueue { project_id: project_id.0, address: ping.address.clone(), latency_ms: data.map(|d| d.latency.as_millis() as u32), - description: data.and_then(|d| { - d.description.as_ref().map(|d| { - serde_json::to_string(&d) - .expect("serialization should not fail") - }) - }), + description: data.map(|d| d.description.clone()), version_name: data.map(|d| d.version_name.clone()), version_protocol: data.map(|d| d.version_protocol), - players_online: data.and_then(|d| d.players_online), - players_max: data.and_then(|d| d.players_max), + players_online: data.map(|d| d.players_online), + players_max: data.map(|d| d.players_max), }; ch.write(&row) @@ -260,27 +256,45 @@ pub async fn ping_server( .map(|duration| duration.min(default_duration)) .unwrap_or(default_duration); - let conn = async_minecraft_ping::ConnectionConfig::build(address) - .with_srv_lookup() - .with_timeout(timeout) - .connect() - .await - .wrap_err("failed to connect to server")?; + let (address, port) = match address.rsplit_once(':') { + Some((addr, port)) => { + let port = port.parse::().wrap_err("invalid port number")?; + (addr, port) + } + None => (address, 25565), + }; - let status = conn - .status() - .await - .wrap_err("failed to get server status")? - .status; + let task = async move { + let conn = async_minecraft_ping::ConnectionConfig::build(address) + .with_port(port) + .with_srv_lookup() + .connect() + .await + .wrap_err("failed to connect to server")?; - eyre::Ok(exp::minecraft::JavaServerPingData { - latency: start.elapsed(), - version_name: status.version.name, - version_protocol: status.version.protocol, - description: status.description, - players_online: status.players.online, - players_max: status.players.max, - }) + let status = conn + .status() + .await + .wrap_err("failed to get server status")? + .status; + + eyre::Ok(exp::minecraft::JavaServerPingData { + latency: start.elapsed(), + version_name: status.version.name, + version_protocol: status.version.protocol, + description: match status.description { + ServerDescription::Plain(text) + | ServerDescription::Object { text } => text, + }, + players_online: status.players.online, + players_max: status.players.max, + }) + }; + + tokio::time::timeout(timeout, task) + .await + .map_err(eyre::Error::new) + .flatten() } #[derive(Debug, Row, Serialize, Clone)] diff --git a/packages/app-lib/Cargo.toml b/packages/app-lib/Cargo.toml index 39f2ae1a4..d2470da7d 100644 --- a/packages/app-lib/Cargo.toml +++ b/packages/app-lib/Cargo.toml @@ -37,7 +37,6 @@ dunce = { workspace = true } either = { workspace = true } encoding_rs = { workspace = true } enumset = { workspace = true } -eyre = { workspace = true } flate2 = { workspace = true } fs4 = { workspace = true, features = ["tokio"] } futures = { workspace = true, features = ["alloc", "async-await"] } diff --git a/packages/app-lib/src/api/worlds.rs b/packages/app-lib/src/api/worlds.rs index 4cdd06088..16b87965b 100644 --- a/packages/app-lib/src/api/worlds.rs +++ b/packages/app-lib/src/api/worlds.rs @@ -6,13 +6,14 @@ use crate::state::attached_world_data::AttachedWorldData; use crate::state::{ Profile, ProfileInstallStage, attached_world_data, server_join_log, }; -use crate::util::io; use crate::util::protocol_version::OLD_PROTOCOL_VERSIONS; pub use crate::util::protocol_version::ProtocolVersion; pub use crate::util::server_ping::{ ServerGameProfile, ServerPlayers, ServerStatus, ServerVersion, }; -use crate::{Context, ErrorKind, Result, State, launcher}; +use crate::util::{io, server_ping}; +use crate::{Error, ErrorKind, Result, State, launcher}; +use async_minecraft_ping::ServerDescription; use async_walkdir::WalkDir; use async_zip::{Compression, ZipEntryBuilder}; use chrono::{DateTime, Local, TimeZone, Utc}; @@ -909,54 +910,67 @@ pub async fn get_server_status( "Pinging {address} with protocol version {protocol_version:?}" ); - // get_server_status_old(address, protocol_version).await - get_server_status_new(address, protocol_version).await + get_server_status_old(address, protocol_version).await + // get_server_status_new(address, protocol_version).await } -// async fn _get_server_status_old( -// address: &str, -// protocol_version: Option, -// ) -> Result { -// let (original_host, original_port) = parse_server_address(address)?; -// let (host, port) = -// resolve_server_address(original_host, original_port).await?; -// tracing::debug!( -// "Pinging {address} with protocol version {protocol_version:?}" -// ); -// server_ping::get_server_status( -// &(&host as &str, port), -// (original_host, original_port), -// protocol_version, -// ) -// .await - -async fn get_server_status_new( +async fn get_server_status_old( address: &str, protocol_version: Option, ) -> Result { + let (original_host, original_port) = parse_server_address(address)?; + let (host, port) = + resolve_server_address(original_host, original_port).await?; + tracing::debug!( + "Pinging {address} with protocol version {protocol_version:?}" + ); + server_ping::get_server_status( + &(&host as &str, port), + (original_host, original_port), + protocol_version, + ) + .await +} + +async fn _get_server_status_new( + address: &str, + protocol_version: Option, +) -> Result { + let (address, port) = match address.rsplit_once(':') { + Some((addr, port)) => { + let port = port.parse::().map_err(|_err| { + Error::from(ErrorKind::InputError("invalid port number".into())) + })?; + (addr, port) + } + None => (address, 25565), + }; + let mut builder = async_minecraft_ping::ConnectionConfig::build(address) + .with_port(port) .with_srv_lookup(); if let Some(version) = protocol_version { builder = builder.with_protocol_version(version.version as usize) } - let conn = builder - .connect() - .await - .wrap_err("failed to connect to server")?; + let conn = builder.connect().await.map_err(|_err| { + Error::from(ErrorKind::InputError("failed to connect to server".into())) + })?; - let ping_conn = conn - .status() - .await - .wrap_err("failed to get server status")?; + let ping_conn = conn.status().await.map_err(|_err| { + Error::from(ErrorKind::InputError("failed to get server status".into())) + })?; let status = &ping_conn.status; - let description = status.description.as_ref().map(|d| { - let json = - serde_json::to_string(d).expect("serializing should not fail"); - RawValue::from_string(json) - .expect("converting to `RawValue` should not fail") - }); + let description = match &status.description { + ServerDescription::Plain(text) => { + serde_json::value::to_raw_value(&text).ok() + } + ServerDescription::Object { text } => { + // TODO: `text` always seems to be empty? + RawValue::from_string(text.clone()).ok() + } + }; let players = ServerPlayers { max: status.players.max, @@ -986,10 +1000,9 @@ async fn get_server_status_new( let latency = { let start = Instant::now(); let ping_magic = Utc::now().timestamp_millis().cast_unsigned(); - ping_conn - .ping(ping_magic) - .await - .wrap_err("failed to do ping")?; + ping_conn.ping(ping_magic).await.map_err(|_err| { + Error::from(ErrorKind::InputError("failed to do ping".into())) + })?; start.elapsed().as_millis() as i64 }; diff --git a/packages/app-lib/src/error.rs b/packages/app-lib/src/error.rs index 0b734c5d0..f18bf3a44 100644 --- a/packages/app-lib/src/error.rs +++ b/packages/app-lib/src/error.rs @@ -1,9 +1,5 @@ //! Theseus error type -use std::{ - convert::Infallible, - fmt::{Debug, Display}, - sync::Arc, -}; +use std::sync::Arc; use crate::{profile, util}; use data_url::DataUrlError; @@ -224,41 +220,4 @@ impl ErrorKind { } } -pub type Result = core::result::Result; - -pub trait Context: Sized { - fn wrap_err_with(self, f: impl FnOnce() -> D) -> Result - where - D: Send + Sync + Debug + Display + 'static; - - #[inline] - fn wrap_err(self, msg: D) -> Result - where - D: Send + Sync + Debug + Display + 'static, - { - self.wrap_err_with(|| msg) - } -} - -impl Context for Result -where - Self: eyre::WrapErr, -{ - fn wrap_err_with(self, f: impl FnOnce() -> D) -> Result - where - D: Send + Sync + Debug + Display + 'static, - { - eyre::WrapErr::wrap_err_with(self, f).map_err(|err| { - Error::from(ErrorKind::OtherError(format!("{err:#}"))) - }) - } -} - -impl Context for Option { - fn wrap_err_with(self, f: impl FnOnce() -> D) -> Result - where - D: Send + Sync + Debug + Display + 'static, - { - self.ok_or_else(|| Error::from(ErrorKind::OtherError(f().to_string()))) - } -} +pub type Result = core::result::Result; diff --git a/packages/app-lib/src/util/io.rs b/packages/app-lib/src/util/io.rs index 4ec3b3975..7bdc358f6 100644 --- a/packages/app-lib/src/util/io.rs +++ b/packages/app-lib/src/util/io.rs @@ -318,7 +318,7 @@ macro_rules! get_resource_file { Ok(dir) => dir, Err(e) => { break 'get_resource_file $crate::Result::Err( - $crate::Error::from($crate::util::io::IOError::from(e)), + $crate::util::io::IOError::from(e).into(), ); } }; diff --git a/packages/app-lib/src/util/server_ping/imp.rs b/packages/app-lib/src/util/server_ping.rs similarity index 86% rename from packages/app-lib/src/util/server_ping/imp.rs rename to packages/app-lib/src/util/server_ping.rs index 6696d8a07..d03991fbc 100644 --- a/packages/app-lib/src/util/server_ping/imp.rs +++ b/packages/app-lib/src/util/server_ping.rs @@ -1,3 +1,53 @@ +use crate::ErrorKind; +use crate::error::Result; +use crate::util::protocol_version::ProtocolVersion; +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; +use std::time::Duration; +use tokio::net::ToSocketAddrs; +use tokio::select; +use url::Url; + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ServerStatus { + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub players: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub favicon: Option, + #[serde(default)] + pub enforces_secure_chat: bool, + + #[serde(skip_serializing_if = "Option::is_none")] + pub ping: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct ServerPlayers { + pub max: i32, + pub online: i32, + #[serde(default)] + pub sample: Vec, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct ServerGameProfile { + pub id: String, + pub name: String, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct ServerVersion { + pub name: String, + pub protocol: i32, + #[serde(skip_deserializing)] + pub legacy: bool, +} + pub async fn get_server_status( address: &impl ToSocketAddrs, original_address: (&str, u16), @@ -255,8 +305,8 @@ mod legacy { }), description: parts.next().and_then(|x| to_raw_value(x).ok()), players: Some(ServerPlayers { - online: parts.next().and_then(|x| x.parse().ok()), - max: parts.next().and_then(|x| x.parse().ok()), + online: parts.next().and_then(|x| x.parse().ok()).unwrap_or(-1), + max: parts.next().and_then(|x| x.parse().ok()).unwrap_or(-1), sample: vec![], }), favicon: None, diff --git a/packages/app-lib/src/util/server_ping/mod.rs b/packages/app-lib/src/util/server_ping/mod.rs deleted file mode 100644 index 40ad314da..000000000 --- a/packages/app-lib/src/util/server_ping/mod.rs +++ /dev/null @@ -1,43 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serde_json::value::RawValue; -use url::Url; - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct ServerStatus { - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub players: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub version: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub favicon: Option, - #[serde(default)] - pub enforces_secure_chat: bool, - - #[serde(skip_serializing_if = "Option::is_none")] - pub ping: Option, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct ServerPlayers { - pub max: Option, - pub online: Option, - #[serde(default)] - pub sample: Vec, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct ServerGameProfile { - pub id: String, - pub name: String, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct ServerVersion { - pub name: String, - pub protocol: i32, - #[serde(skip_deserializing)] - pub legacy: bool, -} diff --git a/packages/async-minecraft-ping/examples/status.rs b/packages/async-minecraft-ping/examples/status.rs index 14308f02c..3715926e7 100644 --- a/packages/async-minecraft-ping/examples/status.rs +++ b/packages/async-minecraft-ping/examples/status.rs @@ -37,12 +37,10 @@ async fn main() -> Result<()> { let connection = connection.status().await?; - if let (Some(online), Some(max)) = ( - connection.status.players.online, - connection.status.players.max, - ) { - println!("{online} of {max} player(s) online"); - } + println!( + "{} of {} player(s) online", + connection.status.players.online, connection.status.players.max + ); connection.ping(42).await?; diff --git a/packages/async-minecraft-ping/src/lib.rs b/packages/async-minecraft-ping/src/lib.rs index 4fc6adc00..5cd0650a3 100644 --- a/packages/async-minecraft-ping/src/lib.rs +++ b/packages/async-minecraft-ping/src/lib.rs @@ -1,6 +1,6 @@ mod protocol; mod server; pub use server::{ - connect, ConnectionConfig, ServerError, ServerPlayer, ServerPlayers, ServerVersion, - StatusConnection, StatusResponse, + connect, ConnectionConfig, ServerDescription, ServerError, ServerPlayer, ServerPlayers, + ServerVersion, StatusConnection, StatusResponse, }; diff --git a/packages/async-minecraft-ping/src/server.rs b/packages/async-minecraft-ping/src/server.rs index 8765b5b7a..03d3f184b 100644 --- a/packages/async-minecraft-ping/src/server.rs +++ b/packages/async-minecraft-ping/src/server.rs @@ -3,7 +3,7 @@ use std::time::Duration; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use thiserror::Error; use tokio::net::TcpStream; @@ -34,7 +34,7 @@ impl From for ServerError { } /// Contains information about the server version. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Deserialize)] pub struct ServerVersion { /// The server's Minecraft version, i.e. "1.15.2". pub name: String, @@ -44,7 +44,7 @@ pub struct ServerVersion { } /// Contains information about a player. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Deserialize)] pub struct ServerPlayer { /// The player's in-game name. pub name: String, @@ -55,33 +55,40 @@ pub struct ServerPlayer { /// Contains information about the currently online /// players. -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Deserialize)] pub struct ServerPlayers { /// The configured maximum number of players for the /// server. - pub max: Option, + pub max: i32, /// The number of players currently online. - pub online: Option, + pub online: i32, /// An optional list of player information for /// currently online players. pub sample: Option>, } +/// Contains the server's MOTD. +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum ServerDescription { + Plain(String), + Object { text: String }, +} + /// The decoded JSON response from a status query over /// ServerListPing. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Deserialize)] pub struct StatusResponse { /// Information about the server's version. pub version: ServerVersion, /// Information about currently online players. - #[serde(default)] pub players: ServerPlayers, /// Single-field struct containing the server's MOTD. - pub description: Option, + pub description: ServerDescription, /// Optional field containing a path to the server's /// favicon. @@ -97,7 +104,7 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_secs(2); pub struct ConnectionConfig { protocol_version: usize, address: String, - port_override: Option, + port: u16, timeout: Duration, #[cfg(feature = "srv")] srv_lookup: bool, @@ -106,19 +113,11 @@ pub struct ConnectionConfig { impl ConnectionConfig { /// Initiates the Minecraft server /// connection build process. - pub fn build(address: impl AsRef) -> Self { - let (address, port_override) = match address.as_ref().rsplit_once(':') { - Some((addr, port)) => match port.parse::() { - Ok(port) => (addr, Some(port)), - Err(_) => (addr, None), - }, - None => (address.as_ref(), None), - }; - + pub fn build>(address: T) -> Self { ConnectionConfig { protocol_version: LATEST_PROTOCOL_VERSION, - address: address.to_string(), - port_override, + address: address.into(), + port: DEFAULT_PORT, timeout: DEFAULT_TIMEOUT, #[cfg(feature = "srv")] srv_lookup: false, @@ -138,7 +137,7 @@ impl ConnectionConfig { /// connection to use. If not specified, the /// default port of 25565 will be used. pub fn with_port(mut self, port: u16) -> Self { - self.port_override = Some(port); + self.port = port; self } @@ -166,8 +165,7 @@ impl ConnectionConfig { /// Connects to the server and consumes the builder. pub async fn connect(self) -> Result { - let (address, resolved_port) = self.resolve_address().await; - let port = self.port_override.or(resolved_port).unwrap_or(DEFAULT_PORT); + let (address, port) = self.resolve_address().await; let stream = tokio::time::timeout( self.timeout, @@ -187,43 +185,43 @@ impl ConnectionConfig { } #[cfg(feature = "srv")] - async fn resolve_address(&self) -> (String, Option) { + async fn resolve_address(&self) -> (String, u16) { if !self.srv_lookup { - return (self.address.clone(), None); + return (self.address.clone(), self.port); } // Try to resolve SRV record, fall back to original address on any failure - match lookup_srv(&self.address, self.timeout).await { - Some((host, port)) => (host, Some(port)), - None => (self.address.clone(), None), + match self.lookup_srv().await { + Some((host, port)) => (host, port), + None => (self.address.clone(), self.port), } } #[cfg(not(feature = "srv"))] - async fn resolve_address(&self) -> (String, Option) { - (self.address.clone(), None) + async fn resolve_address(&self) -> (String, u16) { + (self.address.clone(), self.port) } -} -#[cfg(feature = "srv")] -async fn lookup_srv(address: &str, timeout: Duration) -> Option<(String, u16)> { - use hickory_resolver::TokioAsyncResolver; + #[cfg(feature = "srv")] + async fn lookup_srv(&self) -> Option<(String, u16)> { + use hickory_resolver::TokioAsyncResolver; - let resolver = TokioAsyncResolver::tokio_from_system_conf().ok()?; - let srv_name = format!("_minecraft._tcp.{address}"); + let resolver = TokioAsyncResolver::tokio_from_system_conf().ok()?; + let srv_name = format!("_minecraft._tcp.{}", self.address); - let lookup = tokio::time::timeout(timeout, resolver.srv_lookup(&srv_name)) - .await - .ok()? - .ok()?; + let lookup = tokio::time::timeout(self.timeout, resolver.srv_lookup(&srv_name)) + .await + .ok()? + .ok()?; - let record = lookup.iter().next()?; - let target = record.target().to_string(); - // Remove trailing dot from DNS name - let host = target.trim_end_matches('.').to_string(); - let port = record.port(); + let record = lookup.iter().next()?; + let target = record.target().to_string(); + // Remove trailing dot from DNS name + let host = target.trim_end_matches('.').to_string(); + let port = record.port(); - Some((host, port)) + Some((host, port)) + } } /// Convenience wrapper for easily connecting @@ -328,23 +326,21 @@ mod tests { use super::*; #[test] - fn test_status_response_minimal() { - let json = r#"{ - "version": {"name": "1.20.4", "protocol": 765} - }"#; - - let response: StatusResponse = serde_json::from_str(json).unwrap(); - assert_eq!(response.version.name, "1.20.4"); - assert_eq!(response.version.protocol, 765); - assert_eq!(response.players.max, None); - assert_eq!(response.players.online, None); - assert!(response.description.is_none()); - assert!(response.players.sample.is_none()); - assert!(response.favicon.is_none()); + fn test_server_description_plain() { + let json = r#""A Minecraft Server""#; + let desc: ServerDescription = serde_json::from_str(json).unwrap(); + assert!(matches!(desc, ServerDescription::Plain(s) if s == "A Minecraft Server")); } #[test] - fn test_status_response_small() { + fn test_server_description_object() { + let json = r#"{"text":"A Minecraft Server"}"#; + let desc: ServerDescription = serde_json::from_str(json).unwrap(); + assert!(matches!(desc, ServerDescription::Object { text } if text == "A Minecraft Server")); + } + + #[test] + fn test_status_response_minimal() { let json = r#"{ "version": {"name": "1.20.4", "protocol": 765}, "players": {"max": 20, "online": 5}, @@ -354,8 +350,8 @@ mod tests { let response: StatusResponse = serde_json::from_str(json).unwrap(); assert_eq!(response.version.name, "1.20.4"); assert_eq!(response.version.protocol, 765); - assert_eq!(response.players.max, Some(20)); - assert_eq!(response.players.online, Some(5)); + assert_eq!(response.players.max, 20); + assert_eq!(response.players.online, 5); assert!(response.players.sample.is_none()); assert!(response.favicon.is_none()); } @@ -400,21 +396,15 @@ mod tests { fn test_connection_config_defaults() { let config = ConnectionConfig::build("localhost"); assert_eq!(config.address, "localhost"); - assert_eq!(config.port_override, None); + assert_eq!(config.port, DEFAULT_PORT); assert_eq!(config.timeout, DEFAULT_TIMEOUT); assert_eq!(config.protocol_version, LATEST_PROTOCOL_VERSION); } - #[test] - fn test_connection_config_with_port_in_address() { - let config = ConnectionConfig::build("localhost:12345"); - assert_eq!(config.port_override, Some(12345)); - } - #[test] fn test_connection_config_with_port() { let config = ConnectionConfig::build("localhost").with_port(12345); - assert_eq!(config.port_override, Some(12345)); + assert_eq!(config.port, 12345); } #[test] diff --git a/packages/async-minecraft-ping/tests/integration.rs b/packages/async-minecraft-ping/tests/integration.rs index 8ee7d38b8..58d81e950 100644 --- a/packages/async-minecraft-ping/tests/integration.rs +++ b/packages/async-minecraft-ping/tests/integration.rs @@ -250,8 +250,8 @@ async fn test_status_json_parsing_plain_description() { let response: StatusResponse = serde_json::from_str(json).unwrap(); assert_eq!(response.version.name, "1.20.4"); assert_eq!(response.version.protocol, 765); - assert_eq!(response.players.max, Some(100)); - assert_eq!(response.players.online, Some(42)); + assert_eq!(response.players.max, 100); + assert_eq!(response.players.online, 42); } #[tokio::test] @@ -267,7 +267,7 @@ async fn test_status_json_parsing_object_description() { let response: StatusResponse = serde_json::from_str(json).unwrap(); assert_eq!(response.version.name, "1.19.4"); - assert_eq!(response.players.online, Some(10)); + assert_eq!(response.players.online, 10); assert!(response.players.sample.is_some()); assert_eq!(response.players.sample.as_ref().unwrap().len(), 1); assert_eq!(response.players.sample.as_ref().unwrap()[0].name, "Notch");