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'smove_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
MessageEventand 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()→LayeredCommandRegistrycommands - [ ] Convert applies → EventBus subscriptions
- [ ] Replace
save_object()/restore_object()→DocumentStoreschemas - [ ] 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¶
- Concept Mapping Table — Quick reference across all engines
- ECS Guide — Deep dive into MAID's ECS
- Content Packs — Plugin system details
- Command System — Layered command processing
- Events — EventBus documentation