Skip to content

Content Pack System

MAID uses a content pack system to separate game content from engine infrastructure. Content packs can provide systems, commands, events, and data.

Content Pack Architecture

flowchart TB
    subgraph Pack["Content Pack"]
        M[Manifest<br/>name, version, deps]
        SY[Systems]
        CMD[Commands]
        EV[Events]
        SCH[Schemas]
        HL[Lifecycle Hooks]
    end

    subgraph Engine["Game Engine"]
        SM[System Manager]
        CR[Command Registry]
        EB[Event Bus]
        DS[Document Store]
    end

    M --> Engine
    SY --> SM
    CMD --> CR
    EV --> EB
    SCH --> DS
    HL --> Engine

Creating a Content Pack

from maid_engine.plugins.protocol import BaseContentPack
from maid_engine.plugins.manifest import ContentPackManifest

class MyContentPack(BaseContentPack):
    """Custom content pack example."""

    @property
    def manifest(self) -> ContentPackManifest:
        return ContentPackManifest(
            name="my-pack",
            version="1.0.0",
            display_name="My Pack",
            description="Custom game content",
            dependencies={"stdlib": ">=0.1.0"},  # Depends on stdlib
            provides=["custom-feature"],
            requires=["ecs", "event-bus"],
        )

    def get_dependencies(self) -> list[str]:
        """Packs that must be loaded before this one."""
        return ["stdlib"]

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

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

    def register_commands(self, registry: CommandRegistry) -> None:
        """Commands provided by this pack."""
        registry.register(my_command)
        registry.register(another_command)

    def register_document_schemas(self, store: DocumentStore) -> None:
        """Document schemas for persistence."""
        store.register_schema("my_collection", MyModel)

    async def on_load(self, engine: GameEngine) -> None:
        """Initialize when pack is loaded."""
        # Load data files, connect to services, etc.
        pass

    async def on_unload(self, engine: GameEngine) -> None:
        """Cleanup when pack is unloaded."""
        # Save state, close connections, etc.
        pass

Loading Content Packs

from maid_engine.core.engine import GameEngine
from maid_stdlib.pack import StdlibContentPack
from maid_classic_rpg.pack import ClassicRPGContentPack

# Create engine
engine = GameEngine(settings)

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

# Start engine (calls on_load for each pack)
await engine.start()

Content Pack Discovery

Packs can be discovered automatically via Python entry points:

# In your pack's pyproject.toml
[project.entry-points."maid.content_packs"]
my-pack = "my_package.pack:MyContentPack"

Or from local directories:

from maid_engine.plugins.loader import discover_content_packs, ContentPackLoader

# Discover packs from entry points and directories
packs = discover_content_packs(
    search_paths=[Path("./custom_packs")],
    use_entry_points=True,
)

# Use loader for dependency resolution
loader = ContentPackLoader()
for pack in packs:
    loader.register(pack)

# Get packs in dependency order
ordered = loader.resolve_load_order()

Pack Dependency Graph

flowchart BT
    subgraph Available["Available Packs"]
        STD[stdlib]
        CRP[classic-rpg]
        MY[my-custom-pack]
    end

    CRP -->|depends on| STD
    MY -->|depends on| STD
    MY -.->|optional| CRP