Skip to content

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:

@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"},
    )
# manifest.toml
[pack]
name = "my-pack"
version = "1.0.0"
display_name = "My Pack"
description = "Custom game content"

[pack.dependencies]
stdlib = ">=0.1.0"

Dependency Management

Content packs can depend on other packs. Dependencies are resolved and loaded in the correct order.

Declaring Dependencies

def get_dependencies(self) -> list[str]:
    return ["stdlib"]  # Must be loaded before this pack

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:

[project.entry-points."maid.content_packs"]
my-pack = "my_package.pack:MyContentPack"

Local Directories

Place packs in a search directory with a manifest.toml and pack.py:

custom_packs/
    my-pack/
        manifest.toml
        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:

def get_dependencies(self) -> list[str]:
    return ["stdlib"]  # Be explicit

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