Skip to content

Command System Overview

The command system in MAID handles player input, routing text commands to appropriate handlers. It supports aliases, layered overrides, and content pack integration.

What is a Command?

A command is a text instruction from a player, like:

look
north
get sword
say Hello everyone!
attack goblin

Commands consist of a name and optional arguments.

Command Registry

The LayeredCommandRegistry manages all commands:

from maid_engine.commands import LayeredCommandRegistry, CommandHandler

registry = LayeredCommandRegistry()

# Register a command
registry.register(
    name="look",
    handler=look_handler,
    aliases=["l", "examine"],
    help_text="Look at your surroundings",
)

# Execute a command
result = await registry.execute("look", player_context)

Command Structure

Command Handler

A command handler is an async function:

from maid_engine.commands import CommandContext, CommandResult

async def look_handler(ctx: CommandContext) -> CommandResult:
    """Look at surroundings or a specific target."""
    if ctx.args:
        # look <target>
        return await look_at(ctx.player, ctx.args[0])
    else:
        # look (room description)
        return await look_room(ctx.player)

CommandContext

Contains all information about the command invocation:

@dataclass
class CommandContext:
    player_id: UUID          # Player executing command
    command: str             # Command name ("look")
    args: list[str]          # Arguments (["sword"])
    raw_input: str           # Full input ("look at sword")
    world: World             # Game world reference
    session: Session | None  # Network session (if any)

CommandResult

Describes the outcome of a command:

@dataclass
class CommandResult:
    success: bool
    message: str = ""
    data: dict[str, Any] | None = None

Built-in Commands

MAID provides core commands in maid-stdlib:

Movement Commands

north, south, east, west, up, down  # Move in a direction
n, s, e, w, u, d                    # Direction aliases
go <direction>                       # Explicit movement

Look Commands

look                # Look at current room
look <target>       # Look at entity or item
examine <target>    # Alias for look <target>

Inventory Commands

inventory           # List carried items
i                   # Alias for inventory
get <item>          # Pick up an item
drop <item>         # Drop an item
give <item> <target>  # Give item to someone

Communication Commands

say <message>       # Speak to the room
tell <player> <message>  # Private message
shout <message>     # Broadcast to area

System Commands

help                # List commands
help <command>      # Command help
who                 # List online players
quit                # Disconnect

Command Layers

Commands can be registered at different layers, allowing content packs to override base functionality:

from maid_engine.commands import CommandLayer

# Engine layer - lowest priority, can be overridden
registry.register(
    name="attack",
    handler=basic_attack,
    layer=CommandLayer.ENGINE,
)

# Stdlib layer - standard library commands
registry.register(
    name="attack",
    handler=stdlib_attack,
    layer=CommandLayer.STDLIB,
)

# Content pack layer - game-specific commands
registry.register(
    name="attack",
    handler=rpg_attack,
    layer=CommandLayer.CONTENT,
)

# Override layer - highest priority
registry.register(
    name="attack",
    handler=custom_attack,
    layer=CommandLayer.OVERRIDE,
)

When attack is executed, custom_attack runs (highest priority).

Command Aliases

Commands can have multiple names:

registry.register(
    name="inventory",
    handler=inventory_handler,
    aliases=["i", "inv", "items"],
)

# All of these work:
# inventory
# i
# inv
# items

Command Categories

Organize commands by category:

from maid_engine.commands import CommandCategory

registry.register(
    name="attack",
    handler=attack_handler,
    category=CommandCategory.COMBAT,
)

registry.register(
    name="cast",
    handler=cast_handler,
    category=CommandCategory.MAGIC,
)

# List commands by category
combat_commands = registry.get_by_category(CommandCategory.COMBAT)

Content Pack Integration

Content packs register commands through their interface:

class MyCombatPack:
    def register_commands(self, registry: CommandRegistry) -> None:
        registry.register(
            name="attack",
            handler=self.attack_handler,
            aliases=["a", "hit"],
            help_text="Attack a target",
            category=CommandCategory.COMBAT,
            source_pack="my-combat-pack",
        )

        registry.register(
            name="flee",
            handler=self.flee_handler,
            help_text="Attempt to flee from combat",
            category=CommandCategory.COMBAT,
            source_pack="my-combat-pack",
        )

Command Discovery

List All Commands

# Get all registered commands
for cmd in registry.all_commands():
    print(f"{cmd.name}: {cmd.help_text}")

# Get command by name
cmd = registry.get_command("attack")
if cmd:
    print(f"Found: {cmd.name} (aliases: {cmd.aliases})")

Help System

# Generate help text
help_text = registry.get_help("attack")

# Get all commands a player can use
available = registry.get_available_commands(player_context)

Command Execution Flow

1. Player types: "attack goblin"
2. Parse input -> command="attack", args=["goblin"]
3. Find handler via LayeredCommandRegistry
4. Create CommandContext
5. Execute handler
6. Return CommandResult
7. Send response to player
async def handle_player_input(input_text: str, player: Entity) -> None:
    # Parse command and args
    parts = input_text.strip().split()
    if not parts:
        return

    command = parts[0].lower()
    args = parts[1:]

    # Create context
    ctx = CommandContext(
        player_id=player.id,
        command=command,
        args=args,
        raw_input=input_text,
        world=world,
    )

    # Execute
    result = await registry.execute(command, ctx)

    # Handle result
    if result.success:
        await send_to_player(player, result.message)
    else:
        await send_to_player(player, f"Error: {result.message}")

Error Handling

Unknown Commands

result = await registry.execute("xyzzy", ctx)
# result.success = False
# result.message = "Unknown command: xyzzy"

Command Errors

async def attack_handler(ctx: CommandContext) -> CommandResult:
    if not ctx.args:
        return CommandResult(
            success=False,
            message="Attack what? Usage: attack <target>",
        )

    target = find_target(ctx.args[0], ctx.player_id)
    if not target:
        return CommandResult(
            success=False,
            message=f"You don't see '{ctx.args[0]}' here.",
        )

    # Perform attack
    await do_attack(ctx.player_id, target.id)
    return CommandResult(
        success=True,
        message=f"You attack {target.name}!",
    )

Best Practices

1. Validate Early

async def handler(ctx: CommandContext) -> CommandResult:
    # Validate arguments first
    if len(ctx.args) < 2:
        return CommandResult(False, "Usage: give <item> <target>")

    # Then process
    ...

2. Provide Clear Feedback

async def handler(ctx: CommandContext) -> CommandResult:
    item = find_item(ctx.args[0])
    if not item:
        return CommandResult(
            False,
            f"You don't have '{ctx.args[0]}'.\n"
            f"Check your inventory with 'inventory'.",
        )
    ...

3. Use Categories

# Makes help more organized
registry.register(
    name="sneak",
    handler=sneak_handler,
    category=CommandCategory.MOVEMENT,  # Not COMBAT
)

4. Document Commands

registry.register(
    name="cast",
    handler=cast_handler,
    help_text="Cast a spell.\nUsage: cast <spell> [target]",
    aliases=["c"],
)

Next Steps