elytra_ping/
lib.rs

1//! Through the `elytra_ping` crate, programs can retrieve the status and information about Minecraft Java Edition or Bedrock Edition servers.
2//!
3//! This crate can interact with servers running Minecraft Java 1.7+ or Bedrock. If you have the server's address and port, Elytra Ping can retrieve metadata like the server's description, player count, vendor, and icon. The (lack of the) server's response can also be used to infer whether it is online and usable or not.
4//!
5//! ## Usage
6//!
7//! Use the [`ping_or_timeout`] function to retrieve a Java Edition server's status and latency, aborting if it takes too long.
8//!
9//! ```
10//! # use std::time::Duration;
11//! # #[tokio::main]
12//! # async fn main() {
13//! let (ping_info, latency) = elytra_ping::ping_or_timeout(
14//!     ("mc.hypixel.net".to_string(), 25565),
15//!     Duration::from_secs(1),
16//! ).await.unwrap();
17//! println!("{ping_info:#?}, {latency:?}");
18//! # }
19//! ```
20//!
21//! Use the [`bedrock::ping`] function to retrieve a Bedrock Edition server's status and latency, specifying the number of retries
22//! if the operation fails initially and the amount of time to spend before timing out on a single retry.
23//!
24//! ```
25//! # use std::time::Duration;
26//! # #[tokio::main]
27//! # async fn main() {
28//! let retry_timeout = Duration::from_secs(2);
29//! let retries = 3;
30//! let (ping_info, latency) = elytra_ping::bedrock::ping(
31//!     ("play.cubecraft.net".to_string(), 19132),
32//!     retry_timeout,
33//!     retries,
34//! ).await.unwrap();
35//! println!("{ping_info:#?}, {latency:?}");
36//! // BedrockServerInfo {
37//! //     online_players: 10077,
38//! //     max_players: 55000,
39//! //     game_mode: Some(
40//! //         "Survival",
41//! //     ),
42//! //     ...
43//! // }, 83ms
44//! # }
45//! ```
46//!
47//! ### Advanced API
48//!
49//! Elytra Ping can be customized for advanced usage through the `SlpProtocol` API,
50//! which provides an interface for sending and receiving packets to and from Java Edition servers.
51//!
52//! ```
53//! # #[tokio::main]
54//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
55//! let addrs = ("mc.hypixel.net".to_string(), 25565);
56//! let mut client: elytra_ping::SlpProtocol = elytra_ping::connect(addrs).await?;
57//!
58//! // Set up our connection to receive a status packet
59//! client.handshake().await?;
60//! client.write_frame(elytra_ping::protocol::Frame::StatusRequest).await?;
61//!
62//! // Read the status packet from the server
63//! let frame: elytra_ping::protocol::Frame = client
64//!     .read_frame(None)
65//!     .await?
66//!     .expect("connection closed by server");
67//!
68//! let status: String = match frame {
69//!     elytra_ping::protocol::Frame::StatusResponse { json } => json,
70//!     _ => panic!("expected status packet"),
71//! };
72//!
73//! println!("Status: {}", status);
74//!
75//! client.disconnect().await?;
76//! # Ok(())
77//! # }
78//! ```
79use snafu::{Backtrace, Snafu};
80use std::time::Duration;
81
82#[cfg(feature = "java_connect")]
83pub mod mc_string;
84#[cfg(feature = "java_connect")]
85pub mod protocol;
86#[cfg(feature = "java_connect")]
87pub use crate::protocol::connect;
88#[cfg(feature = "java_connect")]
89pub use protocol::SlpProtocol;
90
91#[cfg(feature = "java_parse")]
92pub mod parse;
93#[cfg(feature = "java_parse")]
94pub use parse::JavaServerInfo;
95
96#[cfg(feature = "bedrock")]
97pub mod bedrock;
98
99#[cfg(feature = "simple")]
100#[derive(Snafu, Debug)]
101pub enum PingError {
102    /// Connection failed.
103    #[snafu(display("Connection failed: {source}"), context(false))]
104    Protocol {
105        #[snafu(backtrace)]
106        source: crate::protocol::ProtocolError,
107    },
108    /// The connection did not finish in time.
109    Timeout { backtrace: Backtrace },
110}
111
112#[cfg(feature = "simple")]
113pub async fn ping(addrs: (String, u16)) -> Result<(JavaServerInfo, Duration), PingError> {
114    let mut client = connect(addrs).await?;
115    client.handshake().await?;
116    let status = client.get_status().await?;
117    let latency = client.get_latency().await?;
118    client.disconnect().await?;
119    Ok((status, latency))
120}
121
122#[cfg(feature = "simple")]
123pub async fn ping_or_timeout(
124    addrs: (String, u16),
125    timeout: Duration,
126) -> Result<(JavaServerInfo, Duration), PingError> {
127    use tokio::{select, time};
128    let sleep = time::sleep(timeout);
129    tokio::pin!(sleep);
130
131    select! {
132        biased;
133        info = ping(addrs) => info,
134        _ = sleep => TimeoutSnafu.fail(),
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use snafu::ErrorCompat;
141
142    use super::*;
143
144    #[ctor::ctor]
145    fn init_logger() {
146        use tracing_subscriber::EnvFilter;
147
148        tracing_subscriber::fmt()
149            .pretty()
150            .with_env_filter(EnvFilter::from_default_env())
151            .init();
152    }
153
154    const PING_TIMEOUT: Duration = Duration::from_secs(5);
155
156    #[tokio::test]
157    async fn hypixel() {
158        let address = "mc.hypixel.net".to_owned();
159        let port = 25565;
160        let ping = ping_or_timeout((address, port), PING_TIMEOUT).await;
161        match ping {
162            Err(err) => panic!("Error: {err} ({err:?})\n{:?}", err.backtrace().unwrap()),
163            Ok(ping) => println!("{:#?} in {:?}", ping.0, ping.1),
164        }
165    }
166
167    #[tokio::test]
168    async fn hypixel_bare() {
169        let address = "hypixel.net".to_owned();
170        let port = 25565;
171        let ping = ping_or_timeout((address, port), PING_TIMEOUT).await;
172        match ping {
173            Err(err) => panic!("Error: {err} ({err:?})\n{:?}", err.backtrace().unwrap()),
174            Ok(ping) => println!("{:#?} in {:?}", ping.0, ping.1),
175        }
176    }
177
178    #[tokio::test]
179    #[ignore = "mineplex is shut down"]
180    async fn mineplex() {
181        let address = "us.mineplex.com".to_owned();
182        let port = 25565;
183        let ping = ping_or_timeout((address, port), PING_TIMEOUT).await;
184        match ping {
185            Err(err) => panic!("Error: {err} ({err:?})\n{:?}", err.backtrace().unwrap()),
186            Ok(ping) => println!("{:#?} in {:?}", ping.0, ping.1),
187        }
188    }
189
190    #[tokio::test]
191    #[ignore = "mineplex is shut down"]
192    async fn mineplex_bare() {
193        let address = "mineplex.com".to_owned();
194        let port = 25565;
195        let ping = ping_or_timeout((address, port), PING_TIMEOUT).await;
196        match ping {
197            Err(err) => panic!("Error: {err} ({err:?})\n{:?}", err.backtrace().unwrap()),
198            Ok(ping) => println!("{:#?} in {:?}", ping.0, ping.1),
199        }
200    }
201}