← All Workshops

MudEngine Part 8: Inventory & Chat

Step 6 / 10

Update WebSocket Endpoint

The WebSocket handler gets three new message types to handle: Say, Take, and Drop. Each follows the same pattern: validate, mutate the game state, save to disk, then broadcast the result.

Also update the initial State message to include items in the RoomInfo list and in the new player's PlayerInfo.

mud-engine/src/main.rs
// ── WebSocket endpoint ──

#[get("/api/mud_ws?name")]
async fn mud_ws(
    name: String,
    options: WebSocketOptions,
) -> Result<Websocket<ClientMessage, ServerMessage>> {
    let id = uuid::Uuid::new_v4().to_string();
    let player_name = name.clone();

    // 1. Register the new player and build room info for the client
    let room_infos: Vec<RoomInfo>;
    {
        let mut state = srv::GAME_STATE.lock().unwrap();
        state.players.insert(
            id.clone(),
            PlayerInfo {
                id: id.clone(),
                name: name.clone(),
                room: 4,
                items: Vec::new(),
            },
        );
        srv::save_game_state(&state);

        room_infos = state
            .rooms
            .iter()
            .map(|r| RoomInfo {
                id: r.id,
                name: r.name.clone(),
                description: r.description.clone(),
                items: r.items.clone(),
            })
            .collect();
    }

    // 2. Subscribe to the broadcast channel
    let mut rx = srv::BROADCAST.subscribe();

    // 3. Announce the new player to everyone
    let _ = srv::BROADCAST.send(ServerMessage::PlayerJoined(PlayerInfo {
        id: id.clone(),
        name: name.clone(),
        room: 4,
        items: Vec::new(),
    }));

    // 4. Build the initial state snapshot for this new player
    let initial_state = {
        let state = srv::GAME_STATE.lock().unwrap();
        ServerMessage::State {
            players: state.players.values().cloned().collect(),
            your_id: id.clone(),
            rooms: room_infos,
        }
    };

    // 5. Upgrade the HTTP connection to a WebSocket
    let ws = options.on_upgrade(move |mut socket| async move {
        let _ = socket.send(initial_state).await;

        // ── Message loop ──
        loop {
            tokio::select! {
                msg = socket.recv() => {
                    match msg {
                        Ok(ClientMessage::Move { direction }) => {
                            let dir = match direction.as_str() {
                                "n" => "north", "s" => "south",
                                "e" => "east", "w" => "west",
                                d => d,
                            };

                            let new_room = {
                                let mut state = srv::GAME_STATE.lock().unwrap();
                                let player = state.players.get_mut(&id).unwrap();
                                let current = player.room;

                                if let Some(next) = srv::can_move(&state, current, dir) {
                                    player.room = next;
                                    srv::save_game_state(&state);
                                    Some(next)
                                } else {
                                    None
                                }
                            };

                            if let Some(room) = new_room {
                                let _ = srv::BROADCAST.send(
                                    ServerMessage::PlayerMoved(PlayerInfo {
                                        id: id.clone(),
                                        name: player_name.clone(),
                                        room,
                                        items: Vec::new(), // items unchanged
                                    }),
                                );
                            }
                        }

                        Ok(ClientMessage::Say { message }) => {
                            let current_room = {
                                let state = srv::GAME_STATE.lock().unwrap();
                                state.players.get(&id).map(|p| p.room).unwrap_or(4)
                            };

                            let _ = srv::BROADCAST.send(ServerMessage::Chat {
                                from: player_name.clone(),
                                message,
                                room: current_room,
                            });
                        }

                        Ok(ClientMessage::Take { item }) => {
                            let mut success = false;
                            {
                                let mut state = srv::GAME_STATE.lock().unwrap();
                                let player = state.players.get_mut(&id).unwrap();
                                let room_id = player.room;

                                if let Some(room) = state.rooms.iter_mut()
                                    .find(|r| r.id == room_id)
                                {
                                    if let Some(pos) = room.items.iter()
                                        .position(|i| i == &item)
                                    {
                                        room.items.remove(pos);
                                        player.items.push(item.clone());
                                        srv::save_game_state(&state);
                                        success = true;
                                    }
                                }
                            }

                            if success {
                                let _ = srv::BROADCAST.send(
                                    ServerMessage::ItemTaken {
                                        player_id: id.clone(),
                                        player_name: player_name.clone(),
                                        item,
                                    },
                                );
                            }
                        }

                        Ok(ClientMessage::Drop { item }) => {
                            let mut success = false;
                            {
                                let mut state = srv::GAME_STATE.lock().unwrap();
                                let player = state.players.get_mut(&id).unwrap();
                                let room_id = player.room;

                                if let Some(pos) = player.items.iter()
                                    .position(|i| i == &item)
                                {
                                    player.items.remove(pos);
                                    if let Some(room) = state.rooms.iter_mut()
                                        .find(|r| r.id == room_id)
                                    {
                                        room.items.push(item.clone());
                                        srv::save_game_state(&state);
                                        success = true;
                                    }
                                }
                            }

                            if success {
                                let _ = srv::BROADCAST.send(
                                    ServerMessage::ItemDropped {
                                        player_id: id.clone(),
                                        player_name: player_name.clone(),
                                        item,
                                        room: 0, // filled in by the client from known room
                                    },
                                );
                            }
                        }

                        Err(_) => break,
                    }
                }

                msg = rx.recv() => {
                    match msg {
                        Ok(server_msg) => {
                            if socket.send(server_msg).await.is_err() {
                                break;
                            }
                        }
                        Err(_) => break,
                    }
                }
            }
        }

        // ── Cleanup on disconnect ──
        {
            let mut state = srv::GAME_STATE.lock().unwrap();

            // Drop all carried items into the player's current room
            if let Some(player) = state.players.get(&id) {
                let room_id = player.room;
                if let Some(room) = state.rooms.iter_mut().find(|r| r.id == room_id) {
                    for item in &player.items {
                        if !room.items.contains(item) {
                            room.items.push(item.clone());
                        }
                    }
                }
            }

            state.players.remove(&id);
            srv::save_game_state(&state);
        }
        let _ = srv::BROADCAST.send(ServerMessage::PlayerLeft { id });
    });

    Ok(ws)
}
🎯 Why validate inside the lock

Both Take and Drop perform their validation and mutation inside a single GAME_STATE.lock() scope. This is critical:

  1. Atomicity — no other WebSocket handler can interleave between "check item exists" and "move item". This prevents race conditions where two players try to take the same item simultaneously.
  2. Consistencysave_game_state() is called inside the lock, so the on-disk state always matches the in-memory state.
  3. Simplicity — no need for optimistic locking or retry logic. The Mutex guarantees sequential access.

The PlayerMoved message still sends an empty items vector. In a future part you might include items in the movement broadcast, but for now the player list on the client refreshes from State / PlayerJoined / ItemTaken / ItemDropped messages.

Step 6 / 10