Skip to content

Migrating from LDMud to MAID

This guide helps experienced LDMud developers understand MAID's architecture and translate their LPC knowledge to MAID's Python-based patterns. Moving from LPC to Python is a significant language shift, but many core MUD concepts carry over — the key difference is MAID's composition-over-inheritance approach.

Note

Automated import tooling is planned but not yet available. This guide covers manual migration patterns.

Architecture at a Glance

LDMud MAID
Language LPC (C-like) Python 3.12+
Paradigm OOP with inheritance Entity Component System (ECS)
Concurrency Single-threaded + call_out() asyncio (async/await)
Object model Inherit from base objects Entity + Components (composition)
Database Save files / custom DocumentStore (PostgreSQL JSONB / in-memory)
Commands add_action() on objects LayeredCommandRegistry with pack priorities
Game loop Heartbeat (2s default) GameEngine tick loop (4/sec default)
Mudlib Custom mudlib (e.g., Lima, Dead Souls) ContentPack modules
Configuration Config files Environment variables (MAID_ prefix)
Networking Built-in Telnet Telnet (GMCP, MXP, MCCP2) + WebSocket

Key Paradigm Shifts

1. LPC Objects → Entities + Components

In LDMud, everything inherits from base objects in the mudlib:

// LDMud: Object inheritance
// /std/weapon.c
inherit "/std/object";

void create() {
    ::create();
    set_name("iron sword");
    set_short("an iron sword");
    set_long("A sturdy iron blade, well-balanced for combat.");
    set_weight(3);
    set_value(100);
    set_weapon_type("sword");
    set_damage(10);
}

In MAID, objects don't inherit behavior — they're composed from components:

# MAID: Entity composition
sword = world.create_entity()
sword.add(DescriptionComponent(
    name="iron sword",
    short_desc="an iron sword",
    long_desc="A sturdy iron blade, well-balanced for combat.",
))
sword.add(WeaponComponent(weapon_type="sword", damage=10))
# PhysicalComponent and ValueComponent are user-defined components
sword.add(PhysicalComponent(weight=3))
sword.add(ValueComponent(value=100))
sword.add_tag("item")

Why this matters: In LPC, if you want an object that's both a weapon and a container (a sword with a hidden compartment), you need multiple inheritance or complex workarounds. In MAID, just add both WeaponComponent and InventoryComponent — no class hierarchy changes needed.

2. LPC → Python Syntax

For LPC developers, here's a quick Python syntax comparison:

// LPC
int attack(object target) {
    int damage = random(this_player()->query_strength());
    target->receive_damage(damage);
    tell_object(this_player(), sprintf("You deal %d damage!\n", damage));
    return 1;
}
# Python (MAID)
async def cmd_attack(ctx: CommandContext) -> bool:
    attacker = ctx.world.get_entity(ctx.player_id)
    stats = attacker.get(CharacterStatsComponent)
    damage = random.randint(1, stats.strength)

    # Emit event instead of calling target directly
    await ctx.world.events.emit(DamageDealtEvent(
        source_id=ctx.player_id,
        target_id=target.id,
        damage=damage,
        damage_type="physical",
    ))

    await ctx.session.send(f"You deal {damage} damage!")
    return True

Key differences:

LPC Python (MAID)
int x = 5; x: int = 5
string *arr = ({}) arr: list[str] = []
mapping m = ([]) m: dict[str, Any] = {}
sprintf("Hello %s", name) f"Hello {name}"
this_player() ctx.player_id / ctx.session
this_object() self or the entity being processed
tell_object(ob, msg) await session.send(msg)
clone_object(path) world.create_entity() + add components
destruct(ob) world.destroy_entity(entity.id)
environment(ob) world.get_entity_room(entity.id)

3. call_out() / Heartbeat → Systems

LDMud uses call_out() for timed events and heartbeat for repeating logic:

// LDMud: call_out and heartbeat
void create() {
    set_heart_beat(1);
}

void heart_beat() {
    // Runs every heartbeat interval
    if (this_player()->query_hp() < this_player()->query_max_hp()) {
        this_player()->heal(1);
    }
}

void start_poison(int duration) {
    call_out("poison_tick", 5, duration);
}

void poison_tick(int remaining) {
    this_player()->receive_damage(3, "poison");
    if (remaining > 0) {
        call_out("poison_tick", 5, remaining - 5);
    }
}

In MAID, a System runs every tick and processes all relevant entities:

# MAID: System handles all regeneration
class RegenerationSystem(System):
    priority: ClassVar[int] = 80

    async def update(self, delta: float) -> None:
        for entity in self.entities.with_components(HealthComponent):
            health = entity.get(HealthComponent)
            if health.current < health.maximum:
                regen = health.regeneration_rate * delta
                health.heal(int(regen))

Key insight: In LDMud, each object manages its own heartbeat. In MAID, one System processes all entities with the relevant components in a single pass. This is simpler to reason about and more efficient.

4. add_action() → LayeredCommandRegistry

LDMud registers commands via add_action() on objects:

// LDMud: Action-based commands
void init() {
    add_action("do_wield", "wield");
    add_action("do_unwield", "unwield");
}

int do_wield(string arg) {
    object weapon = present(arg, this_player());
    if (!weapon) {
        write("You don't have that.\n");
        return 1;
    }
    // ... wield logic
    return 1;
}

MAID registers commands through a centralized registry:

# MAID: Command registration
async def cmd_wield(ctx: CommandContext) -> bool:
    if not ctx.args:
        await ctx.session.send("Wield what?")
        return True

    player = ctx.world.get_entity(ctx.player_id)
    inv = player.get(InventoryComponent)

    target_name = ctx.rest
    for item_id in inv.items:
        item = ctx.world.get_entity(item_id)
        name = item.try_get(DescriptionComponent)
        if name and target_name.lower() in name.name.lower():
            equip = player.get(EquipmentComponent)
            equip.slots["main_hand"] = item_id
            await ctx.session.send(f"You wield {name.name}.")
            return True

    await ctx.session.send("You don't have that.")
    return True

# In ContentPack.register_commands():
registry.register("wield", cmd_wield, pack_name="my-game",
                   category="equipment", description="Wield a weapon")

Using declarative argument parsing (replaces manual sscanf/parsing):

from maid_engine.commands import arguments, ArgumentSpec, ArgumentType, ParsedArguments, SearchScope

@arguments(
    ArgumentSpec("weapon", ArgumentType.ENTITY, search_scope=SearchScope.INVENTORY),
)
async def cmd_wield(ctx: CommandContext, args: ParsedArguments) -> bool:
    weapon = args["weapon"]  # Already found and validated
    # ... wield logic
    return True

5. Efuns → World / EntityManager API

LDMud's efuns (external functions) map to MAID's World and EntityManager:

LPC Efun MAID Equivalent Notes
clone_object(path) world.create_entity() Then add components
destruct(ob) world.destroy_entity(id) Emits EntityDestroyedEvent
move_object(dest) world.move_entity(id, room_id) Emits enter/leave events
this_player() ctx.player_id UUID in command context
this_object() Entity being processed In System or via ID
environment(ob) world.get_entity_room(id) Returns room UUID
all_inventory(ob) entity.get(InventoryComponent).items Component field
present(name, env) world.entities_in_room(room_id) + filter Query + match
find_object(path) entities.with_tag("name") Tag or component query
tell_object(ob, msg) await session.send(msg) Async session I/O
tell_room(room, msg) Iterate world.entities_in_room() Send to all in room
say(msg) Emit MessageEvent Event-driven
write(msg) await ctx.session.send(msg) To current player
random(n) random.randint(0, n-1) Python stdlib
sizeof(arr) len(arr) Python built-in
member(arr, el) el in arr Python built-in
query_*() entity.get(Component).field Component access
set_*() entity.get(Component).field = value Direct assignment
save_object() Automatic via EntityPersistenceManager Dirty tracking handles it
restore_object() Automatic on startup DocumentStore loads state

6. Mudlib → ContentPack

The LDMud mudlib (the standard library of base objects) maps to MAID's content packs. MAID ships with maid-stdlib as its standard library:

Mudlib Concept MAID Equivalent
/std/object.c Entity + base components from maid-stdlib
/std/room.c Entity + PositionComponent + ExtendedRoomComponent
/std/living.c Entity + HealthComponent + PositionComponent
/std/player.c Entity + PlayerComponent + HealthComponent + ...
/std/weapon.c Entity + user-defined WeaponComponent + EquipmentComponent
/std/armour.c Entity + user-defined ArmorComponent + EquipmentComponent
/std/container.c Entity + InventoryComponent
Mudlib package ContentPack
#include headers Python import
Master object GameEngine
Simul efuns Utility functions in maid_stdlib.utils

7. Applies → EventBus

LDMud "applies" (functions called by the driver on objects) map to MAID events:

LPC Apply MAID Event / Pattern
create() async on_load() in ContentPack, or EntityCreatedEvent
init() RoomEnterEvent
exit() / move() RoomLeaveEvent / RoomEnterEvent
heart_beat() System.update(delta)
catch_tell() MessageEvent subscription
receive_damage() DamageDealtEvent handler
clean_up() System with cleanup logic
reset() System with timer-based respawn
query_weight() etc. entity.get(Component).field

What's Familiar

These LDMud concepts have direct parallels:

  • Object properties — Like query_name() / set_name(), components have typed fields you get and set
  • Room exits — Exits work similarly: a direction maps to a destination room
  • Inventory — Containment works via InventoryComponent, like LPC's move_object() into containers
  • Wizard levels — MAID has AccessLevel (PLAYER, HELPER, BUILDER, ADMIN, IMPLEMENTOR) — similar to LPC wizard levels
  • Tell/Say/Shout — Message routing exists via MessageEvent and channels
  • Save/restore — Automatic persistence replaces save_object()/restore_object()
  • Resets — Timer-based respawn logic goes in Systems (equivalent to reset())

Getting Started: Your First MAID Content Pack

Here's how to migrate a simple LPC area to MAID:

Step 1: Replace Object Blueprints with Components

// LDMud: /areas/village/town_square.c
inherit "/std/room";

void create() {
    set_short("Town Square");
    set_long("A bustling town square with a fountain in the center.");
    add_exit("north", "/areas/village/market");
    add_exit("south", "/areas/village/gate");
}
# MAID: Define room in ContentPack on_load or YAML
async def create_town_square(world: World) -> Entity:
    room = world.create_entity()
    room.add(DescriptionComponent(
        name="Town Square",
        long_desc="A bustling town square with a fountain in the center.",
    ))
    room.add_tag("room")
    world.register_room(room.id, {"name": "Town Square"})
    return room

Or use MAID's YAML content loader:

# data/areas/village/rooms.yml
- id: town-square
  name: "Town Square"
  description: "A bustling town square with a fountain in the center."
  tags: ["room"]
  exits:
    north: market
    south: gate

Step 2: Replace LPC Logic with Systems

// LDMud: /std/combat.c
void attack(object target) {
    int damage = random(query_str()) - target->query_armor();
    if (damage > 0) {
        target->receive_damage(damage, "physical");
        tell_object(this_player(), sprintf("You hit %s for %d damage!\n",
            target->query_name(), damage));
    }
}
# MAID: CombatSystem
class SimpleCombatSystem(System):
    priority: ClassVar[int] = 40

    async def startup(self) -> None:
        self.events.subscribe(CombatStartEvent, self.handle_combat)

    async def handle_combat(self, event: CombatStartEvent) -> None:
        attacker = self.entities.get(event.attacker_id)
        defender = self.entities.get(event.defender_id)
        if not attacker or not defender:
            return

        atk_stats = attacker.get(CharacterStatsComponent)
        damage = random.randint(1, atk_stats.strength)

        defender.get(HealthComponent).damage(damage)
        await self.events.emit(DamageDealtEvent(
            source_id=attacker.id,
            target_id=defender.id,
            damage=damage,
            damage_type="physical",
        ))

Step 3: Create the ContentPack

# packages/my-game/src/my_game/pack.py
class MyGamePack(BaseContentPack):
    @property
    def manifest(self) -> ContentPackManifest:
        return ContentPackManifest(
            name="my-game",
            version="1.0.0",
            description="My migrated LPC game",
        )

    def get_dependencies(self) -> list[str]:
        return ["stdlib"]

    def get_systems(self, world: World) -> list[System]:
        return [SimpleCombatSystem(world), RegenerationSystem(world)]

    def register_commands(self, registry: LayeredCommandRegistry) -> None:
        registry.register("attack", cmd_attack, pack_name="my-game")
        registry.register("wield", cmd_wield, pack_name="my-game")

    async def on_load(self, engine: GameEngine) -> None:
        await create_town_square(engine.world)

Migration Checklist

  • [ ] Catalog all LPC object blueprints → design component equivalents
  • [ ] Map mudlib base objects to MAID stdlib components
  • [ ] Convert heart_beat() / call_out() → Systems
  • [ ] Replace add_action()LayeredCommandRegistry commands
  • [ ] Convert applies → EventBus subscriptions
  • [ ] Replace save_object() / restore_object()DocumentStore schemas
  • [ ] Move area files → YAML content or ContentPack on_load()
  • [ ] Convert #include → Python imports
  • [ ] Add type hints to all function signatures
  • [ ] Bundle everything into a ContentPack

Further Reading