Add CommandDefinition and migrate command registry

This commit is contained in:
Jared Miller 2026-02-07 16:15:21 -05:00
parent bea2a73c98
commit dcc8b961bb
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
7 changed files with 65 additions and 46 deletions

View file

@ -1,15 +1,25 @@
command system
==============
commands are registered in a simple dict mapping names to async handlers.
commands are registered as CommandDefinition objects with metadata.
from mudlib.commands import CommandDefinition, register
async def my_command(player: Player, args: str) -> None:
player.writer.write("hello\r\n")
await player.writer.drain()
register("mycommand", my_command, aliases=["mc"])
register(CommandDefinition("mycommand", my_command, aliases=["mc"]))
dispatch parses input, looks up the handler, calls it:
CommandDefinition fields:
name primary command name
handler async function(player, args)
aliases alternative names (default: [])
mode required player mode (default: "normal", "*" = any mode)
help help text (default: "")
dispatch parses input, looks up the definition, calls its handler:
await dispatch(player, "mycommand some args")
@ -43,16 +53,17 @@ adding commands
---------------
1. create src/mudlib/commands/yourcommand.py
2. import register from mudlib.commands
2. import CommandDefinition and register from mudlib.commands
3. define async handler(player, args)
4. call register() with name and aliases
4. call register(CommandDefinition(...))
5. import the module in server.py so registration runs at startup
code
----
src/mudlib/commands/__init__.py registry + dispatch
src/mudlib/commands/__init__.py registry + dispatch + CommandDefinition
src/mudlib/commands/movement.py direction commands
src/mudlib/commands/look.py look/l
src/mudlib/commands/quit.py quit/q
src/mudlib/commands/fly.py fly
src/mudlib/commands/quit.py quit/q (mode="*")
src/mudlib/player.py Player dataclass + registry

View file

@ -1,31 +1,38 @@
"""Command registry and dispatcher."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from mudlib.player import Player
# Type alias for command handlers
CommandHandler = Callable[[Player, str], Awaitable[None]]
# Registry maps command names to handler functions
_registry: dict[str, CommandHandler] = {}
@dataclass
class CommandDefinition:
"""Metadata wrapper for a registered command."""
name: str
handler: CommandHandler
aliases: list[str] = field(default_factory=list)
mode: str = "normal"
help: str = ""
def register(
name: str, handler: CommandHandler, aliases: list[str] | None = None
) -> None:
"""Register a command handler with optional aliases.
# Registry maps command names to definitions
_registry: dict[str, CommandDefinition] = {}
def register(defn: CommandDefinition) -> None:
"""Register a command definition with its aliases.
Args:
name: The primary command name
handler: Async function that handles the command
aliases: Optional list of alternative names for the command
defn: The command definition to register
"""
_registry[name] = handler
if aliases:
for alias in aliases:
_registry[alias] = handler
_registry[defn.name] = defn
for alias in defn.aliases:
_registry[alias] = defn
async def dispatch(player: Player, raw_input: str) -> None:
@ -45,13 +52,13 @@ async def dispatch(player: Player, raw_input: str) -> None:
command = parts[0].lower()
args = parts[1] if len(parts) > 1 else ""
# Look up the handler
handler = _registry.get(command)
# Look up the definition
defn = _registry.get(command)
if handler is None:
if defn is None:
player.writer.write(f"Unknown command: {command}\r\n")
await player.writer.drain()
return
# Execute the handler
await handler(player, args)
await defn.handler(player, args)

View file

@ -2,7 +2,7 @@
from typing import Any
from mudlib.commands import register
from mudlib.commands import CommandDefinition, register
from mudlib.commands.movement import DIRECTIONS, send_nearby_message
from mudlib.effects import add_effect
from mudlib.player import Player
@ -91,4 +91,4 @@ async def cmd_fly(player: Player, args: str) -> None:
await cmd_look(player, "")
register("fly", cmd_fly)
register(CommandDefinition("fly", cmd_fly))

View file

@ -2,7 +2,7 @@
from typing import Any
from mudlib.commands import register
from mudlib.commands import CommandDefinition, register
from mudlib.effects import get_effects_at
from mudlib.player import Player, players
from mudlib.render.ansi import RESET, colorize_terrain
@ -89,4 +89,4 @@ async def cmd_look(player: Player, args: str) -> None:
# Register the look command with its alias
register("look", cmd_look, aliases=["l"])
register(CommandDefinition("look", cmd_look, aliases=["l"]))

View file

@ -2,7 +2,7 @@
from typing import Any
from mudlib.commands import register
from mudlib.commands import CommandDefinition, register
from mudlib.player import Player, players
# World instance will be injected by the server
@ -154,11 +154,11 @@ async def move_southwest(player: Player, args: str) -> None:
# Register all movement commands with their aliases
register("north", move_north, aliases=["n"])
register("south", move_south, aliases=["s"])
register("east", move_east, aliases=["e"])
register("west", move_west, aliases=["w"])
register("northeast", move_northeast, aliases=["ne"])
register("northwest", move_northwest, aliases=["nw"])
register("southeast", move_southeast, aliases=["se"])
register("southwest", move_southwest, aliases=["sw"])
register(CommandDefinition("north", move_north, aliases=["n"]))
register(CommandDefinition("south", move_south, aliases=["s"]))
register(CommandDefinition("east", move_east, aliases=["e"]))
register(CommandDefinition("west", move_west, aliases=["w"]))
register(CommandDefinition("northeast", move_northeast, aliases=["ne"]))
register(CommandDefinition("northwest", move_northwest, aliases=["nw"]))
register(CommandDefinition("southeast", move_southeast, aliases=["se"]))
register(CommandDefinition("southwest", move_southwest, aliases=["sw"]))

View file

@ -1,6 +1,6 @@
"""Quit command for disconnecting from the server."""
from mudlib.commands import register
from mudlib.commands import CommandDefinition, register
from mudlib.player import Player, players
@ -21,4 +21,4 @@ async def cmd_quit(player: Player, args: str) -> None:
# Register the quit command with its aliases
register("quit", cmd_quit, aliases=["q"])
register(CommandDefinition("quit", cmd_quit, aliases=["q"], mode="*"))

View file

@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock
import pytest
from mudlib import commands
from mudlib.commands import look, movement
from mudlib.commands import CommandDefinition, look, movement
from mudlib.effects import active_effects, add_effect
from mudlib.player import Player
from mudlib.render.ansi import RESET
@ -49,8 +49,9 @@ def test_register_command():
async def test_handler(player, args):
pass
commands.register("test", test_handler)
commands.register(CommandDefinition("test", test_handler))
assert "test" in commands._registry
assert commands._registry["test"].handler is test_handler
def test_register_command_with_aliases():
@ -59,12 +60,12 @@ def test_register_command_with_aliases():
async def test_handler(player, args):
pass
commands.register("testcmd", test_handler, aliases=["tc", "t"])
commands.register(CommandDefinition("testcmd", test_handler, aliases=["tc", "t"]))
assert "testcmd" in commands._registry
assert "tc" in commands._registry
assert "t" in commands._registry
assert commands._registry["testcmd"] == commands._registry["tc"]
assert commands._registry["testcmd"] == commands._registry["t"]
assert commands._registry["testcmd"] is commands._registry["tc"]
assert commands._registry["testcmd"] is commands._registry["t"]
@pytest.mark.asyncio
@ -78,7 +79,7 @@ async def test_dispatch_routes_to_handler(player):
called = True
received_args = args
commands.register("testcmd", test_handler)
commands.register(CommandDefinition("testcmd", test_handler))
await commands.dispatch(player, "testcmd arg1 arg2")
assert called