← All Workshops

MudEngine Part 7: Save & Load

Step 8 / 10

Update the Game Component

The Game component previously used ROOM_NAMES[idx] and ROOM_DESCS[info.room] from static arrays. Now room data comes from the server's State message.

Changes:

  1. Add a rooms: Signal<HashMap<usize, RoomInfo>> to store the server-provided room data
  2. In the receive loop, populate the room map from the State message
  3. Replace all ROOM_NAMES[...] and ROOM_DESCS[...] lookups with room-map lookups
  4. Remove the static ROOM_NAMES and static ROOM_DESCS declarations

Update the Game component:

mud-engine/src/main.rs
// ── Game UI ──

#[component]
fn Game(name: String) -> Element {
    let mut players = use_signal::<HashMap<String, PlayerInfo>>(|| HashMap::new());
    let mut my_id = use_signal(|| String::new());
    let mut rooms = use_signal::<HashMap<usize, RoomInfo>>(|| HashMap::new());

    let mut socket = use_websocket(move || {
        mud_ws(name.clone(), WebSocketOptions::new())
    });

    // ── Message receive loop ──
    {
        let mut players = players.clone();
        let mut my_id = my_id.clone();
        let mut rooms = rooms.clone();
        use_future(move || async move {
            loop {
                _ = socket.connect().await;
                while let Ok(msg) = socket.recv().await {
                    match msg {
                        ServerMessage::State {
                            players: p,
                            your_id,
                            rooms: r,
                        } => {
                            let mut map = players.write();
                            map.clear();
                            for pl in p {
                                map.insert(pl.id.clone(), pl);
                            }
                            my_id.set(your_id);

                            // Store room data from the server
                            let mut room_map = rooms.write();
                            room_map.clear();
                            for room in r {
                                room_map.insert(room.id, room);
                            }
                        }
                        ServerMessage::PlayerJoined(pl)
                        | ServerMessage::PlayerMoved(pl) => {
                            players.write().insert(pl.id.clone(), pl);
                        }
                        ServerMessage::PlayerLeft { id } => {
                            players.write().remove(&id);
                        }
                    }
                }
            }
        });
    }

    let my_info = {
        let p = players.read();
        let id = my_id.read();
        id.as_ref()
            .and_then(|id| p.get(id).cloned())
    };

    rsx! {
        div { class: "game-layout",
            // ── Left: 3×3 grid ──
            div { class: "grid-area",
                div { class: "world-grid",
                    for y in 0..3 {
                        for x in 0..3 {
                            let idx = y * 3 + x;
                            let room_name = rooms.read()
                                .get(&idx)
                                .map(|r| r.name.clone())
                                .unwrap_or_default();

                            // Collect the names of players in this room
                            let occupants: Vec<String> = players.read().values()
                                .filter(|p| p.room == idx)
                                .map(|p| p.name.clone())
                                .collect();

                            // Is this the cell where "we" are standing?
                            let is_my_cell = my_id.read().as_ref()
                                .and_then(|id| players.read().get(id))
                                .map_or(false, |p| p.room == idx);

                            div {
                                key: "{idx}",
                                class: if is_my_cell {
                                    "cell active"
                                } else {
                                    "cell"
                                },
                                div { class: "room-name", "{room_name}" }
                                for occupant_name in &occupants {
                                    div { class: "player-indicator", "{occupant_name}" }
                                }
                            }
                        }
                    }
                }
            }

            // ── Right: player list sidebar ──
            div { class: "sidebar",
                h2 { "⚔️ Adventurers" }
                for p in players.read().values() {
                    let is_me = my_id.read().as_ref()
                        .map_or(false, |id| id == &p.id);
                    let room_name = rooms.read()
                        .get(&p.room)
                        .map(|r| r.name.clone())
                        .unwrap_or_default();
                    div {
                        key: "{p.id}",
                        class: if is_me {
                            "player-entry me"
                        } else {
                            "player-entry"
                        },
                        span { class: "player-name",
                            "{p.name}"
                            if is_me { " (you)" }
                        }
                        span { class: "player-room", "{room_name}" }
                    }
                }
            }
        }

        // ── Description panel ──
        if let Some(ref info) = my_info {
            let room_data = rooms.read().get(&info.room);
            let rname = room_data.map(|r| r.name.clone()).unwrap_or_default();
            let rdesc = room_data.map(|r| r.description.clone()).unwrap_or_default();
            div { class: "description",
                "{rname}"
                "\n"
                "{rdesc}"
            }
        }

        // ── D-pad movement controls ──
        div { class: "dpad",
            button {
                class: "dpad-btn up",
                onclick: move |_| {
                    let _ = socket.send(ClientMessage::Move {
                        direction: "north".into(),
                    });
                },
                ""
            }
            div { class: "dpad-row",
                button {
                    class: "dpad-btn left",
                    onclick: move |_| {
                        let _ = socket.send(ClientMessage::Move {
                            direction: "west".into(),
                        });
                    },
                    ""
                }
                button {
                    class: "dpad-btn center",
                    disabled: "true",
                    ""
                }
                button {
                    class: "dpad-btn right",
                    onclick: move |_| {
                        let _ = socket.send(ClientMessage::Move {
                            direction: "east".into(),
                        });
                    },
                    ""
                }
            }
            button {
                class: "dpad-btn down",
                onclick: move |_| {
                    let _ = socket.send(ClientMessage::Move {
                        direction: "south".into(),
                    });
                },
                ""
            }
        }
    }
}
💡 Removed statics

The old ROOM_NAMES and ROOM_DESCS static arrays are completely removed. If you scroll up in your main.rs, you should delete them — they're not referenced anywhere anymore.

Also remove the old EXITS constant from inside the srv module — the new can_move function handles movement validation from the TOML-loaded room data.

Clean up after yourself! Unused statics generate compiler warnings.

Step 8 / 10