← All Workshops

MudEngine Part 7: Save & Load

Step 5 / 10

Define Save Data Models

We need three layers of types:

  1. TOML types — mirror the structure of game.toml, annotated with serde derives
  2. Runtime types — the in-memory representation used by the game logic (RoomData, GameState)
  3. RoomInfo — sent to clients so they can render room names without hardcoded statics

Add these types at the top of src/main.rs, after the existing shared types (PlayerInfo, ClientMessage, ServerMessage):

mud-engine/src/main.rs
// ── Room info sent to clients ──

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct RoomInfo {
    id: usize,
    name: String,
    description: String,
}

// ── TOML save data models ──

#[derive(Serialize, Deserialize)]
struct TomlExit {
    direction: String,
    destination: usize,
}

#[derive(Serialize, Deserialize)]
struct TomlRoom {
    id: usize,
    name: String,
    description: String,
    exits: Vec<TomlExit>,
}

#[derive(Serialize, Deserialize)]
struct TomlPlayer {
    id: String,
    name: String,
    room: usize,
}

#[derive(Serialize, Deserialize)]
struct TomlGame {
    rooms: Vec<TomlRoom>,
    players: Vec<TomlPlayer>,
}

// ── Runtime game state (server only) ──

#[cfg(feature = "server")]
struct RoomData {
    id: usize,
    name: String,
    description: String,
    exits: Vec<(String, usize)>,
}

#[cfg(feature = "server")]
struct GameState {
    rooms: Vec<RoomData>,
    players: HashMap<String, PlayerInfo>,
}
🔍 Why separate TOML and runtime types?

The TOML types (TomlRoom, TomlExit, etc.) are designed to mirror the exact structure of game.toml — serde maps them field-by-field. The runtime types (RoomData, GameState) are designed for efficient game logic:

  • TomlRoom.exits uses Vec<TomlExit> because TOML serialises arrays of inline tables cleanly
  • RoomData.exits uses Vec<(String, usize)> — a tuple is simpler to pattern-match in the movement validation code

The conversion between the two happens in load_game_state() and save_game_state().

RoomInfo lives outside the server-only module because it needs to be shared with the client through the ServerMessage::State payload. It only carries what the client needs (id, name, description — no exits).

Step 5 / 10