Content Pack System Overview¶
The content pack system is MAID's plugin architecture. It allows you to package game functionality into modular, reusable units that can be loaded independently and combined to create different game experiences.
Architecture¶
Content packs sit on top of the core engine and provide game-specific functionality:
+----------------------------------------+
| Your Custom Pack |
+----------------------------------------+
| maid-classic-rpg |
+----------------------------------------+
| maid-stdlib |
+----------------------------------------+
| maid-engine |
+----------------------------------------+
The engine provides pure infrastructure:
- Entity Component System (ECS)
- Event bus for pub/sub messaging
- Networking (Telnet, WebSocket, REST API)
- Document store for persistence
- Configuration system
- Command registry
Content packs provide game content:
- Game systems (combat, magic, crafting)
- Components (health, inventory, stats)
- Commands (look, move, attack)
- Events (damage dealt, item picked up)
- Data schemas and persistence
What Content Packs Provide¶
Systems¶
ECS systems contain game logic that runs every tick. They process entities with specific component combinations:
def get_systems(self, world: World) -> list[System]:
return [
CombatSystem(world),
RegenerationSystem(world),
AIBehaviorSystem(world),
]
Events¶
Events enable decoupled communication between systems:
def get_events(self) -> list[type[Event]]:
return [
CombatStartEvent,
DamageDealtEvent,
EntityDeathEvent,
]
Commands¶
Commands let players interact with the game:
def register_commands(self, registry: CommandRegistry) -> None:
registry.register("attack", attack_handler, pack_name=self.manifest.name)
registry.register("cast", cast_spell_handler, pack_name=self.manifest.name)
Document Schemas¶
Schemas define the structure of persisted data:
def register_document_schemas(self, store: DocumentStore) -> None:
store.register_schema("characters", CharacterModel)
store.register_schema("items", ItemModel)
Lifecycle Hooks¶
Hooks run when the pack is loaded or unloaded:
async def on_load(self, engine: GameEngine) -> None:
# Initialize resources, load data files, start background tasks
pass
async def on_unload(self, engine: GameEngine) -> None:
# Clean up resources, save state, stop background tasks
pass
Content Pack Manifest¶
Every content pack has a manifest that describes its metadata:
@dataclass
class ContentPackManifest:
name: str # Unique identifier (e.g., "classic-rpg")
version: str # Semantic version (e.g., "0.1.0")
display_name: str # Human-readable name (e.g., "Classic RPG")
description: str # Brief description
dependencies: dict # Required packs with version constraints
provides: list # Capability identifiers this pack provides
requires: list # Capability identifiers this pack requires
authors: list # Author names
license: str # SPDX license identifier
homepage: str # URL to homepage/repository
keywords: list # Keywords for discovery
Manifests can be defined in Python or TOML:
Dependency Management¶
Content packs can depend on other packs. Dependencies are resolved and loaded in the correct order.
Declaring Dependencies¶
Version Constraints¶
Dependencies support version constraints in the manifest:
dependencies = {
"stdlib": ">=0.1.0", # Minimum version
"combat": ">=1.0,<2.0", # Version range
"magic": "==1.2.3", # Exact version
}
Dependency Graph¶
The content pack loader resolves dependencies using topological sort:
+----------+
| stdlib |
+----+-----+
|
+--------------+--------------+
| |
+-----+-----+ +------+------+
| classic | | my-custom |
| rpg | | pack |
+-----+-----+ +-------------+
|
+-----+-----+
| my-game |
| content |
+-----------+
Content Pack Discovery¶
Content packs can be discovered automatically through:
Python Entry Points¶
Register your pack in pyproject.toml:
Local Directories¶
Place packs in a search directory with a manifest.toml and pack.py:
Programmatic Loading¶
Load packs directly in code:
from maid_engine.plugins.loader import discover_content_packs, ContentPackLoader
# Discover from entry points and directories
packs = discover_content_packs(
search_paths=[Path("./custom_packs")],
use_entry_points=True,
)
# Load packs in dependency order
loader = ContentPackLoader()
for pack in packs:
loader.register(pack)
ordered = loader.resolve_load_order()
for pack in ordered:
engine.load_content_pack(pack)
Layered Command System¶
Multiple content packs can register the same command. The highest-priority registration wins:
# Set pack priorities
registry.set_pack_priority("my-game", 100) # Highest
registry.set_pack_priority("stdlib", 50) # Lower
# Both packs register "look"
# my-game's handler is used because it has higher priority
This allows you to:
- Override default commands with custom behavior
- Extend base functionality
- Create game-specific variations
Built-in Content Packs¶
MAID includes two built-in content packs:
maid-stdlib¶
The standard library provides common functionality:
- Basic components (Health, Mana, Position, Inventory)
- Core events (RoomEnter, DamageDealt, ItemPickedUp)
- Essential commands (look, move, get, drop, inventory)
- Utility functions (dice rolling, text formatting)
maid-classic-rpg¶
Traditional MUD gameplay content:
- Combat system with tactical positioning
- Magic system with spells and effects
- Crafting and economy systems
- Quest and achievement systems
- Guild and faction support
- World dynamics (time, weather)
Best Practices¶
Keep Packs Focused¶
Each pack should have a clear, focused purpose:
- Good: "combat-system", "magic-system", "crafting-system"
- Avoid: "everything-pack", "game-utils"
Declare Dependencies Explicitly¶
Always declare the packs you depend on, even if they're commonly available:
Use Events for Communication¶
Prefer events over direct system coupling:
# Good: Emit an event
await world.events.emit(DamageDealtEvent(...))
# Avoid: Direct system calls
combat_system.deal_damage(...)
Version Your Manifests¶
Use semantic versioning for your content packs:
- Major: Breaking changes to API or behavior
- Minor: New features, backward compatible
- Patch: Bug fixes, no API changes
Test Your Packs¶
Write tests for your content pack functionality:
@pytest.mark.asyncio
async def test_combat_damage():
world = World()
pack = CombatPack()
for system in pack.get_systems(world):
world.systems.register(system)
# Test your systems...
Next Steps¶
- Creating Content Packs - Detailed guide to creating packs
- Systems Guide - How to write ECS systems
- Commands Guide - Creating player commands
- Events Guide - Working with the event bus
- Persistence Guide - Storing data
- Testing Guide - Testing your content packs
- Publishing Guide - Sharing your packs