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:
- Understand what content packs are and why they matter
- Know the ContentPack protocol and manifest structure
- Create a content pack package with proper structure
- Use the CLI scaffolder to generate new packs
- Implement lifecycle methods (on_load, on_unload)
- Declare dependencies between packs
- Build and publish your pack to PyPI
- 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.
Option 1: Using the CLI Scaffolder (Recommended)¶
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:
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¶
- Documentation: Full reference at maid.dev/docs
- GitHub: github.com/maid-mud/maid
- Discord: Join the community for help and discussions
< 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¶
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/ |