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 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: async def my_command(player: Player, args: str) -> None:
player.writer.write("hello\r\n") player.writer.write("hello\r\n")
await player.writer.drain() 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") await dispatch(player, "mycommand some args")
@ -43,16 +53,17 @@ adding commands
--------------- ---------------
1. create src/mudlib/commands/yourcommand.py 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) 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 5. import the module in server.py so registration runs at startup
code 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/movement.py direction commands
src/mudlib/commands/look.py look/l 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 src/mudlib/player.py Player dataclass + registry

View file

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

View file

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

View file

@ -2,7 +2,7 @@
from typing import Any from typing import Any
from mudlib.commands import register from mudlib.commands import CommandDefinition, register
from mudlib.effects import get_effects_at from mudlib.effects import get_effects_at
from mudlib.player import Player, players from mudlib.player import Player, players
from mudlib.render.ansi import RESET, colorize_terrain 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 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 typing import Any
from mudlib.commands import register from mudlib.commands import CommandDefinition, register
from mudlib.player import Player, players from mudlib.player import Player, players
# World instance will be injected by the server # 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 all movement commands with their aliases
register("north", move_north, aliases=["n"]) register(CommandDefinition("north", move_north, aliases=["n"]))
register("south", move_south, aliases=["s"]) register(CommandDefinition("south", move_south, aliases=["s"]))
register("east", move_east, aliases=["e"]) register(CommandDefinition("east", move_east, aliases=["e"]))
register("west", move_west, aliases=["w"]) register(CommandDefinition("west", move_west, aliases=["w"]))
register("northeast", move_northeast, aliases=["ne"]) register(CommandDefinition("northeast", move_northeast, aliases=["ne"]))
register("northwest", move_northwest, aliases=["nw"]) register(CommandDefinition("northwest", move_northwest, aliases=["nw"]))
register("southeast", move_southeast, aliases=["se"]) register(CommandDefinition("southeast", move_southeast, aliases=["se"]))
register("southwest", move_southwest, aliases=["sw"]) register(CommandDefinition("southwest", move_southwest, aliases=["sw"]))

View file

@ -1,6 +1,6 @@
"""Quit command for disconnecting from the server.""" """Quit command for disconnecting from the server."""
from mudlib.commands import register from mudlib.commands import CommandDefinition, register
from mudlib.player import Player, players 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 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 import pytest
from mudlib import commands 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.effects import active_effects, add_effect
from mudlib.player import Player from mudlib.player import Player
from mudlib.render.ansi import RESET from mudlib.render.ansi import RESET
@ -49,8 +49,9 @@ def test_register_command():
async def test_handler(player, args): async def test_handler(player, args):
pass pass
commands.register("test", test_handler) commands.register(CommandDefinition("test", test_handler))
assert "test" in commands._registry assert "test" in commands._registry
assert commands._registry["test"].handler is test_handler
def test_register_command_with_aliases(): def test_register_command_with_aliases():
@ -59,12 +60,12 @@ def test_register_command_with_aliases():
async def test_handler(player, args): async def test_handler(player, args):
pass pass
commands.register("testcmd", test_handler, aliases=["tc", "t"]) commands.register(CommandDefinition("testcmd", test_handler, aliases=["tc", "t"]))
assert "testcmd" in commands._registry assert "testcmd" in commands._registry
assert "tc" in commands._registry assert "tc" in commands._registry
assert "t" in commands._registry assert "t" in commands._registry
assert commands._registry["testcmd"] == commands._registry["tc"] assert commands._registry["testcmd"] is commands._registry["tc"]
assert commands._registry["testcmd"] == commands._registry["t"] assert commands._registry["testcmd"] is commands._registry["t"]
@pytest.mark.asyncio @pytest.mark.asyncio
@ -78,7 +79,7 @@ async def test_dispatch_routes_to_handler(player):
called = True called = True
received_args = args received_args = args
commands.register("testcmd", test_handler) commands.register(CommandDefinition("testcmd", test_handler))
await commands.dispatch(player, "testcmd arg1 arg2") await commands.dispatch(player, "testcmd arg1 arg2")
assert called assert called