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}