Skip to content

Migration Guide: v0.1 to v0.2

Breaking Changes

MAID v0.2 is a major architectural overhaul. This upgrade contains significant breaking changes. Please read this guide carefully and test thoroughly before upgrading production systems.

Overview

MAID v0.2 introduces a complete restructuring of the codebase into a monorepo with three packages:

  • maid-engine: Pure infrastructure (ECS, networking, auth, storage, plugin system)
  • maid-stdlib: Reusable components, base systems, and utilities
  • maid-classic-rpg: Traditional MUD gameplay content pack

Estimated migration effort: Large (1-3 days depending on customizations)

Key changes at a glance:

  • Monolithic codebase split into three packages
  • New Content Pack system replaces old plugin architecture
  • Entity Component System (ECS) for all game objects
  • Async/await throughout (no more synchronous I/O)
  • New configuration system with environment variables

Prerequisites

Before migrating, ensure you have:

  • [ ] Python 3.12 or higher installed
  • [ ] Read this entire guide
  • [ ] Backed up your database and configuration
  • [ ] Tested the upgrade in a non-production environment
  • [ ] Inventoried all custom plugins/modifications
  • [ ] Allocated sufficient time for testing

Breaking Changes

1. Package Structure

What changed: The single maid package has been split into three separate packages with clear responsibilities.

Why it changed: This separation allows: - Using just the engine without game-specific content - Sharing standard components across different games - Creating custom games without modifying engine code

Migration steps:

# Before (v0.1)
from maid import Engine, Room, Player, Command

# After (v0.2)
from maid_engine.core import GameEngine
from maid_engine.core.world import World
from maid_stdlib.components import PositionComponent
from maid_classic_rpg.models import Room, Player

Installation:

# Before (v0.1)
pip install maid

# After (v0.2)
uv add maid-engine maid-stdlib maid-classic-rpg
# Or with extras:
uv sync --all-extras

2. Entity Component System (ECS)

What changed: Game objects (players, rooms, items, NPCs) are now ECS entities with attached components instead of class instances.

Why it changed: ECS provides: - Better composition over inheritance - Easier serialization and persistence - More flexible behavior modification - Better performance for large numbers of entities

Migration steps:

# Before (v0.1) - Class-based entities
class Player:
    def __init__(self, name: str):
        self.name = name
        self.health = 100
        self.mana = 50
        self.inventory = []

player = Player("Hero")
player.health -= 10

# After (v0.2) - ECS-based entities
from maid_engine.core.ecs import Entity
from maid_stdlib.components import (
    HealthComponent,
    ManaComponent,
    InventoryComponent,
    NameComponent,
)

# Create entity and attach components
player = world.create_entity()
player.add_component(NameComponent(name="Hero"))
player.add_component(HealthComponent(current=100, maximum=100))
player.add_component(ManaComponent(current=50, maximum=50))
player.add_component(InventoryComponent())

# Access components
health = player.get_component(HealthComponent)
health.current -= 10

3. Plugin System to Content Packs

What changed: The old plugin system has been replaced with Content Packs, a more structured approach to extending MAID.

Why it changed: Content Packs provide: - Clear dependency management - Standardized lifecycle hooks - Better isolation between content - Layered command override system

Migration steps:

# Before (v0.1) - Old plugin system
class MyPlugin:
    def __init__(self, engine):
        self.engine = engine

    def on_load(self):
        self.engine.register_command("mycommand", self.do_command)

    def do_command(self, player, args):
        player.send("Hello from plugin!")

# Register plugin
engine.load_plugin(MyPlugin)
# After (v0.2) - Content Pack system
from maid_engine.plugins.protocol import ContentPack, ContentPackManifest
from maid_engine.commands.registry import CommandRegistry
from maid_engine.core.world import World
from maid_engine.core.ecs import System

class MyContentPack:
    @property
    def manifest(self) -> ContentPackManifest:
        return ContentPackManifest(
            name="my-content-pack",
            version="1.0.0",
            api_version="0.2",
            description="My custom content",
            author="Your Name",
            dependencies=["maid-stdlib"],
        )

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

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

    def get_events(self) -> list[type]:
        return []

    def register_commands(self, registry: CommandRegistry) -> None:
        registry.register(
            name="mycommand",
            handler=self._do_command,
            category="custom",
            help_text="My custom command",
        )

    def register_document_schemas(self, store) -> None:
        pass

    async def on_load(self, engine) -> None:
        print("Content pack loaded!")

    async def on_unload(self, engine) -> None:
        print("Content pack unloaded!")

    async def _do_command(self, context, args):
        await context.send("Hello from content pack!")

# Load content pack
engine.load_content_pack(MyContentPack())

4. Command System

What changed: Commands now use a layered registry with async handlers and structured contexts.

Why it changed: - Async commands for database/network operations - Priority-based command overriding - Better command help generation - Type-safe command arguments

Migration steps:

# Before (v0.1)
@engine.command("look")
def look_command(player, args):
    room = player.room
    player.send(f"You are in {room.name}")
    player.send(room.description)

# After (v0.2)
from maid_engine.commands.registry import CommandContext

async def look_command(context: CommandContext, args: str) -> None:
    """Look at your surroundings or an object."""
    entity = context.entity
    position = entity.get_component(PositionComponent)

    if position and position.room_id:
        room = context.world.get_room(position.room_id)
        await context.send(f"You are in {room.name}")
        await context.send(room.description)

# Register with priority
registry.register(
    name="look",
    handler=look_command,
    category="movement",
    help_text="Look at your surroundings",
    priority=100,  # Higher priority overrides lower
)

5. Event System

What changed: Events are now strongly typed and use an async event bus.

Why it changed: - Type safety for event handlers - Async event processing - Better event filtering - Clear event lifecycle

Migration steps:

# Before (v0.1)
@engine.on("player_move")
def on_player_move(player, from_room, to_room):
    print(f"{player.name} moved from {from_room} to {to_room}")

# After (v0.2)
from maid_stdlib.events import RoomEnterEvent
from maid_engine.core.events import EventBus

async def on_room_enter(event: RoomEnterEvent) -> None:
    print(f"Entity {event.entity_id} entered room {event.room_id}")

# Subscribe to event
event_bus.subscribe(RoomEnterEvent, on_room_enter)

# Emit event
await event_bus.emit(RoomEnterEvent(
    entity_id=player.id,
    room_id=new_room.id,
    previous_room_id=old_room.id,
))

6. Configuration

What changed: Configuration now uses environment variables with the MAID_ prefix and supports nested settings.

Why it changed: - 12-factor app compliance - Easier deployment configuration - Support for secrets management - Hierarchical configuration

Migration steps:

# Before (v0.1) - config.py file
config = {
    "port": 4000,
    "database": "sqlite:///maid.db",
    "debug": False,
}

# After (v0.2) - Environment variables
# .env file or environment
MAID_TELNET__PORT=4000
MAID_WEB__PORT=8080
MAID_DB__URL=sqlite:///maid.db
MAID_DEBUG=false
MAID_GAME__TICK_RATE=4.0
# Accessing configuration
from maid_engine.config import Settings

settings = Settings()
print(settings.telnet.port)  # 4000
print(settings.game.tick_rate)  # 4.0

7. Async/Await Throughout

What changed: All I/O operations are now async.

Why it changed: - Better scalability - Non-blocking operations - Consistent async patterns - Support for async databases

Migration steps:

# Before (v0.1) - Synchronous
def save_player(player):
    db.execute("UPDATE players SET ...")
    return True

def send_message(player, message):
    player.connection.send(message.encode())

# After (v0.2) - Asynchronous
async def save_player(player_id: str) -> bool:
    await document_store.save("players", player_id, player_data)
    return True

async def send_message(connection, message: str) -> None:
    await connection.send(message)

Deprecated Features

Deprecation Notice

The following features from v0.1 are deprecated in v0.2 and will be removed in v0.3.

Feature Replacement Notes
engine.register_command() CommandRegistry.register() Use content pack command registration
player.room direct access PositionComponent.room_id Use ECS components
engine.on() decorator EventBus.subscribe() Use typed events
config.py files Environment variables Use MAID_ prefix
Player class Entity + Components Use ECS pattern
Room class (direct) Entity + RoomComponent Rooms are now entities

New Features in v0.2

Content Pack System

Create modular, reusable game content.

engine.load_content_pack(StdlibContentPack())
engine.load_content_pack(ClassicRPGContentPack())
engine.load_content_pack(MyCustomContentPack())

Full documentation

Multi-World Support

Run multiple game worlds with portal connections.

world_manager = WorldManager(engine)
fantasy_world = world_manager.create_world("fantasy")
scifi_world = world_manager.create_world("scifi")
world_manager.create_portal(fantasy_world, scifi_world, "dimensional_gate")

AI Integration

Connect AI providers for NPC dialogue and content generation.

MAID_AI__DEFAULT_PROVIDER=anthropic
MAID_AI__ANTHROPIC_API_KEY=sk-...
response = await ai_provider.generate(
    prompt="The shopkeeper greets the adventurer",
    context={"npc": "shopkeeper", "player": player_data}
)

Full documentation

Hot Reload

Develop faster with automatic content pack reloading.

uv run maid server start --hot-reload

Full documentation

Database Migration

v0.2 uses a new document-based storage system.

Export v0.1 Data

# In v0.1 environment
python -m maid.tools.export --output backup.json

Import to v0.2

# In v0.2 environment
uv run maid db import backup.json --format v0.1

Manual Migration

If automatic import fails:

# Migration script example
import json
from maid_engine.storage import DocumentStore

# Load v0.1 data
with open("backup.json") as f:
    old_data = json.load(f)

# Convert to v0.2 format
store = DocumentStore()
for player in old_data["players"]:
    await store.save("players", player["id"], {
        "name": player["name"],
        "components": {
            "health": {"current": player["health"], "maximum": 100},
            "position": {"room_id": player["room_id"]},
            "inventory": {"items": player["inventory"]},
        }
    })

Testing Your Migration

1. Install v0.2

uv sync --all-extras

2. Run Tests

# Run all tests
uv run pytest packages/

# Run with verbose output
uv run pytest packages/ -v

# Run specific package tests
uv run pytest packages/maid-engine/tests/

3. Test Custom Content

# Check content pack compatibility
uv run maid content validate my-content-pack/

# Run content pack tests
uv run pytest my-content-pack/tests/

4. Manual Testing

# Start server in debug mode
uv run maid server start --debug

# Connect and test basic commands
telnet localhost 4000

Rollback Procedure

If you need to rollback to v0.1:

  1. Stop the v0.2 server:

    # Ctrl+C or
    uv run maid server stop
    

  2. Export any new v0.2 data (if needed):

    uv run maid db export --output v0.2-backup.json
    

  3. Restore v0.1 environment:

    # Create separate environment
    python -m venv venv-v0.1
    source venv-v0.1/bin/activate
    pip install maid==0.1.0
    

  4. Restore v0.1 database from backup

  5. Start v0.1 server

Common Issues

ModuleNotFoundError: No module named 'maid'

Problem: The old maid package name no longer exists.

Solution: Update imports to use the new package names:

# Old
from maid import Engine

# New
from maid_engine.core import GameEngine

TypeError: object MyCoolHandler can't be used in 'await' expression

Problem: Command handlers must be async in v0.2.

Solution: Add async to handler functions:

# Old
def my_handler(player, args):
    player.send("Hello")

# New
async def my_handler(context, args):
    await context.send("Hello")

AttributeError: 'Entity' object has no attribute 'health'

Problem: Entities no longer have direct attributes; use components.

Solution: Access data through components:

# Old
player.health -= 10

# New
health = player.get_component(HealthComponent)
health.current -= 10

Configuration not loading

Problem: v0.2 uses environment variables, not config files.

Solution: Set environment variables with MAID_ prefix:

export MAID_TELNET__PORT=4000
# Or use .env file

Getting Help

Next Steps

After successful migration:

  1. Review the Content Pack Guide
  2. Explore the ECS Overview
  3. Check out Tutorials for v0.2 patterns
  4. Consider contributing your migration experience!