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(
    "look",
    look_handler,
    pack_name="my-pack",
    aliases=["l", "examine"],
    description="Look at your surroundings",
)

# Execute a command
handled = await registry.execute(player_context)

Command Structure

Command Handler

A command handler is an async function:

from maid_engine.commands import CommandContext

async def look_handler(ctx: CommandContext) -> bool:
    """Look at surroundings or a specific target.

    Returns True if handled, False if not recognized.
    """
    if ctx.args:
        # look <target>
        await look_at(ctx.player, ctx.args[0])
    else:
        # look (room description)
        await look_room(ctx.player)
    return True

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)

Return Value

Command handlers return bool:

  • True — the command was handled successfully
  • False — the command was not recognized or could not be handled

Send messages to the player directly via the session (e.g., await send_to_session(ctx.session, "message")) rather than through a return value.

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:

# Set pack priorities (higher number = higher priority)
registry.set_pack_priority("engine", 0)
registry.set_pack_priority("stdlib", 50)
registry.set_pack_priority("my-game", 100)

# Register from different packs — priority comes from pack priority
registry.register("attack", basic_attack, pack_name="engine")
registry.register("attack", stdlib_attack, pack_name="stdlib")
registry.register("attack", rpg_attack, pack_name="my-game")

When attack is executed, rpg_attack runs (pack priority 100 is highest).

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_classic_rpg.commands.registry 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)

Note

CommandCategory is defined in maid_classic_rpg.commands.registry, not in maid_engine.commands. The engine's category field is a plain str; CommandCategory is a game-level enum provided by the classic RPG content pack.

Content Pack Integration

Content packs register commands through their interface:

class MyCombatPack:
    def register_commands(self, registry: CommandRegistry) -> None:
        registry.register(
            "attack",
            self.attack_handler,
            pack_name="my-combat-pack",
            aliases=["a", "hit"],
            description="Attack a target",
            category="combat",
        )

        registry.register(
            "flee",
            self.flee_handler,
            pack_name="my-combat-pack",
            description="Attempt to flee from combat",
            category="combat",
        )

Command Discovery

List All Commands

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

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

Help System

# Get a specific command definition
cmd = registry.get("attack")
if cmd:
    print(cmd.description)
    print(cmd.usage)

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. Handler returns bool (True = handled, False = not handled)
7. Send response to player (done inside the handler)
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,
        session=session,
    )

    # Execute — returns True if handled, False if unknown command
    handled = await registry.execute(ctx)

    if not handled:
        await send_to_player(player, f"Unknown command: {command}")

Error Handling

Unknown Commands

handled = await registry.execute(ctx)
# handled = False when the command is not registered

Command Errors

async def attack_handler(ctx: CommandContext) -> bool:
    if not ctx.args:
        await send_to_session(ctx.session, "Attack what? Usage: attack <target>")
        return True  # Command was recognized, even though args were wrong

    target = find_target(ctx.args[0], ctx.player_id)
    if not target:
        await send_to_session(ctx.session, f"You don't see '{ctx.args[0]}' here.")
        return True

    # Perform attack
    await do_attack(ctx.player_id, target.id)
    await send_to_session(ctx.session, f"You attack {target.name}!")
    return True

Best Practices

1. Validate Early

async def handler(ctx: CommandContext) -> bool:
    # Validate arguments first
    if len(ctx.args) < 2:
        await send_to_session(ctx.session, "Usage: give <item> <target>")
        return True  # Recognized command, bad args

    # Then process
    ...

2. Provide Clear Feedback

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

3. Use Categories

# Makes help more organized
registry.register(
    "sneak",
    sneak_handler,
    pack_name="my-pack",
    category="movement",  # Not "combat"
)

4. Document Commands

registry.register(
    "cast",
    cast_handler,
    pack_name="my-pack",
    description="Cast a spell.\nUsage: cast <spell> [target]",
    aliases=["c"],
)

Next Steps