mud/src/mudlib/things.py
Jared Miller e724abb926
Add TOML verb support for thing templates
Thing templates can now define verbs in TOML using [verbs] section with
module:function references. Verbs are resolved at spawn time and bound
to the spawned object instance using functools.partial. Works for both
Thing and Container instances.
2026-02-11 21:47:33 -05:00

116 lines
3.5 KiB
Python

"""Thing template loading, registry, and spawning."""
import functools
import importlib
import logging
import tomllib
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from pathlib import Path
from mudlib.container import Container
from mudlib.object import Object
from mudlib.thing import Thing
logger = logging.getLogger(__name__)
@dataclass
class ThingTemplate:
"""Definition loaded from TOML — used to spawn Thing instances."""
name: str
description: str
portable: bool = True
aliases: list[str] = field(default_factory=list)
# Container fields (presence of capacity indicates a container template)
capacity: int | None = None
closed: bool = False
locked: bool = False
# Verb handlers (verb_name -> module:function reference)
verbs: dict[str, str] = field(default_factory=dict)
# Module-level registry
thing_templates: dict[str, ThingTemplate] = {}
def _resolve_handler(ref: str) -> Callable[..., Awaitable[None]] | None:
"""Resolve a 'module:function' reference to a callable."""
module_path, _, func_name = ref.rpartition(":")
try:
module = importlib.import_module(module_path)
return getattr(module, func_name)
except (ImportError, AttributeError) as e:
logger.warning("Failed to resolve verb handler '%s': %s", ref, e)
return None
def load_thing_template(path: Path) -> ThingTemplate:
"""Parse a thing TOML file into a ThingTemplate."""
with open(path, "rb") as f:
data = tomllib.load(f)
return ThingTemplate(
name=data["name"],
description=data["description"],
portable=data.get("portable", True),
aliases=data.get("aliases", []),
capacity=data.get("capacity"),
closed=data.get("closed", False),
locked=data.get("locked", False),
verbs=data.get("verbs", {}),
)
def load_thing_templates(directory: Path) -> dict[str, ThingTemplate]:
"""Load all .toml files in a directory into a dict keyed by name."""
templates: dict[str, ThingTemplate] = {}
for path in sorted(directory.glob("*.toml")):
template = load_thing_template(path)
templates[template.name] = template
return templates
def spawn_thing(
template: ThingTemplate,
location: Object | None,
*,
x: int | None = None,
y: int | None = None,
) -> Thing:
"""Create a Thing instance from a template at the given location."""
# If template has capacity, spawn a Container instead of a Thing
if template.capacity is not None:
obj = Container(
name=template.name,
description=template.description,
portable=template.portable,
aliases=list(template.aliases),
capacity=template.capacity,
closed=template.closed,
locked=template.locked,
location=location,
x=x,
y=y,
)
else:
obj = Thing(
name=template.name,
description=template.description,
portable=template.portable,
aliases=list(template.aliases),
location=location,
x=x,
y=y,
)
# Register verbs from the template
for verb_name, handler_ref in template.verbs.items():
handler = _resolve_handler(handler_ref)
if handler is None:
continue
# Bind the object as first argument using partial
bound_handler = functools.partial(handler, obj)
obj.register_verb(verb_name, bound_handler)
return obj