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}