← All Workshops

MudEngine Part 3: File-Based World Loading

Update the World model

Replace the hardcoded World::new() with a World::from_file(path) method that deserializes the TOML file.

We define serde-compatible structs for the TOML format — TomlRoom includes a numeric id field, and each exit's destination is a room id. The loader copies the data straight through; no name-to-index resolution is needed.

mud-engine-repl/src/main.rs
use std::io::{self, Write};
use serde::Deserialize;

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

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

#[derive(Deserialize)]
struct TomlWorld {
    rooms: Vec<TomlRoom>,
}

struct Room {
    id: usize,
    name: String,
    description: String,
    exits: Vec<(String, usize)>,
}

struct World {
    rooms: Vec<Room>,
    player_room: usize,
}

impl World {
    fn from_file(path: &str) -> Result<Self, String> {
        let content = std::fs::read_to_string(path)
            .map_err(|e| format!("Failed to read {}: {}", path, e))?;
        let toml_world: TomlWorld = toml::from_str(&content)
            .map_err(|e| format!("Failed to parse {}: {}", path, e))?;

        let rooms: Vec<Room> = toml_world
            .rooms
            .iter()
            .map(|r| Room {
                id: r.id,
                name: r.name.clone(),
                description: r.description.clone(),
                exits: r.exits.iter().map(|e| (e.direction.clone(), e.destination)).collect(),
            })
            .collect();

        Ok(World { rooms, player_room: 1 })
    }

    fn look(&self) -> String {
        let room = self.rooms.iter().find(|r| r.id == self.player_room).unwrap();
        let mut out = String::new();
        out.push_str(&room.name);
        out.push_str("\n");
        out.push_str(&room.description);
        out.push_str("\n\nExits:");
        for (dir, _) in &room.exits {
            out.push_str(" ");
            out.push_str(dir);
        }
        out
    }

    fn go(&mut self, direction: &str) -> String {
        let room = self.rooms.iter().find(|r| r.id == self.player_room).unwrap();
        for (dir, id) in &room.exits {
            if dir == direction {
                self.player_room = *id;
                return self.look();
            }
        }
        format!("You cannot go {} from here.", direction)
    }
}
🔍 How room lookup works

The loader no longer needs a name-to-index map. Since both the TOML destination and the player_room use numeric room ids, the look and go methods use iter().find() to locate rooms:

let room = self.rooms.iter().find(|r| r.id == self.player_room).unwrap();

This linear scan is fine for a few rooms. For thousands of rooms, you would build a HashMap<usize, &Room> for O(1) lookups — but that is an optimisation you can add later when you need it.