refactor: complete server-first item schema wiring and plugin contract checks

This commit is contained in:
Jage9
2026-02-24 18:48:08 -05:00
parent 7776676e2d
commit fcb5e85b13
20 changed files with 132 additions and 69 deletions

View File

@@ -3,11 +3,11 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal, cast
from typing import TypeAlias, cast
from .items.registry import ITEM_MODULES, ITEM_TYPE_ORDER
ItemType = Literal["radio_station", "dice", "wheel", "clock", "widget", "piano"]
ItemType: TypeAlias = str
ITEM_TYPE_SEQUENCE: tuple[ItemType, ...] = cast(tuple[ItemType, ...], ITEM_TYPE_ORDER)
ITEM_TYPE_LABELS: dict[ItemType, str] = {item_type: ITEM_MODULES[item_type].LABEL for item_type in ITEM_TYPE_SEQUENCE}
ITEM_TYPE_EDITABLE_PROPERTIES: dict[ItemType, tuple[str, ...]] = {
@@ -75,15 +75,6 @@ ITEM_DEFINITIONS: dict[ItemType, ItemDefinition] = {
for item_type in ITEM_TYPE_SEQUENCE
}
ITEM_PROPERTY_OPTIONS: dict[str, tuple[str, ...]] = {
"mediaEffect": RADIO_EFFECT_OPTIONS,
"emitEffect": RADIO_EFFECT_OPTIONS,
"mediaChannel": RADIO_CHANNEL_OPTIONS,
"timeZone": CLOCK_TIME_ZONE_OPTIONS,
"instrument": PIANO_INSTRUMENT_OPTIONS,
"voiceMode": PIANO_VOICE_MODE_OPTIONS,
}
ITEM_TYPE_TOOLTIPS: dict[ItemType, str] = {
item_type: ITEM_MODULES[item_type].TOOLTIP for item_type in ITEM_TYPE_SEQUENCE
}
@@ -125,6 +116,12 @@ def get_item_definition(item_type: ItemType) -> ItemDefinition:
return ITEM_DEFINITIONS[item_type]
def is_known_item_type(item_type: str) -> bool:
"""Return whether a string item type id exists in discovered plugins."""
return item_type in ITEM_DEFINITIONS
def get_item_use_cooldown_ms(item_type: ItemType) -> int:
"""Return validated global use cooldown in milliseconds for an item type."""

View File

@@ -8,8 +8,6 @@ import time
import uuid
from copy import deepcopy
from pathlib import Path
from typing import Literal
from .client import ClientConnection
from .item_catalog import get_item_definition
from .models import PersistedWorldItem, WorldItem
@@ -36,7 +34,7 @@ class ItemService:
return int(time.time() * 1000)
def default_item(self, client: ClientConnection, item_type: Literal["radio_station", "dice", "wheel", "clock", "widget", "piano"]) -> WorldItem:
def default_item(self, client: ClientConnection, item_type: str) -> WorldItem:
"""Create a new server-authoritative item at the caller's position."""
item_def = get_item_definition(item_type)

View File

@@ -60,6 +60,6 @@ PARAM_KEYS: tuple[str, ...] = ("timeZone", "use24Hour")
PROPERTY_METADATA: dict[str, dict[str, object]] = {
"title": {"valueType": "text", "tooltip": "Display name spoken and shown for this item.", "maxLength": 80},
"timeZone": {"valueType": "list", "tooltip": "Timezone used when the clock speaks time."},
"timeZone": {"valueType": "list", "tooltip": "Timezone used when the clock speaks time.", "options": list(TIME_ZONE_OPTIONS)},
"use24Hour": {"valueType": "boolean", "tooltip": "Use 24 hour format instead of AM/PM."},
}

View File

@@ -64,8 +64,8 @@ DEFAULT_ENVELOPE_BY_INSTRUMENT: dict[str, tuple[int, int, int, int, str, int]] =
PROPERTY_METADATA: dict[str, dict[str, object]] = {
"title": {"valueType": "text", "tooltip": "Display name spoken and shown for this item.", "maxLength": 80},
"instrument": {"valueType": "list", "tooltip": "Instrument voice used when playing this piano."},
"voiceMode": {"valueType": "list", "tooltip": "Mono plays one note at a time; poly allows chords."},
"instrument": {"valueType": "list", "tooltip": "Instrument voice used when playing this piano.", "options": list(INSTRUMENT_OPTIONS)},
"voiceMode": {"valueType": "list", "tooltip": "Mono plays one note at a time; poly allows chords.", "options": list(VOICE_MODE_OPTIONS)},
"octave": {
"valueType": "number",
"tooltip": "Shifts played notes in octaves. -1 is one octave down.",

View File

@@ -55,8 +55,8 @@ PROPERTY_METADATA: dict[str, dict[str, object]] = {
"tooltip": "Playback media volume percent for this radio.",
"range": {"min": 0, "max": 100, "step": 1},
},
"mediaChannel": {"valueType": "list", "tooltip": "Select how the station audio channels are rendered."},
"mediaEffect": {"valueType": "list", "tooltip": "Select the active radio effect."},
"mediaChannel": {"valueType": "list", "tooltip": "Select how the station audio channels are rendered.", "options": list(CHANNEL_OPTIONS)},
"mediaEffect": {"valueType": "list", "tooltip": "Select the active radio effect.", "options": list(EFFECT_OPTIONS)},
"mediaEffectValue": {
"valueType": "number",
"tooltip": "Amount for the selected effect.",

View File

@@ -83,7 +83,7 @@ PROPERTY_METADATA: dict[str, dict[str, object]] = {
"tooltip": "Playback tempo percent for emitted sound. 50 is normal, 0 is half, 100 is double. Using speed and tempo together may sound weird.",
"range": {"min": 0, "max": 100, "step": 0.1},
},
"emitEffect": {"valueType": "list", "tooltip": "Effect applied to emitted sound."},
"emitEffect": {"valueType": "list", "tooltip": "Effect applied to emitted sound.", "options": list(EFFECT_OPTIONS)},
"emitEffectValue": {
"valueType": "number",
"tooltip": "Amount for emit effect.",

View File

@@ -42,7 +42,7 @@ class PingPacket(BasePacket):
class ItemAddPacket(BasePacket):
type: Literal["item_add"]
itemType: Literal["radio_station", "dice", "wheel", "clock", "widget", "piano"]
itemType: str = Field(min_length=1)
class ItemPickupPacket(BasePacket):
@@ -173,7 +173,7 @@ class NicknameResultPacket(BasePacket):
class WorldItem(BaseModel):
id: str
type: Literal["radio_station", "dice", "wheel", "clock", "widget", "piano"]
type: str = Field(min_length=1)
title: str
x: int
y: int
@@ -191,7 +191,7 @@ class WorldItem(BaseModel):
class PersistedWorldItem(BaseModel):
model_config = ConfigDict(extra="ignore")
id: str
type: Literal["radio_station", "dice", "wheel", "clock", "widget", "piano"]
type: str = Field(min_length=1)
title: str
x: int
y: int

View File

@@ -25,14 +25,15 @@ from .config import load_config
from .item_catalog import (
CLOCK_DEFAULT_TIME_ZONE,
CLOCK_TIME_ZONE_OPTIONS,
ITEM_PROPERTY_OPTIONS,
ITEM_TYPE_EDITABLE_PROPERTIES,
ITEM_TYPE_LABELS,
ITEM_TYPE_PROPERTY_METADATA,
ITEM_TYPE_SEQUENCE,
ITEM_TYPE_TOOLTIPS,
get_item_definition,
get_item_global_properties,
get_item_use_cooldown_ms,
is_known_item_type,
)
from .item_type_handlers import get_item_type_handler
from .item_service import ItemService
@@ -708,18 +709,13 @@ class SignalingServer:
item_types: list[dict] = []
for item_type in ITEM_TYPE_SEQUENCE:
editable = list(ITEM_TYPE_EDITABLE_PROPERTIES.get(item_type, ("title",)))
property_options: dict[str, list[str]] = {}
for key in editable:
options = ITEM_PROPERTY_OPTIONS.get(key)
if options:
property_options[key] = list(options)
item_types.append(
{
"type": item_type,
"label": ITEM_TYPE_LABELS.get(item_type, item_type),
"tooltip": ITEM_TYPE_TOOLTIPS.get(item_type),
"capabilities": list(get_item_definition(item_type).capabilities),
"editableProperties": editable,
"propertyOptions": property_options,
"propertyMetadata": ITEM_TYPE_PROPERTY_METADATA.get(item_type, {}),
"globalProperties": get_item_global_properties(item_type),
}
@@ -897,6 +893,9 @@ class SignalingServer:
return
if isinstance(packet, ItemAddPacket):
if not is_known_item_type(packet.itemType):
await self._send_item_result(client, False, "add", "Unknown item type.")
return
item = self.item_service.default_item(client, packet.itemType)
self.item_service.add_item(item)
await self._broadcast_item(item)