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.
116 lines
3.5 KiB
Python
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
|