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¶
- MAID installed and running (see Installation)
- Basic familiarity with Python and async/await
- Understanding of Core Concepts
What We're Building¶
We'll create a "Greetings Pack" that:
- Adds a
greetcommand that lets players greet each other - Emits a
GreetingEventwhen someone is greeted - Tracks greeting statistics
Project Structure¶
Create a new directory for your content pack:
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:
Next Steps¶
Congratulations! You've created your first content pack. Here's what to explore next:
- Content Pack Overview - Learn about the content pack architecture
- Creating Systems - Add custom game logic with ECS systems
- Persistence - Store data with the document store
- Publishing - Share your content pack with others
Complete Example¶
The complete example is available in the MAID repository under examples/greetings-pack/.