← All Workshops

MudEngine Part 6: Multiplayer

Step 5 / 12

Server Game State

The server needs to keep track of all connected players. We use global static state behind #[cfg(feature = "server")]. This module is only compiled into the server binary — the WASM client never sees it.

The state has three parts:

  1. PLAYERS — a Mutex<HashMap<String, PlayerInfo>> mapping player IDs to their info
  2. BROADCAST — a tokio::sync::broadcast::Sender that fans out every message to all connected clients
  3. EXITS — the room exit table for movement validation (same grid as Part 4)

Add this module after the room data statics:

mud-engine/src/main.rs
// ── Server-only state ──

#[cfg(feature = "server")]
mod srv {
    use std::collections::HashMap;
    use std::sync::{LazyLock, Mutex};
    use tokio::sync::broadcast;

    use super::*;

    /// All connected players, keyed by their unique ID.
    pub static PLAYERS: LazyLock<Mutex<HashMap<String, PlayerInfo>>> =
        LazyLock::new(|| Mutex::new(HashMap::new()));

    /// Broadcast channel — every connected WebSocket subscribes.
    /// When a player moves, anyone sends, everyone receives.
    pub static BROADCAST: LazyLock<broadcast::Sender<ServerMessage>> =
        LazyLock::new(|| {
            let (tx, _) = broadcast::channel(64);
            tx
        });

    /// Exit table: for each room index, a list of (direction, destination) pairs.
    /// This mirrors the 3×3 grid from Part 4:
    ///   0  1  2
    ///   3  4  5
    ///   6  7  8
    pub const EXITS: &[&[(&str, usize)]] = &[
        &[("south", 3), ("east", 1)],                               // 0 Forest Path
        &[("south", 4), ("west", 0), ("east", 2)],                   // 1 Hilltop
        &[("south", 5), ("west", 1)],                                // 2 Abandoned Tower
        &[("north", 0), ("south", 6), ("east", 4)],                  // 3 Dark Forest
        &[("north", 1), ("south", 7), ("west", 3), ("east", 5)],     // 4 Town Square
        &[("north", 2), ("south", 8), ("west", 4)],                  // 5 Temple Courtyard
        &[("north", 3), ("east", 7)],                                // 6 Riverbank
        &[("north", 4), ("west", 6), ("east", 8)],                   // 7 Old Bridge
        &[("north", 5), ("west", 7)],                                // 8 Graveyard
    ];
}
💡 Why LazyLock and not a plain static?

Mutex::new() is not a const function, so we cannot write static PLAYERS: Mutex<...> = Mutex::new(HashMap::new()). LazyLock initializes the value on first access — the closure runs once and the result lives for the program's lifetime.

broadcast::channel(64) creates a sender/receiver pair with a buffer of 64 messages. If a client is too slow to keep up, they miss messages (the receiver gets a Lagged error). For our 9-room MUD this is more than enough.

🗺️ Grid reference
   Col 0      Col 1      Col 2
┌──────────┬──────────┬──────────┐
│ Forest   │ Hilltop  │ Abandoned│  Row 0
│ Path (0) │    (1)   │ Tower(2) │
├──────────┼──────────┼──────────┤
│ Dark     │ Town     │ Temple   │  Row 1
│ Forest(3)│ Square(4)│ Crt. (5) │
├──────────┼──────────┼──────────┤
│ River-   │ Old      │ Grave-   │  Row 2
│ bank (6) │ Bridge(7)│ yard (8) │
└──────────┴──────────┴──────────┘

Room index = row × 3 + col. All new players start at room 4 (Town Square, the center). The exit table on the server enforces that movement only works along actual connections — no walking through walls.

Step 5 / 12