Skip to content

Migrating from Ranvier to MAID

This guide helps experienced Ranvier developers understand MAID's architecture and translate their knowledge to MAID's patterns. Both engines share some modern design sensibilities — data-driven content, plugin bundles, event-driven architecture — so many concepts will feel familiar. The main differences are language (JavaScript → Python), object model (class instances → ECS), and concurrency (Node.js event loop → Python asyncio).

Note

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

Architecture at a Glance

Ranvier MAID
Language JavaScript (Node.js) Python 3.12+
Async model Node.js event loop (callbacks/promises) asyncio (async/await)
Object model Class instances + Behaviors Entity Component System (ECS)
Data format YAML/JSON area files YAML content loader + DocumentStore
Database JSON flat files (default) DocumentStore (PostgreSQL JSONB / in-memory)
Commands Command classes in bundles LayeredCommandRegistry with pack priorities
Game loop GameLoop class GameEngine tick loop
Plugins Bundles (BundleManager) ContentPack protocol
Events EventManager on entities EventBus (centralized pub/sub)
Configuration ranvier.json Environment variables (MAID_ prefix)
Networking Telnet + WebSocket Telnet (GMCP, MXP, MCCP2) + WebSocket

Key Paradigm Shifts

1. Behaviors → Components + Systems

Ranvier uses Behaviors to attach logic to entities — a pattern between mixins and event listeners:

// Ranvier: Behavior on an NPC
module.exports = {
  listeners: {
    playerEnter: state => function (player) {
      Broadcast.sayAt(player, 'The guard nods at you.');
    },

    hit: state => function (damage, attacker) {
      if (this.getAttribute('health') < 50) {
        Broadcast.sayAt(attacker, 'The guard calls for backup!');
      }
    }
  }
};

In MAID, data lives in Components and logic lives in Systems:

# MAID: Component for data
class GuardComponent(Component):
    calls_backup_at_health_pct: float = 50.0
    has_called_backup: bool = False

class GuardBehaviorSystem(System):
    priority: ClassVar[int] = 70

    async def startup(self) -> None:
        self.events.subscribe(RoomEnterEvent, self.on_player_enter)
        self.events.subscribe(DamageDealtEvent, self.on_damage)

    async def on_player_enter(self, event: RoomEnterEvent) -> None:
        entity = self.entities.get(event.entity_id)
        if not entity or not entity.has(PlayerComponent):
            return

        # Find guards in the room and notify the player via events
        for guard in self.world.entities_in_room(event.room_id):
            if guard.has(GuardComponent):
                await self.events.emit(MessageEvent(
                    sender_id=guard.id,
                    target_ids=[event.entity_id],
                    channel="room",
                    message="The guard nods at you.",
                ))

    async def on_damage(self, event: DamageDealtEvent) -> None:
        target = self.entities.get(event.target_id)
        if not target or not target.has(GuardComponent):
            return

        guard = target.get(GuardComponent)
        health = target.get(HealthComponent)
        if (health.percentage < guard.calls_backup_at_health_pct
                and not guard.has_called_backup):
            guard.has_called_backup = True
            # Emit event for other systems to handle
            await self.events.emit(MessageEvent(
                sender_id=target.id,
                target_ids=[event.source_id],
                channel="combat",
                message="The guard calls for backup!",
            ))

Key difference: Ranvier Behaviors attach listeners to individual entity instances. MAID Systems process all entities with matching components centrally. Data and logic are separated.

2. BundleManager → ContentPack

Ranvier organizes content into bundles with a manifest.yml:

# Ranvier: bundle manifest
# bundles/my-areas/manifest.yml
name: my-areas
version: 1.0.0
requires:
  - combat-base
// bundles/my-areas/commands/look.js
module.exports = {
  command: state => (args, player) => {
    const room = player.room;
    Broadcast.sayAt(player, room.title);
    Broadcast.sayAt(player, room.description);
  }
};

MAID uses ContentPack classes with a similar manifest concept:

# MAID: ContentPack
class MyAreasPack(BaseContentPack):
    @property
    def manifest(self) -> ContentPackManifest:
        return ContentPackManifest(
            name="my-areas",
            version="1.0.0",
            description="My game areas",
        )

    def get_dependencies(self) -> list[str]:
        return ["stdlib"]  # Like Ranvier's "requires"

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

    def register_commands(self, registry: LayeredCommandRegistry) -> None:
        registry.register("look", cmd_look, pack_name="my-areas")

    async def on_load(self, engine: GameEngine) -> None:
        # Load area data, create rooms, etc.
        pass

This should feel natural — both systems use manifests, dependencies, and modular organization.

3. Entity Events → Centralized EventBus

Ranvier attaches events to individual entity instances:

// Ranvier: Events on entities
room.on('playerEnter', player => {
  Broadcast.sayAt(player, 'You feel a chill.');
});

npc.on('hit', (damage, attacker) => {
  // Handle being hit
});

item.on('get', (player) => {
  Broadcast.sayAt(player, 'The ring feels warm.');
});

MAID uses a centralized EventBus — you subscribe to event types, not individual entity events:

# MAID: Centralized event subscriptions
class AtmosphereSystem(System):
    async def startup(self) -> None:
        self.events.subscribe(RoomEnterEvent, self.on_enter)
        self.events.subscribe(ItemPickedUpEvent, self.on_pickup)

    async def on_enter(self, event: RoomEnterEvent) -> None:
        room = self.entities.get(event.room_id)
        if room and room.has_tag("spooky"):
            await self.events.emit(MessageEvent(
                sender_id=event.room_id,
                target_ids=[event.entity_id],
                channel="room",
                message="You feel a chill.",
            ))

    async def on_pickup(self, event: ItemPickedUpEvent) -> None:
        item = self.entities.get(event.item_id)
        if item and item.has_tag("magical-ring"):
            await self.events.emit(MessageEvent(
                sender_id=event.item_id,
                target_ids=[event.entity_id],
                channel="room",
                message="The ring feels warm.",
            ))

Key insight: Instead of attaching handlers per entity, you filter by entity properties (tags, components) inside a centralized handler. This makes it easier to add cross-cutting behavior without modifying individual entities.

4. Attributes → Component Fields

Ranvier uses an attribute system for entity stats:

// Ranvier: Attributes
player.getAttribute('health');
player.setAttributeBase('health', 100);
player.modifyAttribute('health', -10);

// In area YAML
- id: sword
  attributes:
    damage: 10
    speed: fast

MAID uses typed component fields:

# MAID: Component fields
health = entity.get(HealthComponent)
health.current  # Read
health.damage(10)  # Method call with validation
health.heal(5)

# Components are Pydantic models — validated, typed
class WeaponComponent(Component):
    damage: int = 10
    speed: str = "normal"
    weapon_type: str = "sword"

Advantage: Components are Pydantic BaseModel subclasses with full type validation. Invalid data is caught at assignment time, not at runtime when it causes a crash.

5. YAML Areas → YAML Loader or Code

Both engines support YAML-defined areas, so this will feel familiar:

# Ranvier: areas/town/rooms.yml
- id: town-square
  title: "Town Square"
  description: "A bustling town square."
  exits:
    north: market
    south: gate
  npcs:
    - id: guard
      behaviors:
        - ranvier-sentient
# MAID: data/areas/village/rooms.yml (loaded by WorldDataLoader)
- id: town-square
  name: "Town Square"
  description: "A bustling town square."
  tags: ["room"]
  exits:
    north: market
    south: gate

MAID's Pipeline processes YAML through six phases: Discovery → Parsing → Validation → Resolution → Instantiation → Indexing.

6. JavaScript → Python

Common pattern translations:

// Ranvier (JavaScript)
const damage = Math.floor(Math.random() * strength);
const targets = room.npcs.filter(npc => npc.hasBehavior('hostile'));
targets.forEach(target => {
  Broadcast.sayAt(player, `You see ${target.name} here.`);
});
# MAID (Python)
damage = random.randint(0, strength)
targets = [
    e for e in world.entities_in_room(room_id)
    if e.has(NPCComponent) and e.has_tag("hostile")
]
for target in targets:
    name = target.get(DescriptionComponent)
    await session.send(f"You see {name.name} here.")
JavaScript (Ranvier) Python (MAID)
const x = 5 x: int = 5
arr.filter(fn) [x for x in arr if fn(x)]
arr.map(fn) [fn(x) for x in arr]
arr.forEach(fn) for x in arr: fn(x)
async function() async def fn():
await promise await coroutine
obj.property obj.property (same!)
require('module') from module import name
module.exports = {} Top-level definitions (no export syntax)
null / undefined None
Template literals `${x}` f-strings f"{x}"
try { } catch (e) { } try: ... except Exception as e: ...

Concept Mapping

Ranvier MAID Notes
GameEntity Entity Both are ID + data containers
Room Entity + room tag + PositionComponent Rooms are entities
Player Entity + PlayerComponent + components Composed from parts
Npc Entity + NPCComponent + components Same composition pattern
Item Entity + item-related components No specific Item class
Behavior System + Component Split data from logic
EventManager (per-entity) EventBus (centralized) Global pub/sub
Bundle ContentPack Very similar concept
manifest.yml ContentPackManifest Programmatic, not YAML
BundleManager ContentPackLoader Dependency-ordered loading
GameLoop GameEngine tick loop Similar concept
Broadcast.sayAt() await session.send() Async in MAID
Broadcast.sayAtExcept() Iterate entities_in_room(), skip one Manual filtering
state object World + GameEngine Split state management
player.getAttribute() entity.get(Component).field Typed fields
player.setAttributeBase() entity.get(Component).field = value Direct assignment
Command class Async handler function Functions, not classes
requiredRole Lock expressions More expressive
AreaManager YAML loader + World.register_room() Multiple approaches
Channel EventBus channels / MessageEvent Event-driven
Quest tracker QuestGenerationSystem in stdlib Built-in support
InputEvent pipeline LayeredCommandRegistry + pre/post hooks Priority-based

What's Familiar

These Ranvier concepts map closely to MAID:

  • YAML area files — Both engines support YAML-defined content. MAID's loader pipeline adds validation and resolution phases.
  • Bundle/Pack concept — Ranvier bundles ≈ MAID ContentPacks. Both have manifests, dependencies, and modular organization.
  • Event-driven design — Both engines use events for communication. MAID's EventBus is centralized rather than per-entity, but the pattern is similar.
  • Async everywhere — Both use async I/O. JavaScript's Promises/async-await maps directly to Python's async/await.
  • Game loop — Both have a configurable tick-based game loop.
  • Telnet + WebSocket — Both support multiple connection protocols.
  • Command system — Both have structured command handling, though MAID uses functions instead of classes.

Getting Started: Your First MAID Content Pack

Step 1: Map Bundle Structure to ContentPack

# Ranvier bundle structure:
bundles/my-game/
├── manifest.yml
├── areas/
│   └── town/
│       ├── rooms.yml
│       ├── npcs.yml
│       └── items.yml
├── commands/
│   └── look.js
├── behaviors/
│   └── guard.js
└── events/
    └── combat.js
# MAID content pack structure:
packages/my-game/
├── src/my_game/
│   ├── pack.py          # ContentPack (replaces manifest.yml)
│   ├── components.py    # Data definitions (replaces attributes)
│   ├── systems.py       # Logic (replaces behaviors + events)
│   ├── commands.py      # Commands (replaces commands/)
│   └── data/
│       └── areas/
│           └── town/
│               └── rooms.yml  # YAML content (similar!)
└── tests/

Step 2: Convert Behaviors to Components + Systems

// Ranvier: behaviors/guard.js
module.exports = {
  listeners: {
    playerEnter: state => function (player) {
      Broadcast.sayAt(player, 'The guard watches you carefully.');
    },
    playerLeave: state => function (player) {
      Broadcast.sayAt(player, 'The guard relaxes.');
    }
  }
};
# MAID: Split into component (data) and system (logic)
class GuardComponent(Component):
    """Marks an NPC as a guard with awareness behavior."""
    alert_level: str = "normal"

class GuardSystem(System):
    priority: ClassVar[int] = 70

    async def startup(self) -> None:
        self.events.subscribe(RoomEnterEvent, self.on_enter)
        self.events.subscribe(RoomLeaveEvent, self.on_leave)

    async def on_enter(self, event: RoomEnterEvent) -> None:
        entity = self.entities.get(event.entity_id)
        if not entity or not entity.has(PlayerComponent):
            return
        for guard in self.world.entities_in_room(event.room_id):
            if guard.has(GuardComponent):
                await self.events.emit(MessageEvent(
                    sender_id=guard.id,
                    target_ids=[event.entity_id],
                    channel="room",
                    message="The guard watches you carefully.",
                ))

    async def on_leave(self, event: RoomLeaveEvent) -> None:
        entity = self.entities.get(event.entity_id)
        if not entity or not entity.has(PlayerComponent):
            return
        for guard in self.world.entities_in_room(event.room_id):
            if guard.has(GuardComponent):
                await self.events.emit(MessageEvent(
                    sender_id=guard.id,
                    target_ids=[event.entity_id],
                    channel="room",
                    message="The guard relaxes.",
                ))

Step 3: Convert Commands

// Ranvier: commands/score.js
module.exports = {
  usage: 'score',
  command: state => (args, player) => {
    const health = player.getAttribute('health');
    const maxHealth = player.getMaxAttribute('health');
    Broadcast.sayAt(player, `Health: ${health}/${maxHealth}`);
    Broadcast.sayAt(player, `Level: ${player.level}`);
  }
};
# MAID: commands.py
async def cmd_score(ctx: CommandContext) -> bool:
    player = ctx.world.get_entity(ctx.player_id)
    if not player:
        return False

    health = player.get(HealthComponent)
    stats = player.get(CharacterStatsComponent)

    await ctx.session.send(
        f"Health: {health.current}/{health.maximum}\n"
        f"Level: {stats.level}"
    )
    return True

Step 4: Create the ContentPack

class MyGamePack(BaseContentPack):
    @property
    def manifest(self) -> ContentPackManifest:
        return ContentPackManifest(
            name="my-game",
            version="1.0.0",
            description="My migrated Ranvier game",
        )

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

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

    def register_commands(self, registry: LayeredCommandRegistry) -> None:
        registry.register("score", cmd_score, pack_name="my-game",
                          description="Show your stats")

    async def on_load(self, engine: GameEngine) -> None:
        # Load YAML areas, create initial entities, etc.
        pass

Migration Checklist

  • [ ] Map bundle structure → ContentPack directory layout
  • [ ] Convert manifest.ymlContentPackManifest class
  • [ ] Split Behaviors → Components (data) + Systems (logic)
  • [ ] Convert entity event listeners → EventBus.subscribe() subscriptions
  • [ ] Translate command classes → async handler functions
  • [ ] Convert attribute definitions → typed Component fields
  • [ ] Migrate YAML area files → MAID YAML format (minor syntax changes)
  • [ ] Replace Broadcast.sayAt()await session.send()
  • [ ] Replace requiredRole → lock expressions
  • [ ] Add Python type hints to all functions
  • [ ] Bundle everything into a ContentPack

Further Reading