Skip to content

Tutorial Part 7: Your First Content Pack

Estimated time: 45 minutes


Welcome to the final chapter of the MAID tutorial series! Throughout this journey, you have learned how to create commands, build rooms and items, design NPCs with AI dialogue, and implement combat systems. Now it is time to package everything into a reusable, shareable content pack.

What You Will Learn

By the end of this tutorial, you will:

  1. Understand what content packs are and why they matter
  2. Know the ContentPack protocol and manifest structure
  3. Create a content pack package with proper structure
  4. Use the CLI scaffolder to generate new packs
  5. Implement lifecycle methods (on_load, on_unload)
  6. Declare dependencies between packs
  7. Build and publish your pack to PyPI
  8. Complete a hands-on exercise: packaging the tutorial dungeon

What is a Content Pack?

A content pack is a self-contained module that adds functionality to a MAID game. It can include:

  • Systems - ECS systems that process entities each tick
  • Commands - Player-executable commands
  • Events - Custom event types for communication
  • Components - Data structures attached to entities
  • Document Schemas - Persistence schemas for the document store
  • Data Files - Configuration, templates, and static content

Why Create Content Packs?

Reason Benefit
Modularity Keep related functionality together
Reusability Share content between projects
Distribution Publish to PyPI for others to use
Dependency Management Declare what your pack needs
Hot Reload Update packs without restarting the server

The Three-Layer Architecture

MAID uses a layered architecture for content:

+----------------------------------------+
|         Your Content Pack              |  <-- Game-specific content
+----------------------------------------+
|           maid-stdlib                  |  <-- Common components
+----------------------------------------+
|           maid-engine                  |  <-- Core infrastructure
+----------------------------------------+
  • maid-engine - Pure infrastructure (ECS, networking, auth, storage)
  • maid-stdlib - Reusable components, base systems, utilities
  • Your Pack - Game-specific content that depends on the above

The ContentPack Protocol

Every content pack must implement the ContentPack protocol. This defines the interface the engine uses to integrate your pack.

Required Interface

from maid_engine.plugins import ContentPack, ContentPackManifest

class MyContentPack:
    """A custom content pack for MAID."""

    @property
    def manifest(self) -> ContentPackManifest:
        """Get the content pack manifest.

        Returns metadata about this pack including name, version,
        dependencies, and capabilities.
        """
        return ContentPackManifest(
            name="my-content-pack",
            version="0.1.0",
            display_name="My Content Pack",
            description="A custom content pack",
            authors=["Your Name"],
            license="MIT",
        )

    def get_dependencies(self) -> list[str]:
        """Get the names of content packs this pack depends on.

        Dependencies must be loaded before this pack can be loaded.
        """
        return ["maid-stdlib"]  # Most packs depend on stdlib

    def get_systems(self, world: World) -> list[System]:
        """Get ECS systems provided by this pack.

        Systems are registered with the engine and updated each tick.
        """
        return [MySystem(world)]

    def get_events(self) -> list[type[Event]]:
        """Get event types defined by this pack."""
        return [MyCustomEvent]

    def register_commands(self, registry: CommandRegistry) -> None:
        """Register commands provided by this pack."""
        from my_pack.commands import register_commands
        register_commands(registry)

    def register_document_schemas(self, store: DocumentStore) -> None:
        """Register document schemas used by this pack."""
        store.register_schema("my_collection", MyModel)

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

        Initialize resources, load data files, subscribe to events.
        """
        pass

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

        Clean up resources, save state, unsubscribe from events.
        """
        pass

Using BaseContentPack

For convenience, you can extend BaseContentPack which provides sensible defaults:

from maid_engine.plugins import BaseContentPack, ContentPackManifest


class MyPack(BaseContentPack):
    """My content pack with minimal boilerplate."""

    @property
    def manifest(self) -> ContentPackManifest:
        return ContentPackManifest(
            name="my-pack",
            version="0.1.0",
            display_name="My Pack",
            description="Custom content",
        )

    # Override only the methods you need
    def get_systems(self, world: World) -> list[System]:
        return [MySystem(world)]

With BaseContentPack, all methods have default implementations that return empty lists or do nothing.

Manifest Structure

The ContentPackManifest contains metadata about your pack:

ContentPackManifest(
    # Required fields
    name="my-pack",           # Unique identifier (lowercase, hyphens allowed)
    version="0.1.0",          # Semantic version string

    # Optional fields
    display_name="My Pack",   # Human-readable name
    description="...",        # Brief description
    authors=["Your Name"],    # List of author names
    license="MIT",            # SPDX license identifier
    homepage="https://...",   # URL to project homepage

    # Dependencies and capabilities
    dependencies={            # Dict of pack_name -> version_constraint
        "maid-stdlib": ">=0.1.0",
    },
    provides=["combat", "magic"],  # Capabilities this pack provides
    requires=["ecs", "events"],    # Capabilities this pack needs

    # Discovery
    keywords=["rpg", "combat"],    # Keywords for searchability
)

Package Structure

A well-organized content pack follows this directory structure:

my-content-pack/
├── pyproject.toml              # Package configuration
├── README.md                   # Documentation
├── src/
│   └── my_content_pack/        # Python package (underscores)
│       ├── __init__.py         # Package exports
│       ├── pack.py             # ContentPack implementation
│       ├── commands/           # Command handlers
│       │   ├── __init__.py
│       │   └── general.py
│       ├── systems/            # ECS systems
│       │   ├── __init__.py
│       │   └── my_system.py
│       ├── components/         # Custom components
│       │   ├── __init__.py
│       │   └── my_component.py
│       ├── events/             # Custom events
│       │   ├── __init__.py
│       │   └── my_events.py
│       └── data/               # Static data files
│           └── config.toml
└── tests/
    ├── __init__.py
    ├── conftest.py             # Test fixtures
    └── test_pack.py            # Pack tests

Key Files

File Purpose
pyproject.toml Package metadata, dependencies, entry points
pack.py ContentPack class implementation
__init__.py Public API exports
commands/ Player commands
systems/ ECS systems for game logic
components/ Custom component definitions
events/ Custom event types
data/ Configuration and static content

Creating the Package

You have two options for creating a new content pack: using the CLI scaffolder (recommended) or manual creation.

MAID provides a scaffolding tool that generates a complete project structure:

# Interactive wizard (recommended for first-time users)
uv run maid plugin new

# Non-interactive with name
uv run maid plugin new my-dungeon-pack

# With all options
uv run maid plugin new my-dungeon-pack \
    --template standard \
    --author "Your Name" \
    --description "A dungeon adventure pack" \
    --output ./plugins \
    --git

Template Types

Template Description Use Case
minimal Just pack.py and pyproject.toml Quick prototypes
standard Adds tests/, README, all directories Recommended
full Adds example files in each directory Learning/reference
system_only Minimal + systems/ System-focused packs
command_only Minimal + commands/ Command-focused packs

Interactive Wizard

When you run maid plugin new without arguments, an interactive wizard guides you:

$ uv run maid plugin new

╭─ MAID Plugin Scaffolder ─╮
│                          │
│ Create a new content     │
│ pack plugin for MAID     │
│                          │
╰──────────────────────────╯

Plugin name (lowercase, hyphens allowed): my-dungeon-pack
Description: A dungeon crawling adventure
Author name: Your Name
Author email: you@example.com
Template [standard]: standard
Output directory [.]: ./plugins

Creating plugin: my-dungeon-pack

Created plugin: ./plugins/my-dungeon-pack

Created 12 files in 5 directories

Key files:
  pyproject.toml - Package configuration
  src/my_dungeon_pack/pack.py - Content pack implementation
  src/my_dungeon_pack/__init__.py - Package exports
  README.md - Documentation
  tests/test_pack.py - Test suite

Option 2: Manual Creation

If you prefer to create the structure manually:

Step 1: Create Directory Structure

mkdir -p my-content-pack/src/my_content_pack/{commands,systems,components,events,data}
mkdir -p my-content-pack/tests
touch my-content-pack/src/my_content_pack/__init__.py
touch my-content-pack/src/my_content_pack/pack.py
touch my-content-pack/tests/__init__.py

Step 2: Create pyproject.toml

[project]
name = "my-content-pack"
version = "0.1.0"
description = "My custom MAID content pack"
authors = [{name = "Your Name", email = "you@example.com"}]
license = {text = "MIT"}
requires-python = ">=3.12"
dependencies = [
    "maid-engine>=0.1.0",
    "maid-stdlib>=0.1.0",
]

# Entry point for MAID to discover your pack
[project.entry-points."maid.content_packs"]
my-content-pack = "my_content_pack.pack:MyContentPack"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/my_content_pack"]

[tool.hatch.build.targets.sdist]
include = ["src"]

Important: The entry point key is your pack's name, and the value is the module path to your ContentPack class.

Step 3: Create pack.py

"""My Content Pack for MAID."""

from __future__ import annotations

from typing import TYPE_CHECKING

from maid_engine.plugins import BaseContentPack, ContentPackManifest

if TYPE_CHECKING:
    from maid_engine.commands.registry import CommandRegistry
    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.storage.document_store import DocumentStore

__all__ = ["MyContentPack"]


class MyContentPack(BaseContentPack):
    """My custom content pack."""

    @property
    def manifest(self) -> ContentPackManifest:
        return ContentPackManifest(
            name="my-content-pack",
            version="0.1.0",
            display_name="My Content Pack",
            description="A custom MAID content pack",
            authors=["Your Name"],
            license="MIT",
        )

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

    async def on_load(self, engine: GameEngine) -> None:
        print(f"Loaded {self.manifest.display_name}")

    async def on_unload(self, engine: GameEngine) -> None:
        print(f"Unloaded {self.manifest.display_name}")

Step 4: Create init.py

"""My Content Pack - A custom MAID content pack."""

from my_content_pack.pack import MyContentPack

__all__ = ["MyContentPack"]
__version__ = "0.1.0"

Implementing ContentPack Methods

Now let's implement each method of the ContentPack protocol in detail.

get_systems()

Systems are the heart of game logic. They process entities each tick.

from my_content_pack.systems.respawn import RespawnSystem
from my_content_pack.systems.weather import WeatherSystem


def get_systems(self, world: World) -> list[System]:
    """Get ECS systems provided by this pack."""
    return [
        WeatherSystem(world),
        RespawnSystem(world),
    ]

Example System

# systems/weather.py
from maid_engine.core.ecs import System


class WeatherSystem(System):
    """Manages dynamic weather in the game world."""

    priority = 100  # Lower = runs earlier

    def __init__(self, world):
        super().__init__(world)
        self.current_weather = "clear"
        self.weather_timer = 0.0

    def update(self, delta: float) -> None:
        """Called each tick."""
        self.weather_timer += delta

        # Change weather every 5 minutes
        if self.weather_timer >= 300.0:
            self.weather_timer = 0.0
            self._change_weather()

    def _change_weather(self) -> None:
        import random
        weathers = ["clear", "cloudy", "rain", "storm"]
        self.current_weather = random.choice(weathers)
        # Emit event for other systems
        self.world.emit(WeatherChangedEvent(self.current_weather))

get_events()

Custom events allow systems and commands to communicate.

from my_content_pack.events import WeatherChangedEvent, TreasureFoundEvent


def get_events(self) -> list[type[Event]]:
    """Get event types defined by this pack."""
    return [
        WeatherChangedEvent,
        TreasureFoundEvent,
    ]

Example Event

# events/weather.py
from dataclasses import dataclass
from maid_engine.core.events import Event


@dataclass
class WeatherChangedEvent(Event):
    """Emitted when weather changes."""
    new_weather: str
    previous_weather: str | None = None

register_commands()

Commands are how players interact with your content.

def register_commands(self, registry: CommandRegistry) -> None:
    """Register commands provided by this pack."""
    from my_content_pack.commands import register_all_commands
    register_all_commands(registry, pack_name="my-content-pack")

Example Command Registration

# commands/__init__.py
from maid_engine.commands import command, CommandContext


def register_all_commands(registry, pack_name: str) -> None:
    """Register all commands from this pack."""

    @command(
        name="weather",
        category="information",
        help_text="Check the current weather.",
    )
    async def cmd_weather(ctx: CommandContext) -> bool:
        weather_system = ctx.world.systems.get(WeatherSystem)
        if weather_system:
            await ctx.send(f"The weather is currently {weather_system.current_weather}.")
        else:
            await ctx.send("Weather information is not available.")
        return True

    registry.register(cmd_weather, pack_name=pack_name)

register_document_schemas()

If your pack needs to persist data, register schemas with the document store.

def register_document_schemas(self, store: DocumentStore) -> None:
    """Register document schemas used by this pack."""
    from my_content_pack.models import PlayerProgress, GuildData

    store.register_schema("player_progress", PlayerProgress)
    store.register_schema("guilds", GuildData)

Lifecycle Methods: on_load and on_unload

These async methods handle pack initialization and cleanup.

on_load

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

    Use this to:
    - Initialize resources
    - Load data files
    - Subscribe to events
    - Set up background tasks
    """
    # Load configuration
    config_path = Path(__file__).parent / "data" / "config.toml"
    self._config = load_config(config_path)

    # Subscribe to events
    engine.world.event_bus.subscribe(
        PlayerJoinedEvent,
        self._on_player_joined
    )

    # Initialize any persistent state
    self._leaderboard = await engine.document_store.get_collection("leaderboard")

    print(f"{self.manifest.display_name} v{self.manifest.version} loaded!")

on_unload

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

    Use this to:
    - Clean up resources
    - Save state
    - Unsubscribe from events
    - Cancel background tasks
    """
    # Unsubscribe from events
    engine.world.event_bus.unsubscribe(
        PlayerJoinedEvent,
        self._on_player_joined
    )

    # Save any pending state
    if self._leaderboard:
        await self._leaderboard.flush()

    print(f"{self.manifest.display_name} unloaded.")

Dependency Declaration

Content packs can depend on other packs. Dependencies are loaded first, ensuring your pack has access to the components and systems it needs.

Declaring Dependencies

def get_dependencies(self) -> list[str]:
    """Get the names of content packs this pack depends on."""
    return [
        "maid-stdlib",      # Most packs need stdlib
        "maid-classic-rpg", # If using RPG systems
    ]

Dependency Resolution

The engine uses topological sorting to load packs in the correct order:

1. maid-engine (core, always loaded first)
2. maid-stdlib (no dependencies)
3. maid-classic-rpg (depends on maid-stdlib)
4. my-content-pack (depends on maid-stdlib and maid-classic-rpg)

Checking Dependency Availability

In your on_load, you can verify dependencies are available:

async def on_load(self, engine: GameEngine) -> None:
    # Check for optional dependency
    classic_rpg = engine.get_pack("maid-classic-rpg")
    if classic_rpg:
        # Enable combat integration
        self._enable_combat_features(engine)
    else:
        print("Combat features disabled (maid-classic-rpg not loaded)")

Circular Dependencies

MAID detects circular dependencies and raises a DependencyError:

DependencyError: Circular dependency detected involving 'pack-a'
  pack-a -> pack-b -> pack-c -> pack-a

Testing Your Content Pack

MAID provides testing utilities to verify your pack works correctly.

Basic Pack Tests

# tests/test_pack.py
import pytest

from my_content_pack import MyContentPack
from maid_engine.plugins import ContentPackManifest


class TestMyContentPack:
    """Tests for MyContentPack."""

    def test_manifest_exists(self) -> None:
        """Content pack has a valid manifest."""
        pack = MyContentPack()
        manifest = pack.manifest

        assert isinstance(manifest, ContentPackManifest)
        assert manifest.name == "my-content-pack"
        assert manifest.version == "0.1.0"

    def test_dependencies_returns_list(self) -> None:
        """get_dependencies returns a list."""
        pack = MyContentPack()
        deps = pack.get_dependencies()

        assert isinstance(deps, list)
        assert "maid-stdlib" in deps

    def test_events_returns_list(self) -> None:
        """get_events returns a list."""
        pack = MyContentPack()
        events = pack.get_events()

        assert isinstance(events, list)

Using Test Utilities

MAID provides mock objects for testing:

import pytest

from maid_engine.plugins.testing import (
    MockEngine,
    MockWorld,
    run_pack_lifecycle,
)
from my_content_pack import MyContentPack


class TestPackLifecycle:
    """Test pack lifecycle methods."""

    @pytest.mark.asyncio
    async def test_on_load_succeeds(self) -> None:
        """on_load completes without error."""
        pack = MyContentPack()

        async with MockEngine() as engine:
            await pack.on_load(engine)
            # Verify initialization happened
            assert hasattr(pack, "_config")

    @pytest.mark.asyncio
    async def test_full_lifecycle(self) -> None:
        """Pack can be loaded and unloaded cleanly."""
        pack = MyContentPack()
        result = await run_pack_lifecycle(pack)

        assert result.loaded_successfully
        assert result.unloaded_successfully
        assert not result.errors

Running Tests

# Run all tests for your pack
uv run pytest tests/

# Run with coverage
uv run pytest tests/ --cov=src/my_content_pack

# Run specific test
uv run pytest tests/test_pack.py::TestMyContentPack::test_manifest_exists

Building and Publishing

Once your pack is ready, you can build and publish it for others to use.

Building with uv

# Build the package
uv build

# This creates:
# dist/my_content_pack-0.1.0-py3-none-any.whl
# dist/my_content_pack-0.1.0.tar.gz

Installing Locally

Test your built package locally:

# Install from built wheel
uv pip install dist/my_content_pack-0.1.0-py3-none-any.whl

# Or install in development mode
uv pip install -e .

Verifying Discovery

After installation, verify MAID can discover your pack:

# List discovered content packs
uv run maid pack list

# Show pack details
uv run maid pack status my-content-pack

Expected output:

Discovered Content Packs
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Name              ┃ Version ┃ Display Name       ┃ Dependencies  ┃
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ maid-stdlib       │ 0.1.0   │ Standard Library   │ -             │
│ my-content-pack   │ 0.1.0   │ My Content Pack    │ maid-stdlib   │
└───────────────────┴─────────┴────────────────────┴───────────────┘

Total: 2 pack(s)

Publishing to PyPI

When ready to share with the world:

# Build the package
uv build

# Upload to PyPI (requires PyPI account and API token)
uv publish

# Or upload to TestPyPI first
uv publish --repository testpypi

Installation by Others

Once published, others can install your pack:

# Install from PyPI
uv pip install my-content-pack

# Or add to their project
uv add my-content-pack

Exercise: Package the Tutorial Dungeon

Let's package the dungeon you built in Part 4 into a reusable content pack!

Step 1: Create the Pack Structure

# Create the pack using the scaffolder
uv run maid plugin new tutorial-dungeon \
    --template standard \
    --author "Your Name" \
    --description "A tutorial dungeon adventure"

cd tutorial-dungeon

Step 2: Add Dungeon Data

Create a data file with your dungeon rooms:

# src/tutorial_dungeon/data/rooms.py
"""Tutorial dungeon room definitions."""

ROOMS = {
    "entrance": {
        "name": "Dungeon Entrance",
        "description": (
            "You descend into a damp stone passage. Moss covers the ancient "
            "walls, and the air smells of earth and decay. Flickering "
            "torchlight reveals passages leading deeper into the darkness."
        ),
        "exits": {"north": "guard_room", "west": "prison_cell", "east": "boss_chamber"},
    },
    "guard_room": {
        "name": "Guard Room",
        "description": (
            "An abandoned guard post. Rusty weapons hang on the walls, and "
            "a battered table sits in the corner. Empty bottles suggest "
            "the guards left in a hurry."
        ),
        "exits": {"south": "entrance", "east": "treasury"},
        "items": ["torch"],
    },
    "treasury": {
        "name": "Treasury",
        "description": (
            "Piles of gold coins glitter in the torchlight. Jewel-encrusted "
            "goblets and ancient artifacts line the shelves. This must have "
            "been the dragon's hoard."
        ),
        "exits": {"west": "guard_room"},
        "items": ["treasure_chest"],
        "locked_from": "guard_room",  # Door is locked
    },
    "prison_cell": {
        "name": "Prison Cell",
        "description": (
            "A cramped cell with rusted iron bars. Bones litter the floor, "
            "and chains hang from the walls. Whatever was imprisoned here "
            "is long gone."
        ),
        "exits": {"east": "entrance"},
        "items": ["skeleton_key"],
    },
    "boss_chamber": {
        "name": "Boss Chamber",
        "description": (
            "A vast cavern with a high vaulted ceiling. Bones of previous "
            "adventurers crunch underfoot. In the center, upon a throne of "
            "skulls, waits the dungeon's master."
        ),
        "exits": {"west": "entrance"},
        "hidden": True,  # Hidden until discovered
        "locked_from": "entrance",
    },
}

ITEMS = {
    "torch": {
        "name": "Burning Torch",
        "description": "A torch burning with a bright, steady flame.",
        "item_type": "misc",
        "keywords": ["torch", "light", "fire"],
    },
    "skeleton_key": {
        "name": "Skeleton Key",
        "description": "An ancient key made of yellowed bone.",
        "item_type": "key",
        "keywords": ["key", "bone", "skeleton"],
    },
    "treasure_chest": {
        "name": "Treasure Chest",
        "description": "A massive iron-bound chest overflowing with gold.",
        "item_type": "container",
        "keywords": ["chest", "treasure", "gold"],
        "capacity": 20,
    },
}

Step 3: Create a Dungeon Builder System

# src/tutorial_dungeon/systems/dungeon_builder.py
"""System for building and managing the tutorial dungeon."""

from uuid import uuid4
from maid_engine.core.ecs import System
from tutorial_dungeon.data.rooms import ROOMS, ITEMS


class DungeonBuilderSystem(System):
    """Builds the tutorial dungeon on game start."""

    priority = 10  # Run early

    def __init__(self, world):
        super().__init__(world)
        self._built = False
        self._room_ids: dict[str, str] = {}

    def update(self, delta: float) -> None:
        """Build dungeon on first tick if not already built."""
        if not self._built:
            self._build_dungeon()
            self._built = True

    def _build_dungeon(self) -> None:
        """Create all dungeon rooms and items."""
        # First pass: create rooms
        for room_key, room_data in ROOMS.items():
            room_id = uuid4()
            self._room_ids[room_key] = str(room_id)

            self.world.create_room(
                room_id=room_id,
                name=room_data["name"],
                description=room_data["description"],
            )

        # Second pass: connect exits
        for room_key, room_data in ROOMS.items():
            room_id = self._room_ids[room_key]
            for direction, dest_key in room_data.get("exits", {}).items():
                dest_id = self._room_ids.get(dest_key)
                if dest_id:
                    self.world.add_exit(room_id, direction, dest_id)

        # Third pass: create items
        for room_key, room_data in ROOMS.items():
            room_id = self._room_ids[room_key]
            for item_key in room_data.get("items", []):
                item_data = ITEMS.get(item_key)
                if item_data:
                    self._create_item(item_data, room_id)

        print(f"Built tutorial dungeon: {len(self._room_ids)} rooms")

    def _create_item(self, item_data: dict, room_id: str) -> None:
        """Create an item in a room."""
        from maid_stdlib.components import DescriptionComponent, ItemComponent

        entity = self.world.create_entity()
        entity.add_tag("item")

        entity.add(DescriptionComponent(
            name=item_data["name"],
            long_desc=item_data["description"],
            keywords=item_data.get("keywords", []),
        ))

        entity.add(ItemComponent(
            item_type=item_data["item_type"],
        ))

        self.world.place_entity_in_room(entity.id, room_id)

    def get_entrance_id(self) -> str | None:
        """Get the entrance room ID."""
        return self._room_ids.get("entrance")

Step 4: Update pack.py

# src/tutorial_dungeon/pack.py
"""Tutorial Dungeon content pack for MAID."""

from __future__ import annotations

from typing import TYPE_CHECKING

from maid_engine.plugins import BaseContentPack, ContentPackManifest

if TYPE_CHECKING:
    from maid_engine.core.ecs.system import System
    from maid_engine.core.engine import GameEngine
    from maid_engine.core.world import World

__all__ = ["TutorialDungeonPack"]


class TutorialDungeonPack(BaseContentPack):
    """Tutorial dungeon content pack."""

    @property
    def manifest(self) -> ContentPackManifest:
        return ContentPackManifest(
            name="tutorial-dungeon",
            version="0.1.0",
            display_name="Tutorial Dungeon",
            description="A dungeon adventure from the MAID tutorial",
            authors=["Your Name"],
            license="MIT",
            keywords=["tutorial", "dungeon", "adventure"],
        )

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

    def get_systems(self, world: World) -> list[System]:
        from tutorial_dungeon.systems.dungeon_builder import DungeonBuilderSystem
        return [DungeonBuilderSystem(world)]

    async def on_load(self, engine: GameEngine) -> None:
        print(f"Loading {self.manifest.display_name}...")
        # The dungeon will be built on the first tick

    async def on_unload(self, engine: GameEngine) -> None:
        print(f"Unloading {self.manifest.display_name}...")

Step 5: Add Tests

# tests/test_pack.py
import pytest

from tutorial_dungeon import TutorialDungeonPack
from tutorial_dungeon.data.rooms import ROOMS, ITEMS


class TestTutorialDungeonPack:
    """Tests for the tutorial dungeon pack."""

    def test_manifest(self) -> None:
        """Pack has valid manifest."""
        pack = TutorialDungeonPack()
        assert pack.manifest.name == "tutorial-dungeon"
        assert pack.manifest.version == "0.1.0"

    def test_dependencies(self) -> None:
        """Pack declares stdlib dependency."""
        pack = TutorialDungeonPack()
        deps = pack.get_dependencies()
        assert "maid-stdlib" in deps


class TestDungeonData:
    """Tests for dungeon data files."""

    def test_rooms_defined(self) -> None:
        """All expected rooms are defined."""
        expected = ["entrance", "guard_room", "treasury", "prison_cell", "boss_chamber"]
        for room_key in expected:
            assert room_key in ROOMS, f"Missing room: {room_key}"

    def test_rooms_have_descriptions(self) -> None:
        """All rooms have descriptions."""
        for room_key, room_data in ROOMS.items():
            assert "name" in room_data, f"{room_key} missing name"
            assert "description" in room_data, f"{room_key} missing description"

    def test_exits_reference_valid_rooms(self) -> None:
        """All exits point to valid rooms."""
        for room_key, room_data in ROOMS.items():
            for direction, dest in room_data.get("exits", {}).items():
                assert dest in ROOMS, f"{room_key} exit {direction} -> invalid {dest}"

    def test_items_defined(self) -> None:
        """Items referenced by rooms exist."""
        for room_key, room_data in ROOMS.items():
            for item_key in room_data.get("items", []):
                assert item_key in ITEMS, f"{room_key} references missing item: {item_key}"

Step 6: Build and Test

# Run tests
uv run pytest tests/ -v

# Build the package
uv build

# Install locally
uv pip install -e .

# Verify it shows up
uv run maid pack list

Congratulations!

You have created a complete, testable, distributable content pack. You can now:

  • Share the wheel file with friends
  • Publish to PyPI for the world to use
  • Continue adding features like NPCs, quests, and treasure

What's Next?

You have completed the MAID tutorial series. You now know how to:

  • Install and configure MAID
  • Understand the architecture (ECS, events, tick loop)
  • Create commands for player interaction
  • Build rooms and items
  • Design NPCs with AI-powered dialogue
  • Implement combat and skill systems
  • Package everything into a shareable content pack

Advanced Topics to Explore

Topic Description
Hot Reload Guide Update packs without restarting
Building Commands In-game world building
Command System Guide Advanced command features
NPC Dialogue Guide AI dialogue integration
Admin API Reference REST/WebSocket admin interface

Community Resources


< Previous: Part 6 - Combat & Skills Back to Tutorial Index


Quick Reference

ContentPack Protocol Methods

Method Purpose Return Type
manifest Pack metadata ContentPackManifest
get_dependencies() Required packs list[str]
get_systems(world) ECS systems list[System]
get_events() Event types list[type[Event]]
register_commands(registry) Commands None
register_document_schemas(store) Persistence None
on_load(engine) Initialize None (async)
on_unload(engine) Cleanup None (async)

CLI Commands

Command Description
maid plugin new Create new content pack
maid pack list List discovered packs
maid pack status <name> Show pack details
maid pack reload <name> Hot reload a pack

pyproject.toml Entry Point

[project.entry-points."maid.content_packs"]
my-pack = "my_pack.pack:MyContentPack"

Template Types

Type Files Created
minimal pack.py, pyproject.toml, init.py
standard + tests/, README.md, all subdirs
full + example files in each subdir
system_only minimal + systems/
command_only minimal + commands/

Package Structure

my-pack/
├── pyproject.toml
├── README.md
├── src/my_pack/
│   ├── __init__.py
│   ├── pack.py
│   ├── commands/
│   ├── systems/
│   ├── components/
│   ├── events/
│   └── data/
└── tests/