rust-mc-status 3.0.0

High-performance asynchronous Rust library for querying Minecraft server status (Java & Bedrock)
Documentation
<div align="center">
  <img src="assets/logo.svg" alt="rust-mc-status logo" width="160" height="160">
  <h1>rust-mc-status</h1>
  <p>
    <strong>High-performance, async Minecraft server status client for Rust โ€” Java &amp; Bedrock Edition.</strong>
  </p>
  <p>

<!-- prettier-ignore-start -->

[![crates.io](https://img.shields.io/crates/v/rust-mc-status?label=latest)](https://crates.io/crates/rust-mc-status)
[![Documentation](https://docs.rs/rust-mc-status/badge.svg?version=3.0.0)](https://docs.rs/rust-mc-status/3.0.0)
![MSRV](https://img.shields.io/badge/rustc-1.85+-ab6000.svg)
![MIT licensed](https://img.shields.io/crates/l/rust-mc-status.svg)
[![Dependency Status](https://deps.rs/crate/rust-mc-status/3.0.0/status.svg)](https://deps.rs/crate/rust-mc-status/3.0.0)
<br />
![downloads](https://img.shields.io/crates/d/rust-mc-status.svg)

<!-- prettier-ignore-end -->

  </p>
</div>

---

**Documentation**: [https://docs.rs/rust-mc-status](https://docs.rs/rust-mc-status/3.0.0)

**Source Code**: [https://github.com/NameOfShadow/rust-mc-status](https://github.com/NameOfShadow/rust-mc-status)

---

## ๐ŸŽ‰ v3.0.0 โ€” Released 2026-07-01

A major rewrite focused on ergonomics, performance, and extensibility. **Breaking changes** from 2.x โ€” see the [Migration Guide](CHANGELOG.md#migration-guide-2x--300).

**Headline changes:**

- **Builder API** โ€” `McClient::builder().timeout(...).response_cache(...).build()` replaces ad-hoc `with_*` chaining.
- **Per-request fluent pings** โ€” `client.java("addr").timeout(3s).await?` with optional `.is_online()`.
- **Response cache with in-flight deduplication** โ€” repeated pings to the same server collapse into one network request; cache hits return in ~0 ms.
- **Two-level error hierarchy** โ€” `McError::Network(NetworkError::Timeout)` etc. for fine-grained matching and retry policy.
- **Proxy support** *(feature = "proxy")* โ€” SOCKS5 (with optional UDP for Bedrock) and HTTP CONNECT, with auth.
- **Tower integration** *(feature = "tower")* โ€” `McService` + `McRetryPolicy` plug into the Tower middleware ecosystem.
- **Layer-based source tree** โ€” `client/`, `core/`, `models/`, `protocol/`, `proxy/`, `status/` for cleaner navigation.
- **Zero-cost defaults** โ€” without optional features, no extra tower / pin-project / tokio-socks dependencies are pulled in.
- **149 tests across 8 integration test files** โ€” DNS parsing, Bedrock/Java decoders, error hierarchy, response cache, proxy config, MOTD formatting.

Full release notes and the migration guide are in [CHANGELOG.md](CHANGELOG.md).

---

## Features

- **Dual Protocol Support** โ€” ping both Java Edition (port `25565`) and Bedrock Edition (port `19132`)
- **Ergonomic Builder API** โ€” `client.java("addr").timeout(3s).await?` with per-request timeout override
- **Typed Status Wrappers** โ€” `JavaServerStatus` and `BedrockServerStatus` with edition-specific methods
- **Facade Functions** โ€” `ping_java("addr").await?` without creating a client
- **Tower Middleware** *(optional feature)* โ€” rate limiting, retries, buffering, tracing via the Tower ecosystem
- **Async/Await** โ€” built on Tokio for non-blocking, high-concurrency operation
- **Batch Queries** โ€” ping multiple servers in parallel with configurable concurrency
- **LRU DNS Cache** โ€” DNS and SRV lookups cached with automatic eviction (default 1024 entries)
- **SRV Record Support** โ€” automatic SRV lookup for Java servers, mimicking the official client
- **JSON Serialisation** โ€” all status types implement `serde::Serialize`; favicon and raw data excluded automatically
- **MOTD Formatting** โ€” `motd_clean()` strips Minecraft `ยง`-color codes; `truncate_str()` safely slices Unicode
- **Zero-Panic Design** โ€” all server-supplied data validated before use; no silent index panics
- **SmallVec Optimisation** โ€” player sample, plugins, and mods use stack allocation for small lists
- **Zero-cost without Tower** โ€” the default build pulls in no tower, async-trait, or pin-project dependency

## Installation

```toml
[dependencies]
rust-mc-status = "3.0.0"
tokio = { version = "*", features = ["full"] }
```

To enable Tower middleware (rate limiting, retries, buffering, tracing):

```toml
rust-mc-status = { version = "3.0.0", features = ["tower"] }
```

## Quick Start

### One-liner (no client needed)

```rust
use rust_mc_status::{ping_java, ping_bedrock, StatusExt};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let status = ping_java("mc.hypixel.net").await?;
    println!("{}", status);                    // "mc.hypixel.net [42 ms] 32000/200000 โ€” Hypixel Network"
    println!("{}", status.display_players());  // "32000/200000"
    println!("{}", status.motd_clean());       // MOTD without ยง-codes
    Ok(())
}
```

### Client with custom settings

```rust
use rust_mc_status::{McClient, StatusExt};
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = McClient::new()
        .with_timeout(Duration::from_secs(10))
        .with_max_parallel(20)
        .with_cache_size(2048);

    // Typed Java status
    let java = client.java("mc.hypixel.net").await?;
    println!("Version : {}", java.version());
    println!("Players : {}", java.display_players());
    println!("Latency : {:.0} ms", java.latency_ms());
    println!("MOTD    : {}", java.motd_clean());

    // Per-request timeout override โ€” client config unchanged
    let fast = client.java("mc.hypixel.net")
        .timeout(Duration::from_secs(3))
        .await?;
    println!("Fast check: {} ms", fast.latency_ms() as u32);

    // Typed Bedrock status
    let bedrock = client.bedrock("geo.hivebedrock.network:19132").await?;
    println!("Edition : {}", bedrock.edition());
    println!("Players : {}", bedrock.display_players());

    Ok(())
}
```

### Batch queries

```rust
use rust_mc_status::{McClient, ServerEdition, ServerInfo};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = McClient::new();
    let servers = vec![
        ServerInfo { address: "mc.hypixel.net".into(),                edition: ServerEdition::Java },
        ServerInfo { address: "geo.hivebedrock.network:19132".into(), edition: ServerEdition::Bedrock },
    ];

    for (server, result) in client.ping_many(&servers).await {
        match result {
            Ok(s)  => println!("{}: online ({:.0} ms)", server.address, s.latency),
            Err(e) => println!("{}: {}", server.address, e),
        }
    }
    Ok(())
}
```

### JSON serialisation

```rust
let status = client.java("mc.hypixel.net").await?;
let json = serde_json::to_string(&status)?;         // compact
let json = serde_json::to_string_pretty(&status)?;  // pretty-printed
// favicon and raw_data are excluded automatically
```

### Runtime edition selection

```rust
let edition: ServerEdition = "java".parse()?;

// Full status
let status = client.server("mc.hypixel.net", edition).await?;

// Lightweight reachability check only
let online = client.server("mc.hypixel.net", edition)
    .timeout(Duration::from_secs(3))
    .is_online()
    .await;
```

### Custom address types

```rust
struct ServerId(String);
impl From<ServerId> for String { fn from(s: ServerId) -> Self { s.0 } }

let status = client.java(ServerId("mc.hypixel.net".into())).await?;
```

## Tower Middleware

Enable with `features = ["tower"]`. Converts `McClient` into a standard `tower::Service` that works with the entire Tower ecosystem โ€” rate limiting, retries, buffering, `tower-http` tracing, and any custom `tower::Layer`.

```rust
use tower::{ServiceBuilder, ServiceExt};
use rust_mc_status::{McClient, McRetryPolicy, PingRequestTower};
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut svc = ServiceBuilder::new()
        .buffer(64)                                      // Clone + concurrent
        .rate_limit(100, Duration::from_secs(1))         // 100 req/s
        .retry(McRetryPolicy::new(3))                    // 3 attempts on timeout/connection error
        .service(McClient::new().into_service());

    svc.ready().await?;
    let resp = svc.call(PingRequestTower::java("mc.hypixel.net")).await?;
    println!("{:.0} ms", resp.status.latency);
    Ok(())
}
```

### Custom Tower Layer

Implement `tower::Layer` for logging, metrics, or any middleware:

```rust
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tower::{Layer, Service};
use rust_mc_status::{McError, PingRequestTower, PingResponseTower};

#[derive(Clone)]
struct LogLayer;

impl<S> Layer<S> for LogLayer {
    type Service = LogService<S>;
    fn layer(&self, inner: S) -> LogService<S> { LogService { inner } }
}

#[derive(Clone)]
struct LogService<S> { inner: S }

impl<S> Service<PingRequestTower> for LogService<S>
where
    S: Service<PingRequestTower, Response = PingResponseTower, Error = McError> + Send,
    S::Future: Send + 'static,
{
    type Response = PingResponseTower;
    type Error    = McError;
    type Future   = Pin<Box<dyn Future<Output = Result<PingResponseTower, McError>> + Send>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), McError>> {
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, req: PingRequestTower) -> Self::Future {
        println!("โ†’ {}", req.address);
        let fut = self.inner.call(req);
        Box::pin(async move {
            let resp = fut.await?;
            println!("{:.0} ms", resp.status.latency);
            Ok(resp)
        })
    }
}

// Usage
let mut svc = ServiceBuilder::new()
    .layer(LogLayer)
    .service(McClient::new().into_service());
```

## SRV Record Lookup

When pinging a Java server **without** an explicit port, the library queries `_minecraft._tcp.{hostname}` for SRV records โ€” exactly as the official Minecraft client does. Results are cached for 5 minutes.

```rust
// SRV lookup performed automatically
let status = client.java("hypixel.net").await?;

// Explicit port โ€” SRV lookup skipped
let status = client.java("mc.hypixel.net:25565").await?;
```

## MOTD Formatting

```rust
use rust_mc_status::strip_formatting;

let raw = "ยงaHypixel ยงc[1.8/1.21]";
println!("{}", strip_formatting(raw)); // "Hypixel [1.8/1.21]"

// Or via the wrapper method
let status = client.java("mc.hypixel.net").await?;
println!("{}", status.motd_clean()); // same result
```

## Examples

| Example | Description |
|---|---|
| `basic_usage` | Core API: facade, typed status, per-request timeout, serialisation |
| `advanced_usage` | Batch queries, plugins, mods, favicon saving |
| `cache_management` | LRU cache stats, warm/cold timing, `clear_caches()` |
| `tower_usage` | Tower middleware: rate limiting, retries, buffer, custom Layer |
| `srv_lookup_example` | SRV record behaviour |
| `performance_test` | Benchmarking (run with `--release`) |

```bash
cargo run --example basic_usage
cargo run --example cache_management
cargo run --example tower_usage --features tower
cargo run --example performance_test --release
```

## API Reference

### `McClient` builder methods

| Method | Description |
|---|---|
| `new()` | Default settings (10 s timeout, 10 parallel, 1024 cache) |
| `with_timeout(Duration)` | Global request timeout |
| `with_max_parallel(usize)` | Max concurrent pings in `ping_many` |
| `with_cache_size(usize)` | LRU cache capacity for DNS and SRV |
| `into_service()` *(tower)* | Convert to `McService` for Tower middleware |

### Ping methods

| Method | Returns | Notes |
|---|---|---|
| `client.java(addr)` | `JavaPingBuilder` | Supports `.timeout(d).await` |
| `client.bedrock(addr)` | `BedrockPingBuilder` | Supports `.timeout(d).await` |
| `client.server(addr, edition)` | `ServerPingBuilder` | Runtime edition; `.is_online().await` |
| `client.ping_many(&[ServerInfo])` | `Vec<(ServerInfo, Result<ServerStatus>)>` | Parallel batch |
| `ping_java(addr)` | `Result<JavaServerStatus>` | Global client, no setup needed |
| `ping_bedrock(addr)` | `Result<BedrockServerStatus>` | Global client, no setup needed |

### Tower types *(feature = "tower")*

| Type | Description |
|---|---|
| `McService` | `tower::Service<PingRequestTower>` โ€” wraps `McClient` |
| `McRetryPolicy` | Retries on `Timeout` / `ConnectionError`; configurable attempt count |
| `PingRequestTower` | Request type with `.java(addr)` / `.bedrock(addr)` constructors |
| `PingResponseTower` | Response containing `status: ServerStatus` |

### `JavaServerStatus` methods

`motd()`, `motd_clean()`, `version()`, `players_online()`, `players_max()`, `display_players()`, `favicon()`, `save_favicon(path)`, `ip()`, `port()`, `hostname()`, `latency_ms()`, `is_online()`, `raw()`

### `BedrockServerStatus` methods

`motd()`, `motd_clean()`, `motd2()`, `motd2_clean()`, `edition()`, `version()`, `players_online()`, `players_max()`, `display_players()`, `game_mode()`, `ip()`, `port()`, `hostname()`, `latency_ms()`, `is_online()`, `raw()`

### Utility functions

| Function | Description |
|---|---|
| `strip_formatting(s)` | Remove Minecraft `ยงX` codes, collapse whitespace |
| `truncate_str(s, n)` | Truncate to `n` Unicode characters (panic-safe) |

### Cache methods (all `async`)

`cache_stats().await`, `clear_caches().await`

## Error Handling

```rust
use rust_mc_status::McError;

match client.java("server.com").await {
    Ok(s)                              => println!("online: {}", s.display_players()),
    Err(McError::Timeout)              => println!("timed out"),
    Err(McError::DnsError(msg))        => println!("DNS: {}", msg),
    Err(McError::ConnectionError(msg)) => println!("connection: {}", msg),
    Err(e)                             => println!("error: {}", e),
}
```

## Performance Notes

- DNS and SRV results cached for 5 minutes with LRU eviction โ€” no memory leaks
- `tokio::sync::RwLock` allows concurrent readers with minimal contention
- `SmallVec` for player sample (โ‰ค 12), plugins (โ‰ค 8), and mods (โ‰ค 8) โ€” stack-allocated for common case
- Per-request `.timeout()` reduces tail latency in batch scenarios without touching the client
- `ping_bedrock` uses UDP; `ping_java` uses TCP with `TCP_NODELAY`
- Default build has zero tower/async-trait dependency โ€” only pulled in with `features = ["tower"]`

## License

MIT โ€” see [LICENSE](LICENSE).

## Changelog

See [CHANGELOG.md](CHANGELOG.md).

## Version

Current version: **3.0.0** (released 2026-07-01) โ€” see [CHANGELOG.md](CHANGELOG.md) for full history and migration guide.