Skip to content

Commands Guide

Commands are how players interact with your content pack. This guide covers everything from basic command creation to advanced features like hooks, locks, and layered command resolution.

Command Basics

Creating a Command Handler

Command handlers are async functions that receive a CommandContext:

from maid_engine.commands.registry import CommandContext


async def hello_command(ctx: CommandContext) -> bool:
    """A simple hello command."""
    await ctx.session.send("Hello, world!")
    return True  # Command handled successfully

Command Context

The CommandContext provides everything you need:

@dataclass
class CommandContext:
    session: Any           # Player's network session
    player_id: UUID        # UUID of the player entity
    command: str           # The command name executed
    args: list[str]        # Command arguments
    raw_input: str         # Full raw input string
    world: World           # The game world
    document_store: DocumentStore | None  # For persistence
    metadata: dict         # Additional data

Registering Commands

Register commands through the command registry:

from maid_engine.commands.registry import AccessLevel, LayeredCommandRegistry


def register_commands(registry: LayeredCommandRegistry, pack_name: str) -> None:
    registry.register(
        name="hello",
        handler=hello_command,
        pack_name=pack_name,
        aliases=["hi", "greet"],
        category="social",
        description="Say hello",
        usage="hello [name]",
        access_level=AccessLevel.PLAYER,
        hidden=False,
    )

Command Parameters

Access Levels

Commands can require specific access levels:

from maid_engine.commands.registry import AccessLevel

class AccessLevel(IntEnum):
    PLAYER = 0       # Regular players
    HELPER = 1       # Helpers/guides
    BUILDER = 2      # World builders
    ADMIN = 3        # Administrators
    IMPLEMENTOR = 4  # Full access

Usage:

registry.register(
    name="ban",
    handler=ban_command,
    pack_name=pack_name,
    access_level=AccessLevel.ADMIN,  # Only admins can use
)

Command Categories

Categories organize commands for help listings:

registry.register(
    name="attack",
    handler=attack_command,
    pack_name=pack_name,
    category="combat",
)

registry.register(
    name="cast",
    handler=cast_command,
    pack_name=pack_name,
    category="magic",
)

Hidden Commands

Hide commands from help listings:

registry.register(
    name="debug",
    handler=debug_command,
    pack_name=pack_name,
    hidden=True,  # Won't appear in help
)

Argument Handling

Basic Arguments

Arguments are provided as a list of strings:

async def give_command(ctx: CommandContext) -> bool:
    """Give an item to another player."""
    if len(ctx.args) < 2:
        await ctx.session.send("Usage: give <player> <item>")
        return True

    target_name = ctx.args[0]
    item_name = " ".join(ctx.args[1:])  # Item name may have spaces

    # Find target...
    return True

Parsing Complex Arguments

For complex argument parsing, consider using a helper:

def parse_args(args: list[str], pattern: str) -> dict | None:
    """Parse arguments based on a pattern.

    Pattern: "target:str count:int? force:bool?"
    """
    result = {}
    # Implementation...
    return result


async def complex_command(ctx: CommandContext) -> bool:
    parsed = parse_args(ctx.args, "target:str amount:int")
    if not parsed:
        await ctx.session.send("Usage: command <target> <amount>")
        return True

    target = parsed["target"]
    amount = parsed["amount"]
    # Process...
    return True

Layered Command System

MAID's command registry supports layered resolution, allowing content packs to override commands.

Setting Pack Priorities

Higher priority packs override lower ones:

# In your engine setup
registry.set_pack_priority("my-game", 100)    # Highest
registry.set_pack_priority("classic-rpg", 50)  # Middle
registry.set_pack_priority("stdlib", 0)        # Lowest

Overriding Commands

To override a command from a lower-priority pack:

# stdlib registers a basic "look" command at priority 0
# Your pack can override it at priority 100

async def custom_look(ctx: CommandContext) -> bool:
    """Enhanced look command with weather and time."""
    # Your custom implementation
    await ctx.session.send("You look around...")
    return True


def register_commands(registry: LayeredCommandRegistry, pack_name: str) -> None:
    registry.register(
        name="look",  # Same name as stdlib's
        handler=custom_look,
        pack_name=pack_name,  # Your pack's higher priority wins
    )

Viewing All Layers

You can inspect all registered versions of a command:

layers = registry.get_all_layers("look")
for layer in layers:
    print(f"{layer.pack_name}: priority {layer.priority}")

Command Hooks

Hooks allow you to run code before or after command execution.

Pre-Hooks

Pre-hooks run before the command handler and can cancel execution:

from maid_engine.commands.hooks import PreHookContext


async def rate_limit_hook(ctx: PreHookContext) -> bool:
    """Limit command frequency."""
    player_id = ctx.player_entity_id
    if is_rate_limited(player_id):
        await ctx.session.send("Please wait before using another command.")
        return False  # Cancel execution
    return True  # Continue


registry.register_pre_hook(
    name="rate_limiter",
    handler=rate_limit_hook,
    commands=["attack", "cast"],  # Only these commands
)

Post-Hooks

Post-hooks run after the command completes:

from maid_engine.commands.hooks import PostHookContext


async def command_logger(ctx: PostHookContext) -> None:
    """Log command execution."""
    log.info(
        f"Command: {ctx.command_name} by {ctx.player_entity_id} "
        f"took {ctx.execution_time_ms:.2f}ms"
    )


registry.register_post_hook(
    name="logger",
    handler=command_logger,
)

Hook Priorities

Hooks run in priority order:

from maid_engine.commands.hooks import HookPriority

registry.register_pre_hook(
    name="authentication",
    handler=auth_hook,
    priority=HookPriority.HIGHEST,  # Runs first
)

registry.register_pre_hook(
    name="rate_limiter",
    handler=rate_limit_hook,
    priority=HookPriority.NORMAL,  # Runs after auth
)

Filtering Hooks

Hooks can filter by command name or category:

# Only for specific commands
registry.register_pre_hook(
    name="combat_check",
    handler=combat_hook,
    commands=["attack", "flee", "defend"],
)

# Only for a category
registry.register_pre_hook(
    name="magic_check",
    handler=magic_hook,
    categories=["magic"],
)

Lock System

Locks provide fine-grained permission control using expressions.

Lock Expressions

Locks are string expressions evaluated at runtime:

registry.register(
    name="godmode",
    handler=godmode_command,
    pack_name=pack_name,
    locks="perm(admin)",  # Requires admin permission
)

registry.register(
    name="guild_bank",
    handler=guild_bank_command,
    pack_name=pack_name,
    locks="perm(guild_leader) OR perm(admin)",  # Either works
)

Custom Lock Functions

Register custom lock functions:

from maid_engine.commands.locks import LockContext


def check_gold(ctx: LockContext, args: list[str]) -> bool:
    """Check if player has enough gold."""
    player = ctx.world.entities.get(ctx.player_entity_id)
    if not player:
        return False

    inventory = player.try_get(InventoryComponent)
    if not inventory:
        return False

    required = int(args[0]) if args else 0
    return inventory.gold >= required


registry.register_lock_function("has_gold", check_gold)

# Use in command registration
registry.register(
    name="expensive_action",
    handler=expensive_command,
    pack_name=pack_name,
    locks="has_gold(1000)",  # Requires 1000 gold
)

Lock Expression Syntax

Expression Meaning
perm(name) Has permission "name"
func(arg) Custom function with argument
A AND B Both A and B must be true
A OR B Either A or B must be true
NOT A A must be false
(A AND B) OR C Grouping with parentheses

Command Metadata

Add custom metadata for hooks and other systems:

registry.register(
    name="fireball",
    handler=fireball_command,
    pack_name=pack_name,
    metadata={
        "cooldown": 5.0,        # 5 second cooldown
        "mana_cost": 50,        # Costs 50 mana
        "requires_target": True,
        "combat_only": True,
    },
)

Access metadata in hooks:

async def cooldown_hook(ctx: PreHookContext) -> bool:
    cooldown = ctx.metadata.get("cooldown", 0)
    if cooldown > 0:
        if is_on_cooldown(ctx.player_entity_id, ctx.command_name):
            await ctx.session.send("That ability is on cooldown.")
            return False
    return True

Complete Example

Here's a complete example of a sophisticated command:

from uuid import UUID

from maid_engine.commands.registry import (
    AccessLevel,
    CommandContext,
    LayeredCommandRegistry,
)
from maid_stdlib.components import (
    DescriptionComponent,
    HealthComponent,
    InventoryComponent,
    PositionComponent,
)


async def heal_command(ctx: CommandContext) -> bool:
    """Heal yourself or another player.

    Usage:
        heal           - Heal yourself
        heal <target>  - Heal another player
    """
    player = ctx.world.entities.get(ctx.player_id)
    if not player:
        await ctx.session.send("Error: Could not find your character.")
        return False

    # Determine target
    if ctx.args:
        # Heal another player
        target_name = ctx.args[0].lower()
        target = await find_player_in_room(ctx, target_name)
        if not target:
            await ctx.session.send(f"You don't see '{ctx.args[0]}' here.")
            return True
    else:
        # Heal self
        target = player

    # Check target has health
    health = target.try_get(HealthComponent)
    if not health:
        await ctx.session.send("That target cannot be healed.")
        return True

    # Check already at full health
    if health.current >= health.maximum:
        if target == player:
            await ctx.session.send("You are already at full health.")
        else:
            target_desc = target.get(DescriptionComponent)
            await ctx.session.send(f"{target_desc.name} is already at full health.")
        return True

    # Calculate healing amount
    heal_amount = min(50, health.maximum - health.current)
    health.heal(heal_amount)

    # Send messages
    if target == player:
        await ctx.session.send(f"You heal yourself for {heal_amount} health.")
    else:
        target_desc = target.get(DescriptionComponent)
        await ctx.session.send(
            f"You heal {target_desc.name} for {heal_amount} health."
        )

    return True


async def find_player_in_room(ctx: CommandContext, name: str):
    """Find a player by name in the same room."""
    player = ctx.world.entities.get(ctx.player_id)
    if not player:
        return None

    player_pos = player.try_get(PositionComponent)
    if not player_pos:
        return None

    for entity in ctx.world.entities.with_components(
        PositionComponent, DescriptionComponent
    ):
        if entity.id == ctx.player_id:
            continue

        pos = entity.get(PositionComponent)
        if pos.room_id != player_pos.room_id:
            continue

        desc = entity.get(DescriptionComponent)
        if name in str(desc.name).lower():
            return entity

    return None


def register_commands(registry: LayeredCommandRegistry, pack_name: str) -> None:
    registry.register(
        name="heal",
        handler=heal_command,
        pack_name=pack_name,
        category="magic",
        description="Heal yourself or another player",
        usage="heal [target]",
        access_level=AccessLevel.PLAYER,
        metadata={
            "cooldown": 10.0,
            "mana_cost": 25,
        },
    )

Best Practices

Validate Input Early

async def my_command(ctx: CommandContext) -> bool:
    if not ctx.args:
        await ctx.session.send("Usage: command <required_arg>")
        return True  # Still return True - command was handled

Return Appropriate Values

  • Return True when the command was handled (even if the action failed)
  • Return False only when the command wasn't applicable
async def my_command(ctx: CommandContext) -> bool:
    if not ctx.args:
        await ctx.session.send("Usage: command <arg>")
        return True  # Handled - showed usage

    if not is_valid(ctx.args[0]):
        await ctx.session.send("Invalid argument.")
        return True  # Handled - showed error

    # Do the actual thing
    return True  # Handled - did the action

Use Descriptive Categories

Standard categories:

  • movement - look, go, enter, exit
  • combat - attack, flee, defend
  • magic - cast, channel, dispel
  • social - say, tell, emote
  • inventory - get, drop, wear, remove
  • information - who, score, help
  • admin - ban, kick, reload

Provide Clear Usage Messages

async def complex_command(ctx: CommandContext) -> bool:
    if len(ctx.args) < 2:
        await ctx.session.send("Usage: command <target> <action> [options]")
        await ctx.session.send("Actions: add, remove, list")
        await ctx.session.send("Example: command player1 add --force")
        return True

Next Steps