rust_mc_status/client.rs
1//! Client for querying Minecraft server status.
2//!
3//! This module provides the [`McClient`] struct for performing server status queries.
4//! It supports both Java Edition and Bedrock Edition servers with automatic SRV record
5//! lookup, DNS caching, and batch queries.
6//!
7//! # Examples
8//!
9//! ## Basic Usage
10//!
11//! ```no_run
12//! use rust_mc_status::{McClient, ServerEdition};
13//! use std::time::Duration;
14//!
15//! # #[tokio::main]
16//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
17//! let client = McClient::new()
18//! .with_timeout(Duration::from_secs(5))
19//! .with_max_parallel(10);
20//!
21//! // Ping a Java server (automatically uses SRV lookup if port not specified)
22//! let status = client.ping("mc.hypixel.net", ServerEdition::Java).await?;
23//! println!("Server is online: {}", status.online);
24//! # Ok(())
25//! # }
26//! ```
27//!
28//! ## Batch Queries
29//!
30//! ```no_run
31//! use rust_mc_status::{McClient, ServerEdition, ServerInfo};
32//!
33//! # #[tokio::main]
34//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
35//! let client = McClient::new();
36//! let servers = vec![
37//! ServerInfo {
38//! address: "mc.hypixel.net".to_string(),
39//! edition: ServerEdition::Java,
40//! },
41//! ServerInfo {
42//! address: "geo.hivebedrock.network:19132".to_string(),
43//! edition: ServerEdition::Bedrock,
44//! },
45//! ];
46//!
47//! let results = client.ping_many(&servers).await;
48//! for (server, result) in results {
49//! match result {
50//! Ok(status) => println!("{}: Online ({}ms)", server.address, status.latency),
51//! Err(e) => println!("{}: Error - {}", server.address, e),
52//! }
53//! }
54//! # Ok(())
55//! # }
56//! ```
57//!
58//! ## SRV Record Lookup
59//!
60//! When pinging Java servers without an explicit port, the library automatically
61//! performs an SRV DNS lookup for `_minecraft._tcp.{hostname}`. This mimics the
62//! behavior of the official Minecraft client.
63//!
64//! ```no_run
65//! use rust_mc_status::{McClient, ServerEdition};
66//!
67//! # #[tokio::main]
68//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
69//! let client = McClient::new();
70//!
71//! // This will perform SRV lookup for _minecraft._tcp.example.com
72//! let status = client.ping("example.com", ServerEdition::Java).await?;
73//!
74//! // This will skip SRV lookup and use port 25565 directly
75//! let status = client.ping("example.com:25565", ServerEdition::Java).await?;
76//! # Ok(())
77//! # }
78//! ```
79
80use std::io::Cursor;
81use std::net::{SocketAddr, ToSocketAddrs};
82use std::sync::Arc;
83use std::time::{Duration, SystemTime};
84
85use dashmap::DashMap;
86use once_cell::sync::Lazy;
87use tokio::io::{AsyncReadExt, AsyncWriteExt};
88use tokio::net::{TcpStream, UdpSocket};
89use tokio::sync::OnceCell;
90use tokio::time::timeout;
91use trust_dns_resolver::{
92 config::{ResolverConfig, ResolverOpts},
93 TokioAsyncResolver,
94};
95
96use crate::error::McError;
97use crate::models::*;
98
99/// Default timeout for server queries (10 seconds).
100const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
101
102/// Default maximum number of parallel queries (10).
103const DEFAULT_MAX_PARALLEL: usize = 10;
104
105/// DNS cache TTL in seconds (5 minutes).
106const DNS_CACHE_TTL: u64 = 300;
107
108/// Default Java Edition port.
109const JAVA_DEFAULT_PORT: u16 = 25565;
110
111/// Default Bedrock Edition port.
112const BEDROCK_DEFAULT_PORT: u16 = 19132;
113
114/// Protocol version for status requests (legacy protocol).
115const PROTOCOL_VERSION: i32 = 47;
116
117/// Handshake packet ID.
118const HANDSHAKE_PACKET_ID: i32 = 0x00;
119
120/// Status request packet ID.
121const STATUS_REQUEST_PACKET_ID: i32 = 0x00;
122
123/// Status response packet ID.
124const STATUS_RESPONSE_PACKET_ID: i32 = 0x00;
125
126/// Maximum VarInt size (35 bits).
127const MAX_VARINT_SHIFT: u32 = 35;
128
129/// Initial buffer capacity for packets.
130const INITIAL_PACKET_CAPACITY: usize = 64;
131
132/// Initial response buffer capacity.
133const INITIAL_RESPONSE_CAPACITY: usize = 1024;
134
135/// Read buffer size.
136const READ_BUFFER_SIZE: usize = 4096;
137
138/// Bedrock ping packet size.
139const BEDROCK_PING_PACKET_SIZE: usize = 35;
140
141/// Minimum Bedrock response size.
142const BEDROCK_MIN_RESPONSE_SIZE: usize = 35;
143
144/// DNS resolution cache.
145///
146/// Maps `"{host}:{port}"` to `(SocketAddr, timestamp)`.
147static DNS_CACHE: Lazy<DashMap<String, (SocketAddr, SystemTime)>> = Lazy::new(DashMap::new);
148
149/// SRV record cache.
150///
151/// Maps `"srv:{host}"` to `((target_host, port), timestamp)`.
152static SRV_CACHE: Lazy<DashMap<String, ((String, u16), SystemTime)>> = Lazy::new(DashMap::new);
153
154/// Global DNS resolver for SRV lookups.
155///
156/// Initialized lazily on first use and reused for all subsequent lookups.
157/// This improves performance by avoiding resolver creation overhead.
158static RESOLVER: OnceCell<Arc<TokioAsyncResolver>> = OnceCell::const_new();
159
160/// Initialize the global DNS resolver.
161///
162/// Creates a resolver with default configuration optimized for performance.
163async fn get_resolver() -> Arc<TokioAsyncResolver> {
164 RESOLVER
165 .get_or_init(|| async {
166 let config = ResolverConfig::default();
167 let mut opts = ResolverOpts::default();
168 // Optimize for performance
169 opts.cache_size = 1000;
170 opts.positive_min_ttl = Some(Duration::from_secs(60));
171 opts.negative_min_ttl = Some(Duration::from_secs(10));
172
173 Arc::new(TokioAsyncResolver::tokio(config, opts))
174 })
175 .await
176 .clone()
177}
178
179/// Minecraft server status client.
180///
181/// This client provides methods for querying Minecraft server status with
182/// support for both Java Edition and Bedrock Edition servers.
183///
184/// # Features
185///
186/// - Automatic SRV record lookup for Java servers
187/// - DNS caching for improved performance
188/// - Configurable timeouts and concurrency limits
189/// - Batch queries for multiple servers
190///
191/// # Example
192///
193/// ```no_run
194/// use rust_mc_status::{McClient, ServerEdition};
195/// use std::time::Duration;
196///
197/// # #[tokio::main]
198/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
199/// let client = McClient::new()
200/// .with_timeout(Duration::from_secs(5))
201/// .with_max_parallel(10);
202///
203/// let status = client.ping("mc.hypixel.net", ServerEdition::Java).await?;
204/// println!("Server is online: {}", status.online);
205/// # Ok(())
206/// # }
207/// ```
208#[derive(Clone)]
209pub struct McClient {
210 /// Request timeout.
211 timeout: Duration,
212 /// Maximum number of parallel queries.
213 max_parallel: usize,
214}
215
216impl Default for McClient {
217 fn default() -> Self {
218 Self {
219 timeout: DEFAULT_TIMEOUT,
220 max_parallel: DEFAULT_MAX_PARALLEL,
221 }
222 }
223}
224
225impl McClient {
226 /// Create a new client with default settings.
227 ///
228 /// Default settings:
229 /// - Timeout: 10 seconds
230 /// - Max parallel: 10 queries
231 ///
232 /// # Example
233 ///
234 /// ```no_run
235 /// use rust_mc_status::McClient;
236 ///
237 /// let client = McClient::new();
238 /// ```
239 #[must_use]
240 pub fn new() -> Self {
241 Self::default()
242 }
243
244 /// Set the request timeout.
245 ///
246 /// This timeout applies to all network operations including DNS resolution,
247 /// connection establishment, and response reading.
248 ///
249 /// # Arguments
250 ///
251 /// * `timeout` - Maximum time to wait for a server response
252 /// - Default: 10 seconds
253 /// - Recommended: 5-10 seconds for most use cases
254 /// - Use shorter timeouts (1-3 seconds) for quick checks
255 ///
256 /// # Returns
257 ///
258 /// Returns `Self` for method chaining.
259 ///
260 /// # Example
261 ///
262 /// ```no_run
263 /// use rust_mc_status::McClient;
264 /// use std::time::Duration;
265 ///
266 /// // Quick timeout for fast checks
267 /// let fast_client = McClient::new()
268 /// .with_timeout(Duration::from_secs(2));
269 ///
270 /// // Longer timeout for reliable queries
271 /// let reliable_client = McClient::new()
272 /// .with_timeout(Duration::from_secs(10));
273 /// ```
274 #[must_use]
275 pub fn with_timeout(mut self, timeout: Duration) -> Self {
276 self.timeout = timeout;
277 self
278 }
279
280 /// Set the maximum number of parallel queries.
281 ///
282 /// This limit controls how many servers can be queried simultaneously
283 /// when using [`ping_many`](Self::ping_many). Higher values increase
284 /// throughput but consume more resources.
285 ///
286 /// # Arguments
287 ///
288 /// * `max_parallel` - Maximum number of concurrent server queries
289 /// - Default: 10
290 /// - Recommended: 5-20 for most use cases
291 /// - Higher values (50-100) for batch processing
292 /// - Lower values (1-5) for resource-constrained environments
293 ///
294 /// # Returns
295 ///
296 /// Returns `Self` for method chaining.
297 ///
298 /// # Performance
299 ///
300 /// - Higher values = faster batch processing but more memory/CPU usage
301 /// - Lower values = slower but more resource-friendly
302 /// - DNS and SRV caches are shared across all parallel queries
303 ///
304 /// # Example
305 ///
306 /// ```no_run
307 /// use rust_mc_status::{McClient, ServerEdition, ServerInfo};
308 ///
309 /// # #[tokio::main]
310 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
311 /// // High concurrency for batch processing
312 /// let batch_client = McClient::new()
313 /// .with_max_parallel(50);
314 ///
315 /// // Low concurrency for resource-constrained environments
316 /// let limited_client = McClient::new()
317 /// .with_max_parallel(3);
318 /// # Ok(())
319 /// # }
320 /// ```
321 #[must_use]
322 pub fn with_max_parallel(mut self, max_parallel: usize) -> Self {
323 self.max_parallel = max_parallel;
324 self
325 }
326
327 /// Get the maximum number of parallel queries.
328 ///
329 /// # Example
330 ///
331 /// ```no_run
332 /// use rust_mc_status::McClient;
333 ///
334 /// let client = McClient::new().with_max_parallel(20);
335 /// assert_eq!(client.max_parallel(), 20);
336 /// ```
337 #[must_use]
338 pub fn max_parallel(&self) -> usize {
339 self.max_parallel
340 }
341
342 /// Get the request timeout.
343 ///
344 /// # Example
345 ///
346 /// ```no_run
347 /// use rust_mc_status::McClient;
348 /// use std::time::Duration;
349 ///
350 /// let client = McClient::new().with_timeout(Duration::from_secs(5));
351 /// assert_eq!(client.timeout(), Duration::from_secs(5));
352 /// ```
353 #[must_use]
354 pub fn timeout(&self) -> Duration {
355 self.timeout
356 }
357
358 /// Clear DNS cache.
359 ///
360 /// This method clears all cached DNS resolutions. Useful when you need to
361 /// force fresh DNS lookups or free memory.
362 ///
363 /// # Example
364 ///
365 /// ```no_run
366 /// use rust_mc_status::McClient;
367 ///
368 /// # #[tokio::main]
369 /// # async fn main() {
370 /// let client = McClient::new();
371 /// client.clear_dns_cache();
372 /// # }
373 /// ```
374 pub fn clear_dns_cache(&self) {
375 DNS_CACHE.clear();
376 }
377
378 /// Clear SRV record cache.
379 ///
380 /// This method clears all cached SRV record lookups. Useful when you need to
381 /// force fresh SRV lookups or free memory.
382 ///
383 /// # Example
384 ///
385 /// ```no_run
386 /// use rust_mc_status::McClient;
387 ///
388 /// # #[tokio::main]
389 /// # async fn main() {
390 /// let client = McClient::new();
391 /// client.clear_srv_cache();
392 /// # }
393 /// ```
394 pub fn clear_srv_cache(&self) {
395 SRV_CACHE.clear();
396 }
397
398 /// Clear all caches (DNS and SRV).
399 ///
400 /// This method clears both DNS and SRV caches. Useful when you need to
401 /// force fresh lookups or free memory.
402 ///
403 /// # Example
404 ///
405 /// ```no_run
406 /// use rust_mc_status::McClient;
407 ///
408 /// # #[tokio::main]
409 /// # async fn main() {
410 /// let client = McClient::new();
411 /// client.clear_all_caches();
412 /// # }
413 /// ```
414 pub fn clear_all_caches(&self) {
415 self.clear_dns_cache();
416 self.clear_srv_cache();
417 }
418
419 /// Get cache statistics.
420 ///
421 /// Returns the number of entries in DNS and SRV caches.
422 ///
423 /// # Example
424 ///
425 /// ```no_run
426 /// use rust_mc_status::McClient;
427 ///
428 /// # #[tokio::main]
429 /// # async fn main() {
430 /// let client = McClient::new();
431 /// let stats = client.cache_stats();
432 /// println!("DNS cache entries: {}", stats.dns_entries);
433 /// println!("SRV cache entries: {}", stats.srv_entries);
434 /// # }
435 /// ```
436 #[must_use]
437 pub fn cache_stats(&self) -> CacheStats {
438 CacheStats {
439 dns_entries: DNS_CACHE.len(),
440 srv_entries: SRV_CACHE.len(),
441 }
442 }
443
444 /// Resolve DNS and measure resolution time.
445 ///
446 /// This method resolves a hostname to an IP address and returns both
447 /// the resolved address and the time taken for resolution. Useful for
448 /// measuring cache effectiveness and DNS performance.
449 ///
450 /// # Arguments
451 ///
452 /// * `host` - Hostname to resolve (e.g., `"mc.hypixel.net"` or `"192.168.1.1"`)
453 /// * `port` - Port number (e.g., `25565` for Java, `19132` for Bedrock)
454 ///
455 /// # Returns
456 ///
457 /// Returns `Ok((SocketAddr, f64))` where:
458 /// - `SocketAddr`: Resolved IP address and port
459 /// - `f64`: Resolution time in milliseconds
460 ///
461 /// # Errors
462 ///
463 /// Returns [`McError::DnsError`] if DNS resolution fails.
464 ///
465 /// # Performance Notes
466 ///
467 /// - First resolution (cold cache): Typically 10-100ms
468 /// - Subsequent resolutions (warm cache): Typically <1ms
469 /// - Cache TTL: 5 minutes
470 /// - Use [`clear_dns_cache`](Self::clear_dns_cache) to force fresh lookups
471 ///
472 /// # Example
473 ///
474 /// ```no_run
475 /// use rust_mc_status::McClient;
476 ///
477 /// # #[tokio::main]
478 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
479 /// let client = McClient::new();
480 ///
481 /// // First resolution (cold cache - actual DNS lookup)
482 /// let (addr1, time1) = client.resolve_dns_timed("mc.hypixel.net", 25565).await?;
483 /// println!("First resolution: {:.2} ms", time1);
484 /// println!("Resolved to: {}", addr1);
485 ///
486 /// // Second resolution (warm cache - from cache)
487 /// let (addr2, time2) = client.resolve_dns_timed("mc.hypixel.net", 25565).await?;
488 /// println!("Cached resolution: {:.2} ms", time2);
489 /// println!("Cache speedup: {:.1}x faster", time1 / time2);
490 /// println!("Time saved: {:.2} ms", time1 - time2);
491 /// # Ok(())
492 /// # }
493 /// ```
494 pub async fn resolve_dns_timed(&self, host: &str, port: u16) -> Result<(SocketAddr, f64), McError> {
495 let start = std::time::Instant::now();
496 let addr = self.resolve_dns(host, port).await?;
497 let elapsed = start.elapsed().as_secs_f64() * 1000.0;
498 Ok((addr, elapsed))
499 }
500
501 /// Ping a server and get its status.
502 ///
503 /// This is a convenience method that dispatches to either [`ping_java`](Self::ping_java)
504 /// or [`ping_bedrock`](Self::ping_bedrock) based on the server edition.
505 ///
506 /// # Arguments
507 ///
508 /// * `address` - Server address in one of the following formats:
509 /// - `"hostname"` - Hostname without port (uses default port for edition)
510 /// - `"hostname:port"` - Hostname with explicit port
511 /// - `"192.168.1.1"` - IP address without port (uses default port)
512 /// - `"192.168.1.1:25565"` - IP address with explicit port
513 /// * `edition` - Server edition: `ServerEdition::Java` or `ServerEdition::Bedrock`
514 ///
515 /// # Returns
516 ///
517 /// Returns a [`ServerStatus`] struct containing:
518 /// - `online`: Whether the server is online
519 /// - `ip`: Resolved IP address
520 /// - `port`: Server port number
521 /// - `hostname`: Original hostname used for query
522 /// - `latency`: Response time in milliseconds
523 /// - `dns`: Optional DNS information (A records, CNAME, TTL)
524 /// - `data`: Edition-specific server data (version, players, plugins, etc.)
525 ///
526 /// # Errors
527 ///
528 /// Returns [`McError`] if:
529 /// - DNS resolution fails ([`McError::DnsError`])
530 /// - Connection cannot be established ([`McError::ConnectionError`])
531 /// - Request times out ([`McError::Timeout`])
532 /// - Server returns invalid response ([`McError::InvalidResponse`])
533 /// - Invalid port number in address ([`McError::InvalidPort`])
534 ///
535 /// # Example
536 ///
537 /// ```no_run
538 /// use rust_mc_status::{McClient, ServerEdition};
539 ///
540 /// # #[tokio::main]
541 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
542 /// let client = McClient::new();
543 ///
544 /// // Ping Java server (automatic SRV lookup)
545 /// let status = client.ping("mc.hypixel.net", ServerEdition::Java).await?;
546 /// println!("Server is online: {}", status.online);
547 /// println!("Latency: {:.2}ms", status.latency);
548 ///
549 /// // Ping Bedrock server
550 /// let status = client.ping("geo.hivebedrock.network:19132", ServerEdition::Bedrock).await?;
551 /// println!("Players: {}/{}", status.players().unwrap_or((0, 0)).0, status.players().unwrap_or((0, 0)).1);
552 /// # Ok(())
553 /// # }
554 /// ```
555 pub async fn ping(&self, address: &str, edition: ServerEdition) -> Result<ServerStatus, McError> {
556 match edition {
557 ServerEdition::Java => self.ping_java(address).await,
558 ServerEdition::Bedrock => self.ping_bedrock(address).await,
559 }
560 }
561
562 /// Ping a Java Edition server and get its status.
563 ///
564 /// This method automatically performs an SRV DNS lookup if no port is specified
565 /// in the address. The lookup follows the Minecraft client behavior:
566 /// - Queries `_minecraft._tcp.{hostname}` for SRV records
567 /// - Uses the target host and port from the SRV record if found
568 /// - Falls back to the default port (25565) if no SRV record exists
569 /// - Skips SRV lookup if port is explicitly specified
570 ///
571 /// # Arguments
572 ///
573 /// * `address` - Server address in one of the following formats:
574 /// - `"hostname"` - Hostname without port (performs SRV lookup, defaults to 25565)
575 /// - `"hostname:25565"` - Hostname with explicit port (skips SRV lookup)
576 /// - `"192.168.1.1"` - IP address without port (uses port 25565)
577 /// - `"192.168.1.1:25565"` - IP address with explicit port
578 ///
579 /// # Returns
580 ///
581 /// Returns a [`ServerStatus`] with `ServerData::Java` containing:
582 /// - Version information (name, protocol version)
583 /// - Player information (online count, max players, sample list)
584 /// - Server description (MOTD)
585 /// - Optional favicon (base64-encoded PNG)
586 /// - Optional plugins list
587 /// - Optional mods list
588 /// - Additional metadata (map, gamemode, software)
589 ///
590 /// # Errors
591 ///
592 /// Returns [`McError`] if:
593 /// - DNS resolution fails ([`McError::DnsError`])
594 /// - SRV lookup fails (non-critical, falls back to default port)
595 /// - Connection cannot be established ([`McError::ConnectionError`])
596 /// - Request times out ([`McError::Timeout`])
597 /// - Server returns invalid response ([`McError::InvalidResponse`])
598 /// - JSON parsing fails ([`McError::JsonError`])
599 /// - Invalid port number ([`McError::InvalidPort`])
600 ///
601 /// # Example
602 ///
603 /// ```no_run
604 /// use rust_mc_status::McClient;
605 ///
606 /// # #[tokio::main]
607 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
608 /// let client = McClient::new();
609 ///
610 /// // Automatic SRV lookup (queries _minecraft._tcp.mc.hypixel.net)
611 /// let status = client.ping_java("mc.hypixel.net").await?;
612 /// if let rust_mc_status::ServerData::Java(java) = &status.data {
613 /// println!("Version: {}", java.version.name);
614 /// println!("Players: {}/{}", java.players.online, java.players.max);
615 /// println!("Description: {}", java.description);
616 /// }
617 ///
618 /// // Skip SRV lookup (uses port 25565 directly)
619 /// let status = client.ping_java("mc.hypixel.net:25565").await?;
620 /// # Ok(())
621 /// # }
622 /// ```
623 pub async fn ping_java(&self, address: &str) -> Result<ServerStatus, McError> {
624 let start = SystemTime::now();
625 let (host, port) = Self::parse_address(address, JAVA_DEFAULT_PORT)?;
626
627 // Check for SRV record first (mimics Minecraft's server address field behavior)
628 // Only perform SRV lookup if port was not explicitly specified in address
629 let port_explicit = address.contains(':');
630 let (actual_host, actual_port) = if port_explicit {
631 // Port was explicitly specified, skip SRV lookup
632 (host.to_string(), port)
633 } else {
634 // No port specified, check for SRV record
635 self.lookup_srv_record(host, port).await?
636 };
637
638 let resolved = self.resolve_dns(&actual_host, actual_port).await?;
639 let dns_info = self.get_dns_info(host).await.ok(); // DNS info is optional
640
641 let mut stream = timeout(self.timeout, TcpStream::connect(resolved))
642 .await
643 .map_err(|_| McError::Timeout)?
644 .map_err(|e| McError::ConnectionError(e.to_string()))?;
645
646 stream.set_nodelay(true).map_err(McError::IoError)?;
647
648 // Send handshake - use original host and actual port in handshake packet
649 // (Minecraft protocol expects original hostname in handshake, but actual port)
650 self.send_handshake(&mut stream, host, actual_port).await?;
651
652 // Send status request
653 self.send_status_request(&mut stream).await?;
654
655 // Read and parse response
656 let response = self.read_response(&mut stream).await?;
657 let (json, latency) = self.parse_java_response(response, start)?;
658
659 // Build result
660 Ok(ServerStatus {
661 online: true,
662 ip: resolved.ip().to_string(),
663 port: resolved.port(),
664 hostname: host.to_string(),
665 latency,
666 dns: dns_info,
667 data: ServerData::Java(self.parse_java_json(&json)?),
668 })
669 }
670
671 /// Ping a Bedrock Edition server and get its status.
672 ///
673 /// Bedrock Edition servers use UDP protocol and typically run on port 19132.
674 /// This method sends a UDP ping packet and parses the response.
675 ///
676 /// # Arguments
677 ///
678 /// * `address` - Server address in one of the following formats:
679 /// - `"hostname"` - Hostname without port (uses default port 19132)
680 /// - `"hostname:19132"` - Hostname with explicit port
681 /// - `"192.168.1.1"` - IP address without port (uses port 19132)
682 /// - `"192.168.1.1:19132"` - IP address with explicit port
683 ///
684 /// # Returns
685 ///
686 /// Returns a [`ServerStatus`] with `ServerData::Bedrock` containing:
687 /// - Edition information (e.g., "MCPE")
688 /// - Version information
689 /// - Player counts (online and max)
690 /// - Message of the day (MOTD)
691 /// - Protocol version
692 /// - Server UID
693 /// - Game mode information
694 /// - Port information (IPv4 and IPv6)
695 /// - Optional map name and software
696 ///
697 /// # Errors
698 ///
699 /// Returns [`McError`] if:
700 /// - DNS resolution fails ([`McError::DnsError`])
701 /// - Connection cannot be established ([`McError::ConnectionError`])
702 /// - Request times out ([`McError::Timeout`])
703 /// - Server returns invalid response ([`McError::InvalidResponse`])
704 /// - Invalid port number ([`McError::InvalidPort`])
705 /// - UTF-8 conversion fails ([`McError::Utf8Error`])
706 ///
707 /// # Example
708 ///
709 /// ```no_run
710 /// use rust_mc_status::McClient;
711 ///
712 /// # #[tokio::main]
713 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
714 /// let client = McClient::new();
715 /// let status = client.ping_bedrock("geo.hivebedrock.network:19132").await?;
716 ///
717 /// if let rust_mc_status::ServerData::Bedrock(bedrock) = &status.data {
718 /// println!("Edition: {}", bedrock.edition);
719 /// println!("Version: {}", bedrock.version);
720 /// println!("Players: {}/{}", bedrock.online_players, bedrock.max_players);
721 /// println!("MOTD: {}", bedrock.motd);
722 /// }
723 /// # Ok(())
724 /// # }
725 /// ```
726 pub async fn ping_bedrock(&self, address: &str) -> Result<ServerStatus, McError> {
727 let start = SystemTime::now();
728 let (host, port) = Self::parse_address(address, BEDROCK_DEFAULT_PORT)?;
729 let resolved = self.resolve_dns(host, port).await?;
730 let dns_info = self.get_dns_info(host).await.ok(); // DNS info is optional
731
732 let socket = UdpSocket::bind("0.0.0.0:0").await.map_err(McError::IoError)?;
733
734 // Send ping packet
735 let ping_packet = self.create_bedrock_ping_packet();
736 timeout(self.timeout, socket.send_to(&ping_packet, resolved))
737 .await
738 .map_err(|_| McError::Timeout)?
739 .map_err(McError::IoError)?;
740
741 // Receive response
742 let mut buf = [0u8; READ_BUFFER_SIZE];
743 let (len, _) = timeout(self.timeout, socket.recv_from(&mut buf))
744 .await
745 .map_err(|_| McError::Timeout)?
746 .map_err(McError::IoError)?;
747
748 if len < BEDROCK_MIN_RESPONSE_SIZE {
749 return Err(McError::InvalidResponse("Response too short".to_string()));
750 }
751
752 let latency = start
753 .elapsed()
754 .map_err(|_| McError::InvalidResponse("Time error".to_string()))?
755 .as_secs_f64()
756 * 1000.0;
757
758 let pong_data = String::from_utf8_lossy(&buf[BEDROCK_PING_PACKET_SIZE..len]).to_string();
759
760 Ok(ServerStatus {
761 online: true,
762 ip: resolved.ip().to_string(),
763 port: resolved.port(),
764 hostname: host.to_string(),
765 latency,
766 dns: dns_info,
767 data: ServerData::Bedrock(self.parse_bedrock_response(&pong_data)?),
768 })
769 }
770
771 /// Quick check if a server is online.
772 ///
773 /// This method performs a minimal connection check without retrieving
774 /// full server status. It's faster than `ping()` but provides less information.
775 /// Use this method when you only need to know if a server is reachable.
776 ///
777 /// # Arguments
778 ///
779 /// * `address` - Server address in one of the following formats:
780 /// - `"hostname"` - Hostname without port (uses default port for edition)
781 /// - `"hostname:port"` - Hostname with explicit port
782 /// - `"192.168.1.1"` - IP address without port (uses default port)
783 /// - `"192.168.1.1:25565"` - IP address with explicit port
784 /// * `edition` - Server edition: `ServerEdition::Java` or `ServerEdition::Bedrock`
785 ///
786 /// # Returns
787 ///
788 /// Returns `true` if the server is reachable and responds to ping requests,
789 /// `false` if the server is offline, unreachable, or times out.
790 ///
791 /// # Performance
792 ///
793 /// This method is faster than `ping()` because it:
794 /// - Doesn't parse full server status
795 /// - Doesn't retrieve player information
796 /// - Doesn't parse JSON responses (for Java servers)
797 ///
798 /// # Example
799 ///
800 /// ```no_run
801 /// use rust_mc_status::{McClient, ServerEdition};
802 ///
803 /// # #[tokio::main]
804 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
805 /// let client = McClient::new();
806 ///
807 /// // Quick check without full status
808 /// if client.is_online("mc.hypixel.net", ServerEdition::Java).await {
809 /// println!("Server is online!");
810 /// // Now get full status if needed
811 /// let status = client.ping("mc.hypixel.net", ServerEdition::Java).await?;
812 /// println!("Players: {}/{}", status.players().unwrap_or((0, 0)).0, status.players().unwrap_or((0, 0)).1);
813 /// } else {
814 /// println!("Server is offline or unreachable");
815 /// }
816 /// # Ok(())
817 /// # }
818 /// ```
819 pub async fn is_online(&self, address: &str, edition: ServerEdition) -> bool {
820 self.ping(address, edition).await.is_ok()
821 }
822
823 /// Ping multiple servers in parallel.
824 ///
825 /// This method queries multiple servers concurrently with a configurable
826 /// concurrency limit. Results are returned in the same order as input.
827 /// The concurrency limit is controlled by [`with_max_parallel`](Self::with_max_parallel).
828 ///
829 /// # Arguments
830 ///
831 /// * `servers` - Slice of [`ServerInfo`] structures containing:
832 /// - `address`: Server address (hostname or IP, optionally with port)
833 /// - `edition`: Server edition (`ServerEdition::Java` or `ServerEdition::Bedrock`)
834 ///
835 /// # Returns
836 ///
837 /// Returns a `Vec<(ServerInfo, Result<ServerStatus, McError>)>` where:
838 /// - Each tuple contains the original `ServerInfo` and the query result
839 /// - Results are in the same order as the input slice
840 /// - `Ok(ServerStatus)` contains full server status information
841 /// - `Err(McError)` contains error information (timeout, DNS error, etc.)
842 ///
843 /// # Performance
844 ///
845 /// - Queries are executed in parallel up to the configured limit
846 /// - DNS and SRV caches are shared across all queries
847 /// - Failed queries don't block other queries
848 /// - Use `with_max_parallel()` to control resource usage
849 ///
850 /// # Example
851 ///
852 /// ```no_run
853 /// use rust_mc_status::{McClient, ServerEdition, ServerInfo};
854 ///
855 /// # #[tokio::main]
856 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
857 /// let client = McClient::new()
858 /// .with_max_parallel(5) // Process up to 5 servers concurrently
859 /// .with_timeout(std::time::Duration::from_secs(5));
860 ///
861 /// let servers = vec![
862 /// ServerInfo {
863 /// address: "mc.hypixel.net".to_string(),
864 /// edition: ServerEdition::Java,
865 /// },
866 /// ServerInfo {
867 /// address: "geo.hivebedrock.network:19132".to_string(),
868 /// edition: ServerEdition::Bedrock,
869 /// },
870 /// ];
871 ///
872 /// let results = client.ping_many(&servers).await;
873 /// for (server, result) in results {
874 /// match result {
875 /// Ok(status) => {
876 /// println!("{}: ✅ Online ({}ms)", server.address, status.latency);
877 /// if let Some((online, max)) = status.players() {
878 /// println!(" Players: {}/{}", online, max);
879 /// }
880 /// }
881 /// Err(e) => println!("{}: ❌ Error - {}", server.address, e),
882 /// }
883 /// }
884 /// # Ok(())
885 /// # }
886 /// ```
887 pub async fn ping_many(&self, servers: &[ServerInfo]) -> Vec<(ServerInfo, Result<ServerStatus, McError>)> {
888 use futures::stream::StreamExt;
889 use tokio::sync::Semaphore;
890
891 let semaphore = Arc::new(Semaphore::new(self.max_parallel));
892 let client = self.clone();
893
894 let futures = servers.iter().map(|server| {
895 let server = server.clone();
896 let semaphore = semaphore.clone();
897 let client = client.clone();
898
899 async move {
900 let _permit = semaphore.acquire().await;
901 let result = client.ping(&server.address, server.edition).await;
902 (server, result)
903 }
904 });
905
906 futures::stream::iter(futures)
907 .buffer_unordered(self.max_parallel)
908 .collect()
909 .await
910 }
911
912 // === Private Helper Methods ===
913
914 /// Parse server address into host and port.
915 ///
916 /// # Arguments
917 ///
918 /// * `address` - Server address string
919 /// * `default_port` - Default port to use if not specified
920 ///
921 /// # Returns
922 ///
923 /// Tuple of `(host, port)`.
924 ///
925 /// # Errors
926 ///
927 /// Returns an error if the port cannot be parsed as a `u16`.
928 fn parse_address(address: &str, default_port: u16) -> Result<(&str, u16), McError> {
929 if let Some((host, port_str)) = address.split_once(':') {
930 let port = port_str
931 .parse::<u16>()
932 .map_err(|e| McError::InvalidPort(e.to_string()))?;
933 Ok((host, port))
934 } else {
935 Ok((address, default_port))
936 }
937 }
938
939 /// Lookup SRV record for Minecraft Java server.
940 ///
941 /// This method mimics Minecraft's server address field behavior by querying
942 /// `_minecraft._tcp.{host}` for SRV records. Results are cached for 5 minutes.
943 ///
944 /// # Arguments
945 ///
946 /// * `host` - Hostname to lookup
947 /// * `port` - Default port to use if no SRV record is found
948 ///
949 /// # Returns
950 ///
951 /// Tuple of `(target_host, port)` from SRV record or original values.
952 ///
953 /// # Errors
954 ///
955 /// Returns an error if DNS resolution fails critically (timeouts are handled gracefully).
956 async fn lookup_srv_record(&self, host: &str, port: u16) -> Result<(String, u16), McError> {
957 let cache_key = format!("srv:{}", host);
958
959 // Check cache with TTL validation
960 if let Some(entry) = SRV_CACHE.get(&cache_key) {
961 let ((cached_host, cached_port), timestamp) = entry.value();
962 if timestamp
963 .elapsed()
964 .map(|d| d.as_secs() < DNS_CACHE_TTL)
965 .unwrap_or(false)
966 {
967 return Ok((cached_host.clone(), *cached_port));
968 }
969 }
970
971 // Build SRV record name: _minecraft._tcp.{host}
972 let srv_name = format!("_minecraft._tcp.{}", host);
973
974 // Try to resolve SRV record using cached resolver
975 let resolver = get_resolver().await;
976
977 match timeout(self.timeout, resolver.srv_lookup(&srv_name)).await {
978 Ok(Ok(srv_lookup)) => {
979 // Get the first SRV record (sorted by priority and weight)
980 if let Some(srv) = srv_lookup.iter().next() {
981 let target = srv.target().to_string().trim_end_matches('.').to_string();
982 let srv_port = srv.port();
983
984 // Cache and return SRV result
985 let result = (target, srv_port);
986 SRV_CACHE.insert(cache_key, (result.clone(), SystemTime::now()));
987 return Ok(result);
988 }
989 }
990 Ok(Err(_)) => {
991 // SRV record not found - this is normal, use original values
992 }
993 Err(_) => {
994 // Timeout - use original values
995 }
996 }
997
998 // No SRV record found or error occurred, use original host and port
999 let result = (host.to_string(), port);
1000 SRV_CACHE.insert(cache_key, (result.clone(), SystemTime::now()));
1001 Ok(result)
1002 }
1003
1004 /// Resolve hostname to IP address with caching.
1005 ///
1006 /// DNS resolutions are cached for 5 minutes to improve performance.
1007 ///
1008 /// # Arguments
1009 ///
1010 /// * `host` - Hostname to resolve
1011 /// * `port` - Port number
1012 ///
1013 /// # Returns
1014 ///
1015 /// Resolved `SocketAddr`.
1016 ///
1017 /// # Errors
1018 ///
1019 /// Returns an error if DNS resolution fails.
1020 async fn resolve_dns(&self, host: &str, port: u16) -> Result<SocketAddr, McError> {
1021 let cache_key = format!("{}:{}", host, port);
1022
1023 // Check cache with TTL validation
1024 if let Some(entry) = DNS_CACHE.get(&cache_key) {
1025 let (addr, timestamp) = *entry.value();
1026 if timestamp
1027 .elapsed()
1028 .map(|d| d.as_secs() < DNS_CACHE_TTL)
1029 .unwrap_or(false)
1030 {
1031 return Ok(addr);
1032 }
1033 }
1034
1035 // Resolve and cache
1036 let addrs: Vec<SocketAddr> = format!("{}:{}", host, port)
1037 .to_socket_addrs()
1038 .map_err(|e| McError::DnsError(e.to_string()))?
1039 .collect();
1040
1041 let addr = addrs
1042 .iter()
1043 .find(|a| a.is_ipv4())
1044 .or_else(|| addrs.first())
1045 .copied()
1046 .ok_or_else(|| McError::DnsError("No addresses resolved".to_string()))?;
1047
1048 DNS_CACHE.insert(cache_key, (addr, SystemTime::now()));
1049 Ok(addr)
1050 }
1051
1052 /// Get DNS information for a hostname.
1053 ///
1054 /// This is a simplified implementation that retrieves A records.
1055 /// More advanced DNS queries (e.g., CNAME) would require additional DNS library features.
1056 ///
1057 /// # Arguments
1058 ///
1059 /// * `host` - Hostname to query
1060 ///
1061 /// # Returns
1062 ///
1063 /// DNS information or an error if resolution fails.
1064 async fn get_dns_info(&self, host: &str) -> Result<DnsInfo, McError> {
1065 let addrs: Vec<SocketAddr> = format!("{}:0", host)
1066 .to_socket_addrs()
1067 .map_err(|e| McError::DnsError(e.to_string()))?
1068 .collect();
1069
1070 Ok(DnsInfo {
1071 a_records: addrs.iter().map(|a| a.ip().to_string()).collect(),
1072 cname: None, // This would require proper DNS queries
1073 ttl: DNS_CACHE_TTL as u32,
1074 })
1075 }
1076
1077 /// Send handshake packet to Java server.
1078 ///
1079 /// # Arguments
1080 ///
1081 /// * `stream` - TCP stream to write to
1082 /// * `host` - Hostname (used in handshake packet)
1083 /// * `port` - Port number (used in handshake packet)
1084 ///
1085 /// # Errors
1086 ///
1087 /// Returns an error if writing to the stream fails or times out.
1088 async fn send_handshake(&self, stream: &mut TcpStream, host: &str, port: u16) -> Result<(), McError> {
1089 let mut handshake = Vec::with_capacity(INITIAL_PACKET_CAPACITY);
1090 write_var_int(&mut handshake, HANDSHAKE_PACKET_ID);
1091 write_var_int(&mut handshake, PROTOCOL_VERSION);
1092 write_string(&mut handshake, host);
1093 handshake.extend_from_slice(&port.to_be_bytes());
1094 write_var_int(&mut handshake, 1); // Next state: status
1095
1096 let mut packet = Vec::with_capacity(handshake.len() + 5);
1097 write_var_int(&mut packet, handshake.len() as i32);
1098 packet.extend_from_slice(&handshake);
1099
1100 timeout(self.timeout, stream.write_all(&packet))
1101 .await
1102 .map_err(|_| McError::Timeout)?
1103 .map_err(McError::IoError)
1104 }
1105
1106 /// Send status request packet to Java server.
1107 ///
1108 /// # Arguments
1109 ///
1110 /// * `stream` - TCP stream to write to
1111 ///
1112 /// # Errors
1113 ///
1114 /// Returns an error if writing to the stream fails or times out.
1115 async fn send_status_request(&self, stream: &mut TcpStream) -> Result<(), McError> {
1116 let mut status_request = Vec::with_capacity(5);
1117 write_var_int(&mut status_request, STATUS_REQUEST_PACKET_ID);
1118
1119 let mut status_packet = Vec::with_capacity(status_request.len() + 5);
1120 write_var_int(&mut status_packet, status_request.len() as i32);
1121 status_packet.extend_from_slice(&status_request);
1122
1123 timeout(self.timeout, stream.write_all(&status_packet))
1124 .await
1125 .map_err(|_| McError::Timeout)?
1126 .map_err(McError::IoError)
1127 }
1128
1129 /// Read response from Java server.
1130 ///
1131 /// This method reads the complete response packet, handling variable-length
1132 /// packets correctly.
1133 ///
1134 /// # Arguments
1135 ///
1136 /// * `stream` - TCP stream to read from
1137 ///
1138 /// # Returns
1139 ///
1140 /// Complete response packet as bytes.
1141 ///
1142 /// # Errors
1143 ///
1144 /// Returns an error if reading fails, times out, or no data is received.
1145 async fn read_response(&self, stream: &mut TcpStream) -> Result<Vec<u8>, McError> {
1146 let mut response = Vec::with_capacity(INITIAL_RESPONSE_CAPACITY);
1147 let mut buf = [0u8; READ_BUFFER_SIZE];
1148 let mut expected_length = None;
1149
1150 loop {
1151 let n = timeout(self.timeout, stream.read(&mut buf))
1152 .await
1153 .map_err(|_| McError::Timeout)?
1154 .map_err(McError::IoError)?;
1155
1156 if n == 0 {
1157 break;
1158 }
1159
1160 response.extend_from_slice(&buf[..n]);
1161
1162 // Check if we have enough data to determine packet length
1163 if expected_length.is_none() && response.len() >= 5 {
1164 let mut cursor = Cursor::new(&response);
1165 if let Ok(packet_length) = read_var_int(&mut cursor) {
1166 expected_length = Some(cursor.position() as usize + packet_length as usize);
1167 }
1168 }
1169
1170 if let Some(expected) = expected_length {
1171 if response.len() >= expected {
1172 break;
1173 }
1174 }
1175 }
1176
1177 if response.is_empty() {
1178 return Err(McError::InvalidResponse("No response from server".to_string()));
1179 }
1180
1181 Ok(response)
1182 }
1183
1184 /// Parse Java server response.
1185 ///
1186 /// Extracts JSON data and calculates latency from the response packet.
1187 ///
1188 /// # Arguments
1189 ///
1190 /// * `response` - Complete response packet
1191 /// * `start` - Start time for latency calculation
1192 ///
1193 /// # Returns
1194 ///
1195 /// Tuple of `(JSON value, latency in milliseconds)`.
1196 ///
1197 /// # Errors
1198 ///
1199 /// Returns an error if the packet is malformed or contains invalid data.
1200 fn parse_java_response(&self, response: Vec<u8>, start: SystemTime) -> Result<(serde_json::Value, f64), McError> {
1201 let mut cursor = Cursor::new(&response);
1202 let packet_length = read_var_int(&mut cursor)
1203 .map_err(|e| McError::InvalidResponse(format!("Failed to read packet length: {}", e)))?;
1204
1205 let total_expected = cursor.position() as usize + packet_length as usize;
1206 if response.len() < total_expected {
1207 return Err(McError::InvalidResponse(format!(
1208 "Incomplete packet: expected {}, got {}",
1209 total_expected, response.len()
1210 )));
1211 }
1212
1213 let packet_id = read_var_int(&mut cursor)
1214 .map_err(|e| McError::InvalidResponse(format!("Failed to read packet ID: {}", e)))?;
1215
1216 if packet_id != STATUS_RESPONSE_PACKET_ID {
1217 return Err(McError::InvalidResponse(format!("Unexpected packet ID: {}", packet_id)));
1218 }
1219
1220 let json_length = read_var_int(&mut cursor)
1221 .map_err(|e| McError::InvalidResponse(format!("Failed to read JSON length: {}", e)))?;
1222
1223 if cursor.position() as usize + json_length as usize > response.len() {
1224 return Err(McError::InvalidResponse("JSON data truncated".to_string()));
1225 }
1226
1227 let json_buf = &response[cursor.position() as usize..cursor.position() as usize + json_length as usize];
1228 let json_str = String::from_utf8(json_buf.to_vec()).map_err(McError::Utf8Error)?;
1229
1230 let json: serde_json::Value = serde_json::from_str(&json_str).map_err(McError::JsonError)?;
1231
1232 let latency = start
1233 .elapsed()
1234 .map_err(|_| McError::InvalidResponse("Time error".to_string()))?
1235 .as_secs_f64()
1236 * 1000.0;
1237
1238 Ok((json, latency))
1239 }
1240
1241 /// Parse Java server JSON response.
1242 ///
1243 /// Extracts structured data from the JSON response, including version info,
1244 /// players, plugins, mods, and more.
1245 ///
1246 /// # Arguments
1247 ///
1248 /// * `json` - JSON value from server response
1249 ///
1250 /// # Returns
1251 ///
1252 /// Parsed `JavaStatus` structure.
1253 ///
1254 /// # Errors
1255 ///
1256 /// Returns an error if required fields are missing or malformed.
1257 fn parse_java_json(&self, json: &serde_json::Value) -> Result<JavaStatus, McError> {
1258 let version = JavaVersion {
1259 name: json["version"]["name"]
1260 .as_str()
1261 .unwrap_or("Unknown")
1262 .to_string(),
1263 protocol: json["version"]["protocol"].as_i64().unwrap_or(0),
1264 };
1265
1266 let players = JavaPlayers {
1267 online: json["players"]["online"].as_i64().unwrap_or(0),
1268 max: json["players"]["max"].as_i64().unwrap_or(0),
1269 sample: if let Some(sample) = json["players"]["sample"].as_array() {
1270 Some(
1271 sample
1272 .iter()
1273 .filter_map(|p| {
1274 Some(JavaPlayer {
1275 name: p["name"].as_str()?.to_string(),
1276 id: p["id"].as_str()?.to_string(),
1277 })
1278 })
1279 .collect(),
1280 )
1281 } else {
1282 None
1283 },
1284 };
1285
1286 let description = if let Some(desc) = json["description"].as_str() {
1287 desc.to_string()
1288 } else if let Some(text) = json["description"]["text"].as_str() {
1289 text.to_string()
1290 } else {
1291 "No description".to_string()
1292 };
1293
1294 let favicon = json["favicon"].as_str().map(|s| s.to_string());
1295 let map = json["map"].as_str().map(|s| s.to_string());
1296 let gamemode = json["gamemode"].as_str().map(|s| s.to_string());
1297 let software = json["software"].as_str().map(|s| s.to_string());
1298
1299 let plugins = if let Some(plugins_array) = json["plugins"].as_array() {
1300 Some(
1301 plugins_array
1302 .iter()
1303 .filter_map(|p| {
1304 Some(JavaPlugin {
1305 name: p["name"].as_str()?.to_string(),
1306 version: p["version"].as_str().map(|s| s.to_string()),
1307 })
1308 })
1309 .collect(),
1310 )
1311 } else {
1312 None
1313 };
1314
1315 let mods = if let Some(mods_array) = json["mods"].as_array() {
1316 Some(
1317 mods_array
1318 .iter()
1319 .filter_map(|m| {
1320 Some(JavaMod {
1321 modid: m["modid"].as_str()?.to_string(),
1322 version: m["version"].as_str().map(|s| s.to_string()),
1323 })
1324 })
1325 .collect(),
1326 )
1327 } else {
1328 None
1329 };
1330
1331 Ok(JavaStatus {
1332 version,
1333 players,
1334 description,
1335 favicon,
1336 map,
1337 gamemode,
1338 software,
1339 plugins,
1340 mods,
1341 raw_data: json.clone(),
1342 })
1343 }
1344
1345 /// Create Bedrock ping packet.
1346 ///
1347 /// Returns a properly formatted UDP packet for querying Bedrock servers.
1348 fn create_bedrock_ping_packet(&self) -> Vec<u8> {
1349 let mut ping_packet = Vec::with_capacity(BEDROCK_PING_PACKET_SIZE);
1350 ping_packet.push(0x01);
1351 ping_packet.extend_from_slice(
1352 &(SystemTime::now()
1353 .duration_since(SystemTime::UNIX_EPOCH)
1354 .unwrap()
1355 .as_millis() as u64)
1356 .to_be_bytes(),
1357 );
1358 ping_packet.extend_from_slice(&[
1359 0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE, 0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78,
1360 ]);
1361 ping_packet.extend_from_slice(&[0x00; 8]);
1362 ping_packet
1363 }
1364
1365 /// Parse Bedrock server response.
1366 ///
1367 /// Extracts structured data from the Bedrock server's UDP response.
1368 ///
1369 /// # Arguments
1370 ///
1371 /// * `pong_data` - Response data string from server
1372 ///
1373 /// # Returns
1374 ///
1375 /// Parsed `BedrockStatus` structure.
1376 ///
1377 /// # Errors
1378 ///
1379 /// Returns an error if the response is malformed or missing required fields.
1380 fn parse_bedrock_response(&self, pong_data: &str) -> Result<BedrockStatus, McError> {
1381 let parts: Vec<&str> = pong_data.split(';').collect();
1382
1383 if parts.len() < 6 {
1384 return Err(McError::InvalidResponse("Invalid Bedrock response".to_string()));
1385 }
1386
1387 Ok(BedrockStatus {
1388 edition: parts[0].to_string(),
1389 motd: parts[1].to_string(),
1390 protocol_version: parts[2].to_string(),
1391 version: parts[3].to_string(),
1392 online_players: parts[4].to_string(),
1393 max_players: parts[5].to_string(),
1394 server_uid: parts.get(6).map_or("", |s| *s).to_string(),
1395 motd2: parts.get(7).map_or("", |s| *s).to_string(),
1396 game_mode: parts.get(8).map_or("", |s| *s).to_string(),
1397 game_mode_numeric: parts.get(9).map_or("", |s| *s).to_string(),
1398 port_ipv4: parts.get(10).map_or("", |s| *s).to_string(),
1399 port_ipv6: parts.get(11).map_or("", |s| *s).to_string(),
1400 map: parts.get(12).map(|s| s.to_string()),
1401 software: parts.get(13).map(|s| s.to_string()),
1402 raw_data: pong_data.to_string(),
1403 })
1404 }
1405}
1406
1407// === Protocol Helper Functions ===
1408
1409/// Write a VarInt to a buffer.
1410///
1411/// VarInt is a variable-length integer encoding used in Minecraft protocol.
1412/// It uses 7 bits per byte, with the most significant bit indicating continuation.
1413///
1414/// # Arguments
1415///
1416/// * `buffer` - Buffer to write to
1417/// * `value` - Integer value to encode
1418fn write_var_int(buffer: &mut Vec<u8>, value: i32) {
1419 let mut value = value as u32;
1420 loop {
1421 let mut temp = (value & 0x7F) as u8;
1422 value >>= 7;
1423 if value != 0 {
1424 temp |= 0x80;
1425 }
1426 buffer.push(temp);
1427 if value == 0 {
1428 break;
1429 }
1430 }
1431}
1432
1433/// Write a string to a buffer (VarInt length + UTF-8 bytes).
1434///
1435/// # Arguments
1436///
1437/// * `buffer` - Buffer to write to
1438/// * `s` - String to write
1439fn write_string(buffer: &mut Vec<u8>, s: &str) {
1440 write_var_int(buffer, s.len() as i32);
1441 buffer.extend_from_slice(s.as_bytes());
1442}
1443
1444/// Read a VarInt from a reader.
1445///
1446/// # Arguments
1447///
1448/// * `reader` - Reader to read from
1449///
1450/// # Returns
1451///
1452/// Decoded integer value.
1453///
1454/// # Errors
1455///
1456/// Returns an error if:
1457/// - Reading fails
1458/// - VarInt is too large (exceeds 35 bits)
1459fn read_var_int(reader: &mut impl std::io::Read) -> Result<i32, String> {
1460 let mut result = 0i32;
1461 let mut shift = 0;
1462 loop {
1463 let mut byte = [0u8];
1464 reader.read_exact(&mut byte).map_err(|e| e.to_string())?;
1465 let value = byte[0] as i32;
1466 result |= (value & 0x7F) << shift;
1467 shift += 7;
1468 if shift > MAX_VARINT_SHIFT {
1469 return Err("VarInt too big".to_string());
1470 }
1471 if (value & 0x80) == 0 {
1472 break;
1473 }
1474 }
1475 Ok(result)
1476}