use crate::error::PingError;
use crate::protocol;
use serde::Deserialize;
use std::time::{Duration, Instant};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
#[derive(Debug, Clone)]
pub struct PingResult {
pub description: String,
pub players_online: i32,
pub players_max: i32,
pub version_name: String,
pub version_protocol: i32,
pub latency_ms: u64,
pub raw_json: String,
}
#[derive(Debug, Deserialize)]
struct StatusResponse {
version: VersionInfo,
players: PlayersInfo,
description: serde_json::Value,
#[serde(default)]
#[allow(dead_code)]
favicon: Option<String>,
}
#[derive(Debug, Deserialize)]
struct VersionInfo {
name: String,
protocol: i32,
}
#[derive(Debug, Deserialize)]
struct PlayersInfo {
max: i32,
online: i32,
#[serde(default)]
#[allow(dead_code)]
sample: Vec<PlayerSample>,
}
#[derive(Debug, Deserialize)]
struct PlayerSample {
#[allow(dead_code)]
name: String,
#[allow(dead_code)]
id: String,
}
fn extract_motd_text(description: &serde_json::Value) -> String {
match description {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Object(obj) => {
if let Some(extra) = obj.get("extra") {
if let Some(arr) = extra.as_array() {
let parts: Vec<String> = arr
.iter()
.filter_map(|e| e.get("text").and_then(|t| t.as_str()).map(|s| s.to_string()))
.collect();
if !parts.is_empty() {
return parts.join("");
}
}
}
obj.get("text")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| serde_json::to_string(description).unwrap_or_default())
}
_ => serde_json::to_string(description).unwrap_or_default(),
}
}
pub async fn ping(
address: &str,
port: u16,
timeout: Duration,
protocol_version: Option<i32>,
) -> Result<PingResult, PingError> {
let protocol_ver = protocol_version.unwrap_or(protocol::DEFAULT_PROTOCOL_VERSION);
let start = Instant::now();
let addr = format!("{}:{}", address, port);
let mut stream = tokio::time::timeout(timeout, TcpStream::connect(&addr))
.await
.map_err(|_| PingError::Timeout)?
.map_err(PingError::Connection)?;
let handshake = protocol::build_handshake_packet(protocol_ver, address, port, 1);
tokio::time::timeout(timeout, stream.write_all(&handshake))
.await
.map_err(|_| PingError::Timeout)?
.map_err(PingError::Connection)?;
let status_req = protocol::build_status_request_packet();
tokio::time::timeout(timeout, stream.write_all(&status_req))
.await
.map_err(|_| PingError::Timeout)?
.map_err(PingError::Connection)?;
let mut response_buf = vec![0u8; 65536];
let total_read = tokio::time::timeout(timeout, stream.read(&mut response_buf))
.await
.map_err(|_| PingError::Timeout)?
.map_err(PingError::Connection)?;
if total_read == 0 {
return Err(PingError::Protocol("connection closed with no response".to_string()));
}
let latency_ms = start.elapsed().as_millis() as u64;
let (json_str, _consumed) = protocol::parse_status_response(&response_buf[..total_read])?;
let status: StatusResponse = serde_json::from_str(&json_str)?;
let description = extract_motd_text(&status.description);
Ok(PingResult {
description,
players_online: status.players.online,
players_max: status.players.max,
version_name: status.version.name,
version_protocol: status.version.protocol,
latency_ms,
raw_json: json_str,
})
}