Add CommandDefinition and migrate command registry
This commit is contained in:
parent
bea2a73c98
commit
dcc8b961bb
7 changed files with 65 additions and 46 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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"]))
|
||||
|
|
|
|||
|
|
@ -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"]))
|
||||
|
|
|
|||
|
|
@ -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="*"))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue