refactor: complete server-first item schema wiring and plugin contract checks
This commit is contained in:
@@ -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."""
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."},
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user