From d209f302449b76a22c15ee2aea404f9378295410 Mon Sep 17 00:00:00 2001 From: Jage9 Date: Tue, 24 Feb 2026 02:40:40 -0500 Subject: [PATCH] Add auto-discovered server item type plugins --- server/app/items/registry.py | 66 +++++++++++++++---- server/app/items/types/__init__.py | 1 + server/app/items/types/clock/__init__.py | 1 + server/app/items/types/clock/plugin.py | 11 ++++ server/app/items/types/dice/__init__.py | 1 + server/app/items/types/dice/plugin.py | 11 ++++ server/app/items/types/piano/__init__.py | 1 + server/app/items/types/piano/plugin.py | 11 ++++ .../app/items/types/radio_station/__init__.py | 1 + .../app/items/types/radio_station/plugin.py | 11 ++++ server/app/items/types/wheel/__init__.py | 1 + server/app/items/types/wheel/plugin.py | 11 ++++ server/app/items/types/widget/__init__.py | 1 + server/app/items/types/widget/plugin.py | 11 ++++ 14 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 server/app/items/types/__init__.py create mode 100644 server/app/items/types/clock/__init__.py create mode 100644 server/app/items/types/clock/plugin.py create mode 100644 server/app/items/types/dice/__init__.py create mode 100644 server/app/items/types/dice/plugin.py create mode 100644 server/app/items/types/piano/__init__.py create mode 100644 server/app/items/types/piano/plugin.py create mode 100644 server/app/items/types/radio_station/__init__.py create mode 100644 server/app/items/types/radio_station/plugin.py create mode 100644 server/app/items/types/wheel/__init__.py create mode 100644 server/app/items/types/wheel/plugin.py create mode 100644 server/app/items/types/widget/__init__.py create mode 100644 server/app/items/types/widget/plugin.py diff --git a/server/app/items/registry.py b/server/app/items/registry.py index 1711615..6279a9a 100644 --- a/server/app/items/registry.py +++ b/server/app/items/registry.py @@ -2,13 +2,14 @@ from __future__ import annotations +from dataclasses import dataclass +from importlib import import_module +from pathlib import Path from typing import Callable, Protocol from ..item_types import ItemUseResult from ..models import WorldItem -from . import clock, dice, piano, radio, wheel, widget - class ItemModule(Protocol): """Shape required by item modules consumed by catalog/handlers.""" @@ -29,13 +30,56 @@ class ItemModule(Protocol): use_item: Callable[[WorldItem, str, Callable[[dict], str]], ItemUseResult] -ITEM_TYPE_ORDER: tuple[str, ...] = ("clock", "dice", "piano", "radio_station", "wheel", "widget") +@dataclass(frozen=True) +class ItemTypePlugin: + """Runtime-loaded item type plugin metadata.""" -ITEM_MODULES: dict[str, ItemModule] = { - "clock": clock, - "dice": dice, - "piano": piano, - "radio_station": radio, - "wheel": wheel, - "widget": widget, -} + type: str + order: int + module: ItemModule + + +def _load_item_type_plugins() -> list[ItemTypePlugin]: + """Discover and load item-type plugins from `items/types/*/plugin.py`.""" + + base_dir = Path(__file__).resolve().parent / "types" + plugins: list[ItemTypePlugin] = [] + if not base_dir.exists(): + raise RuntimeError(f"item type plugin directory missing: {base_dir}") + + for entry in sorted(base_dir.iterdir(), key=lambda path: path.name): + if not entry.is_dir(): + continue + if entry.name.startswith("__"): + continue + plugin_module = import_module(f"{__package__}.types.{entry.name}.plugin") + raw_plugin = getattr(plugin_module, "ITEM_TYPE_PLUGIN", None) + if not isinstance(raw_plugin, dict): + raise RuntimeError(f"invalid ITEM_TYPE_PLUGIN in {plugin_module.__name__}") + type_id = raw_plugin.get("type") + order = raw_plugin.get("order") + module = raw_plugin.get("module") + if not isinstance(type_id, str) or not type_id.strip(): + raise RuntimeError(f"plugin {plugin_module.__name__} missing string 'type'") + if not isinstance(order, int): + raise RuntimeError(f"plugin {plugin_module.__name__} missing int 'order'") + if module is None: + raise RuntimeError(f"plugin {plugin_module.__name__} missing 'module'") + plugins.append(ItemTypePlugin(type=type_id.strip(), order=order, module=module)) + + if not plugins: + raise RuntimeError("no item type plugins discovered") + + seen: set[str] = set() + for plugin in plugins: + if plugin.type in seen: + raise RuntimeError(f"duplicate item type plugin registered: {plugin.type}") + seen.add(plugin.type) + + plugins.sort(key=lambda plugin: (plugin.order, plugin.type)) + return plugins + + +ITEM_PLUGINS: tuple[ItemTypePlugin, ...] = tuple(_load_item_type_plugins()) +ITEM_TYPE_ORDER: tuple[str, ...] = tuple(plugin.type for plugin in ITEM_PLUGINS) +ITEM_MODULES: dict[str, ItemModule] = {plugin.type: plugin.module for plugin in ITEM_PLUGINS} diff --git a/server/app/items/types/__init__.py b/server/app/items/types/__init__.py new file mode 100644 index 0000000..c30f073 --- /dev/null +++ b/server/app/items/types/__init__.py @@ -0,0 +1 @@ +"""Item type plugin package.""" diff --git a/server/app/items/types/clock/__init__.py b/server/app/items/types/clock/__init__.py new file mode 100644 index 0000000..c30f073 --- /dev/null +++ b/server/app/items/types/clock/__init__.py @@ -0,0 +1 @@ +"""Item type plugin package.""" diff --git a/server/app/items/types/clock/plugin.py b/server/app/items/types/clock/plugin.py new file mode 100644 index 0000000..10026e2 --- /dev/null +++ b/server/app/items/types/clock/plugin.py @@ -0,0 +1,11 @@ +"""Plugin registration for clock item type.""" + +from __future__ import annotations + +from ... import clock + +ITEM_TYPE_PLUGIN = { + "type": "clock", + "order": 10, + "module": clock, +} diff --git a/server/app/items/types/dice/__init__.py b/server/app/items/types/dice/__init__.py new file mode 100644 index 0000000..c30f073 --- /dev/null +++ b/server/app/items/types/dice/__init__.py @@ -0,0 +1 @@ +"""Item type plugin package.""" diff --git a/server/app/items/types/dice/plugin.py b/server/app/items/types/dice/plugin.py new file mode 100644 index 0000000..cdfa9c6 --- /dev/null +++ b/server/app/items/types/dice/plugin.py @@ -0,0 +1,11 @@ +"""Plugin registration for dice item type.""" + +from __future__ import annotations + +from ... import dice + +ITEM_TYPE_PLUGIN = { + "type": "dice", + "order": 20, + "module": dice, +} diff --git a/server/app/items/types/piano/__init__.py b/server/app/items/types/piano/__init__.py new file mode 100644 index 0000000..c30f073 --- /dev/null +++ b/server/app/items/types/piano/__init__.py @@ -0,0 +1 @@ +"""Item type plugin package.""" diff --git a/server/app/items/types/piano/plugin.py b/server/app/items/types/piano/plugin.py new file mode 100644 index 0000000..9824d2f --- /dev/null +++ b/server/app/items/types/piano/plugin.py @@ -0,0 +1,11 @@ +"""Plugin registration for piano item type.""" + +from __future__ import annotations + +from ... import piano + +ITEM_TYPE_PLUGIN = { + "type": "piano", + "order": 30, + "module": piano, +} diff --git a/server/app/items/types/radio_station/__init__.py b/server/app/items/types/radio_station/__init__.py new file mode 100644 index 0000000..c30f073 --- /dev/null +++ b/server/app/items/types/radio_station/__init__.py @@ -0,0 +1 @@ +"""Item type plugin package.""" diff --git a/server/app/items/types/radio_station/plugin.py b/server/app/items/types/radio_station/plugin.py new file mode 100644 index 0000000..382fdd5 --- /dev/null +++ b/server/app/items/types/radio_station/plugin.py @@ -0,0 +1,11 @@ +"""Plugin registration for radio_station item type.""" + +from __future__ import annotations + +from ... import radio + +ITEM_TYPE_PLUGIN = { + "type": "radio_station", + "order": 40, + "module": radio, +} diff --git a/server/app/items/types/wheel/__init__.py b/server/app/items/types/wheel/__init__.py new file mode 100644 index 0000000..c30f073 --- /dev/null +++ b/server/app/items/types/wheel/__init__.py @@ -0,0 +1 @@ +"""Item type plugin package.""" diff --git a/server/app/items/types/wheel/plugin.py b/server/app/items/types/wheel/plugin.py new file mode 100644 index 0000000..1e413fd --- /dev/null +++ b/server/app/items/types/wheel/plugin.py @@ -0,0 +1,11 @@ +"""Plugin registration for wheel item type.""" + +from __future__ import annotations + +from ... import wheel + +ITEM_TYPE_PLUGIN = { + "type": "wheel", + "order": 50, + "module": wheel, +} diff --git a/server/app/items/types/widget/__init__.py b/server/app/items/types/widget/__init__.py new file mode 100644 index 0000000..c30f073 --- /dev/null +++ b/server/app/items/types/widget/__init__.py @@ -0,0 +1 @@ +"""Item type plugin package.""" diff --git a/server/app/items/types/widget/plugin.py b/server/app/items/types/widget/plugin.py new file mode 100644 index 0000000..25a435a --- /dev/null +++ b/server/app/items/types/widget/plugin.py @@ -0,0 +1,11 @@ +"""Plugin registration for widget item type.""" + +from __future__ import annotations + +from ... import widget + +ITEM_TYPE_PLUGIN = { + "type": "widget", + "order": 60, + "module": widget, +}