Skip to content

Content Pack System

Content packs are the modular building blocks of MAID games.

Overview

A content pack is a self-contained module that provides:

  • Systems - Game logic (combat, crafting, etc.)
  • Commands - Player commands (attack, craft, etc.)
  • Events - Custom events for the pack
  • Components - Data types for game objects
  • Document Schemas - Persistence definitions

What Content Packs Provide

Feature Description Example
Systems ECS systems that process entities CombatSystem, QuestSystem
Commands Player-facing commands attack, craft, quest
Events Custom event types CombatStartedEvent
Components Entity data types WeaponComponent
Schemas Database collections Character save format
Connection Handler Login/gameplay flow Character selection

The ContentPack Protocol

from maid_engine.plugins.protocol import ContentPack, ContentPackManifest
from maid_engine.core.world import World
from maid_engine.core.ecs import System
from maid_engine.commands.registry import CommandRegistry
from maid_engine.storage.document_store import DocumentStore

class MyContentPack(ContentPack):
    @property
    def manifest(self) -> ContentPackManifest:
        return ContentPackManifest(
            name="my-pack",
            version="1.0.0",
            description="My custom content pack",
            author="Your Name",
        )

    def get_dependencies(self) -> list[str]:
        """Declare which packs must be loaded before this one."""
        return ["maid-stdlib"]

    def get_systems(self, world: World) -> list[System]:
        """Return systems to register with the world."""
        return [
            MyCustomSystem(world),
            AnotherSystem(world),
        ]

    def get_events(self) -> list[type[Event]]:
        """Return custom event types for documentation."""
        return [MyCustomEvent, AnotherEvent]

    def register_commands(self, registry: CommandRegistry) -> None:
        """Register commands with the command registry."""
        registry.register("mycommand", my_command_handler, pack="my-pack")

    def register_document_schemas(self, store: DocumentStore) -> None:
        """Register database collections and schemas."""
        store.register_collection("my_data", MyDataSchema)

    async def on_load(self, engine: GameEngine) -> None:
        """Called when the pack is loaded. Initialize resources here."""
        self._engine = engine
        # Load data, start background tasks, etc.

    async def on_unload(self, engine: GameEngine) -> None:
        """Called when the pack is unloaded. Clean up here."""
        # Stop background tasks, release resources
        pass

Dependencies and Loading Order

Content packs declare dependencies, and MAID loads them in the correct order:

# maid-stdlib has no dependencies
class StdlibContentPack(ContentPack):
    def get_dependencies(self) -> list[str]:
        return []

# maid-classic-rpg depends on maid-stdlib
class ClassicRPGContentPack(ContentPack):
    def get_dependencies(self) -> list[str]:
        return ["maid-stdlib"]

# Your pack can depend on both
class MyContentPack(ContentPack):
    def get_dependencies(self) -> list[str]:
        return ["maid-stdlib", "maid-classic-rpg"]

Loading order matters - dependencies must be loaded first:

engine = GameEngine(settings)

# Correct order
engine.load_content_pack(StdlibContentPack())      # First - no deps
engine.load_content_pack(ClassicRPGContentPack())  # Second - needs stdlib
engine.load_content_pack(MyContentPack())          # Third - needs both

# Wrong order raises ValueError
engine.load_content_pack(MyContentPack())  # Error: dependency not loaded

Command Priority

Later-loaded packs can override commands from earlier packs:

# maid-stdlib registers "look" at default priority
registry.register("look", basic_look_handler)

# maid-classic-rpg can override with enhanced version
registry.register("look", enhanced_look_handler, priority=10)

# Your pack can override again
registry.register("look", custom_look_handler, priority=20)

Higher priority commands are executed first. If a handler returns True, the command is considered handled and lower-priority handlers don't run.

Creating a Content Pack

1. Define the Manifest

@property
def manifest(self) -> ContentPackManifest:
    return ContentPackManifest(
        name="my-awesome-pack",
        version="1.0.0",
        description="Adds awesome features to MAID",
        author="Your Name",
        homepage="https://github.com/you/my-awesome-pack",
        license="MIT",
    )

2. Implement Systems

from maid_engine.core.ecs import System

class AwesomeSystem(System):
    priority = 50

    async def update(self, delta: float) -> None:
        # Process entities each tick
        for entity in self.entities.with_components(AwesomeComponent):
            component = entity.get(AwesomeComponent)
            # Do awesome things

3. Register Commands

def register_commands(self, registry: CommandRegistry) -> None:
    from .commands import cmd_awesome, cmd_super_awesome

    registry.register("awesome", cmd_awesome, pack=self.manifest.name)
    registry.register("superawesome", cmd_super_awesome, pack=self.manifest.name)

4. Package for Distribution

my-awesome-pack/
    pyproject.toml
    src/
        my_awesome_pack/
            __init__.py
            pack.py          # ContentPack class
            systems/
                __init__.py
                awesome.py
            commands/
                __init__.py
                awesome.py
            components/
                __init__.py
                awesome.py

Hot Reloading

Content packs can be reloaded without restarting the server:

# Reload a pack during development
await engine.hot_reload.reload_pack("my-awesome-pack")

The hot reload system: 1. Pauses the game loop 2. Unregisters the pack's systems, commands, and events 3. Reloads Python modules 4. Re-registers everything 5. Resumes the game loop

Further Reading