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
ContentPack Protocol Extensions¶
In addition to the core lifecycle methods shown above, content packs can implement these optional registration hooks:
Component Type Registration¶
Register custom component types so the persistence system can serialize and deserialize them:
from maid_engine.core.ecs.registry import ComponentRegistry
def register_component_types(self, registry: ComponentRegistry) -> None:
"""Register component types for persistence serialization."""
registry.register(MyCustomComponent)
registry.register(AnotherComponent)
This is called during pack loading, before on_load. Each pack registers its custom components so that persisted entity data can be reconstructed on server restart.
Document Schema Registration¶
Register document collection schemas for pack-specific persistent data (e.g., NPC schedules, quest history, faction standings):
def register_document_schemas(self, store: DocumentStore) -> None:
"""Register document schemas for this pack's data."""
store.register_schema("npc_goals", NPCGoalModel)
store.register_schema("quest_history", QuestHistoryModel)
See the Persistence Guide for full details on both registration hooks.
Data Loader Integration¶
Content packs can provide YAML data files for declarative content authoring instead of (or alongside) programmatic entity creation. Packs that expose data files implement the DataLoaderPack protocol:
from maid_engine.loader.protocols import DataLoaderPack
class MyContentPack(BaseContentPack):
# ... other methods ...
def get_data_paths(self) -> list[Path]:
"""Directories containing YAML entity definitions."""
return [Path(__file__).parent / "data"]
def get_entity_type_configs(self) -> list[EntityTypeConfig]:
"""Supported entity types and their component mappings."""
return [my_npc_config, my_item_config]
def get_semantic_rules(self) -> list[SemanticRule]:
"""Validation rules for data files."""
return [require_health_rule]
The loader pipeline processes data files through these phases:
- Discovery — Finds YAML files in
get_data_paths()directories - Parsing — Parses YAML with template inheritance and variable substitution
- Validation — Validates against component schemas and semantic rules
- Reference resolution — Resolves cross-entity
@ref:references to UUIDs - Instantiation — Creates entities with the resolved components
- Post-load hooks — Runs any post-load callbacks
Migration Support¶
Content packs can include database migrations for schema changes. Migrations are namespaced per pack and resolved in dependency order by MigrationRunner.
- Namespaces follow the pack name (e.g.,
engine:,stdlib:,classic-rpg:) - Dependencies between pack migrations are resolved automatically based on pack dependency order
- Use
MigrationRunner.migrate(namespace="my-pack")to run a single pack's migrations, or omit the namespace to run all pending migrations - Dry-run mode is available for validation:
migrate(dry_run=True)