Skip to content

Your First Content Pack

This tutorial walks you through creating a simple content pack for MAID. By the end, you will have a working content pack that adds a custom command and event to your game.

Prerequisites

What We're Building

We'll create a "Greetings Pack" that:

  1. Adds a greet command that lets players greet each other
  2. Emits a GreetingEvent when someone is greeted
  3. Tracks greeting statistics

Project Structure

Create a new directory for your content pack:

my-greetings-pack/
    __init__.py
    pack.py
    commands.py
    events.py
    manifest.toml

Step 1: Create the Manifest

The manifest describes your content pack's metadata and dependencies.

# manifest.toml
[pack]
name = "greetings-pack"
version = "1.0.0"
display_name = "Greetings Pack"
description = "Adds greeting commands and events"
authors = ["Your Name"]
license = "MIT"

[pack.dependencies]
stdlib = ">=0.1.0"

[pack.capabilities]
provides = ["greeting-system"]
requires = ["ecs", "event-bus"]

Step 2: Define the Event

Events allow systems to communicate without direct coupling.

# events.py
from dataclasses import dataclass
from uuid import UUID

from maid_engine.core.events import Event


@dataclass
class GreetingEvent(Event):
    """Emitted when a player greets another player."""

    greeter_id: UUID
    target_id: UUID
    greeting_type: str = "wave"  # wave, bow, handshake, etc.

Step 3: Create the Command

Commands are how players interact with your content pack.

# commands.py
from maid_engine.commands.registry import CommandContext, LayeredCommandRegistry
from maid_stdlib.components import DescriptionComponent, PositionComponent

from .events import GreetingEvent


async def greet_command(ctx: CommandContext) -> bool:
    """Greet another player in the room."""
    if not ctx.args:
        await ctx.session.send("Usage: greet <player> [type]")
        await ctx.session.send("Types: wave, bow, handshake, nod")
        return True

    target_name = ctx.args[0].lower()
    greeting_type = ctx.args[1] if len(ctx.args) > 1 else "wave"

    # Get the greeter's position
    greeter = ctx.world.entities.get(ctx.player_id)
    if not greeter or not greeter.has(PositionComponent):
        await ctx.session.send("You need to be somewhere to greet someone!")
        return True

    greeter_pos = greeter.get(PositionComponent)
    greeter_desc = greeter.try_get(DescriptionComponent)
    greeter_name = str(greeter_desc.name) if greeter_desc else "Someone"

    # Find the target in the same room
    target_entity = None
    for entity in ctx.world.entities.with_components(PositionComponent, DescriptionComponent):
        if entity.id == ctx.player_id:
            continue
        pos = entity.get(PositionComponent)
        desc = entity.get(DescriptionComponent)
        if pos.room_id == greeter_pos.room_id and target_name in str(desc.name).lower():
            target_entity = entity
            break

    if not target_entity:
        await ctx.session.send(f"You don't see '{target_name}' here.")
        return True

    target_desc = target_entity.get(DescriptionComponent)
    target_name_display = str(target_desc.name)

    # Format the greeting message based on type
    messages = {
        "wave": f"{greeter_name} waves at {target_name_display}.",
        "bow": f"{greeter_name} bows respectfully to {target_name_display}.",
        "handshake": f"{greeter_name} extends a hand to {target_name_display}.",
        "nod": f"{greeter_name} nods at {target_name_display}.",
    }
    message = messages.get(greeting_type, f"{greeter_name} greets {target_name_display}.")

    # Send message to the room
    await ctx.session.send(message)

    # Emit the greeting event
    event = GreetingEvent(
        greeter_id=ctx.player_id,
        target_id=target_entity.id,
        greeting_type=greeting_type,
    )
    await ctx.world.events.emit(event)

    return True


def register_commands(registry: LayeredCommandRegistry, pack_name: str) -> None:
    """Register all commands from this pack."""
    registry.register(
        name="greet",
        handler=greet_command,
        pack_name=pack_name,
        aliases=["wave", "bow"],
        category="social",
        description="Greet another player",
        usage="greet <player> [wave|bow|handshake|nod]",
    )

Step 4: Create the Content Pack Class

The pack class ties everything together.

# pack.py
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from maid_engine.commands.registry import CommandRegistry, LayeredCommandRegistry
    from maid_engine.core.ecs.system import System
    from maid_engine.core.engine import GameEngine
    from maid_engine.core.events import Event
    from maid_engine.core.world import World
    from maid_engine.plugins.manifest import ContentPackManifest
    from maid_engine.storage.document_store import DocumentStore

    AnyCommandRegistry = CommandRegistry | LayeredCommandRegistry


class GreetingsContentPack:
    """Greetings content pack - adds social greeting commands."""

    @property
    def manifest(self) -> ContentPackManifest:
        """Get pack manifest."""
        from maid_engine.plugins.manifest import ContentPackManifest

        return ContentPackManifest(
            name="greetings-pack",
            version="1.0.0",
            display_name="Greetings Pack",
            description="Adds greeting commands and events",
            dependencies={"stdlib": ">=0.1.0"},
            provides=["greeting-system"],
            requires=["ecs", "event-bus"],
        )

    def get_dependencies(self) -> list[str]:
        """Get pack dependencies."""
        return ["stdlib"]

    def get_systems(self, world: World) -> list[System]:
        """Get systems provided by this pack."""
        # This simple pack doesn't need custom systems
        return []

    def get_events(self) -> list[type[Event]]:
        """Get events defined by this pack."""
        from .events import GreetingEvent
        return [GreetingEvent]

    def register_commands(self, registry: AnyCommandRegistry) -> None:
        """Register commands provided by this pack."""
        from .commands import register_commands
        register_commands(registry, pack_name="greetings-pack")

    def register_document_schemas(self, store: DocumentStore) -> None:
        """Register document schemas used by this pack."""
        # No persistence needed for this simple pack
        pass

    async def on_load(self, engine: GameEngine) -> None:
        """Called when pack is loaded."""
        print("Greetings Pack loaded!")

    async def on_unload(self, engine: GameEngine) -> None:
        """Called when pack is unloaded."""
        print("Greetings Pack unloaded!")

Step 5: Create the Package Init

# __init__.py
"""Greetings content pack for MAID."""

from .pack import GreetingsContentPack

__all__ = ["GreetingsContentPack"]

Step 6: Load Your Content Pack

There are two ways to load your content pack:

Option A: Programmatic Loading

from maid_engine.core.engine import GameEngine
from maid_stdlib.pack import StdlibContentPack
from my_greetings_pack import GreetingsContentPack

# Create engine
engine = GameEngine(settings)

# Load packs in dependency order
engine.load_content_pack(StdlibContentPack())
engine.load_content_pack(GreetingsContentPack())

# Start engine
await engine.start()

Option B: Entry Point Discovery

Add an entry point to your pyproject.toml:

[project.entry-points."maid.content_packs"]
greetings-pack = "my_greetings_pack:GreetingsContentPack"

Then MAID will automatically discover and load your pack.

Testing Your Content Pack

Create a simple test to verify your pack works:

# tests/test_greetings.py
import pytest
from uuid import uuid4

from maid_engine.core.world import World
from maid_engine.storage.document_store import InMemoryDocumentStore
from maid_stdlib.components import DescriptionComponent, PositionComponent

from my_greetings_pack.events import GreetingEvent


@pytest.fixture
def world():
    """Create a test world."""
    return World()


@pytest.mark.asyncio
async def test_greeting_event(world):
    """Test that greeting events are emitted correctly."""
    received_events = []

    async def handler(event: GreetingEvent):
        received_events.append(event)

    world.events.subscribe(GreetingEvent, handler)

    # Emit a test event
    event = GreetingEvent(
        greeter_id=uuid4(),
        target_id=uuid4(),
        greeting_type="wave",
    )
    await world.events.emit(event)

    assert len(received_events) == 1
    assert received_events[0].greeting_type == "wave"

Run tests with:

uv run pytest tests/test_greetings.py

Next Steps

Congratulations! You've created your first content pack. Here's what to explore next:

Complete Example

The complete example is available in the MAID repository under examples/greetings-pack/.