Use structured piano status packets instead of message text matching

This commit is contained in:
Jage9
2026-02-24 19:56:44 -05:00
parent 7488ac9f67
commit fe07fa3e8f
13 changed files with 155 additions and 61 deletions

View File

@@ -247,3 +247,18 @@ class ItemPianoNoteBroadcastPacket(BasePacket):
x: int
y: int
emitRange: int
class ItemPianoStatusPacket(BasePacket):
type: Literal["item_piano_status"]
itemId: str
event: Literal[
"use_mode_entered",
"record_started",
"record_paused",
"record_resumed",
"record_stopped",
"playback_started",
"playback_stopped",
]
recordingState: Literal["idle", "recording", "paused", "playback"] | None = None

View File

@@ -52,6 +52,7 @@ from .models import (
ItemPianoNoteBroadcastPacket,
ItemPianoNotePacket,
ItemPianoRecordingPacket,
ItemPianoStatusPacket,
ItemPickupPacket,
ItemRemovePacket,
ItemUpdatePacket,
@@ -328,7 +329,7 @@ class SignalingServer:
elapsed_ms += max(0, int((now_value - float(last_resume)) * 1000))
return max(0, elapsed_ms)
async def _finalize_piano_recording(self, item_id: str, *, status_message: str | None = None) -> None:
async def _finalize_piano_recording(self, item_id: str, *, notify_owner: bool = False) -> None:
"""Persist and broadcast one active recording session, then clear runtime state."""
session = self.piano_recording_state_by_item.pop(item_id, None)
@@ -414,8 +415,14 @@ class SignalingServer:
await self._broadcast_item(item)
owner_id = str(session.get("ownerClientId", ""))
owner = self._get_client_by_id(owner_id) if owner_id else None
if owner and status_message:
await self._send_item_result(owner, True, "use", status_message, item.id)
if owner and notify_owner:
await self._send_piano_status(
owner,
item_id=item.id,
event="record_stopped",
recording_state="idle",
)
await self._send_item_result(owner, True, "use", "Recording stopped.", item.id)
async def _auto_stop_piano_recording(self, item_id: str) -> None:
"""Stop a recording automatically at the max recording duration."""
@@ -426,7 +433,7 @@ class SignalingServer:
if not isinstance(session, dict):
return
if self._recording_elapsed_ms(session) >= PIANO_RECORDING_MAX_MS:
await self._finalize_piano_recording(item_id, status_message="stop")
await self._finalize_piano_recording(item_id, notify_owner=True)
return
await asyncio.sleep(0.25)
except asyncio.CancelledError:
@@ -636,6 +643,34 @@ class SignalingServer:
),
)
async def _send_piano_status(
self,
client: ClientConnection,
*,
item_id: str,
event: Literal[
"use_mode_entered",
"record_started",
"record_paused",
"record_resumed",
"record_stopped",
"playback_started",
"playback_stopped",
],
recording_state: Literal["idle", "recording", "paused", "playback"] | None = None,
) -> None:
"""Send structured piano state transitions without relying on status-message text."""
await self._send(
client.websocket,
ItemPianoStatusPacket(
type="item_piano_status",
itemId=item_id,
event=event,
recordingState=recording_state,
),
)
async def _broadcast_item(self, item: WorldItem) -> None:
"""Broadcast a full item snapshot update to all connected clients."""
@@ -1100,6 +1135,13 @@ class SignalingServer:
y=item.y,
)
)
if item.type == "piano":
await self._send_piano_status(
client,
item_id=item.id,
event="use_mode_entered",
recording_state="idle",
)
await self._send_item_result(client, True, "use", use_result.self_message, item.id)
if use_result.delayed_self_message is not None and use_result.delayed_others_message is not None:
asyncio.create_task(
@@ -1156,7 +1198,7 @@ class SignalingServer:
}
)
if elapsed_ms >= PIANO_RECORDING_MAX_MS:
await self._finalize_piano_recording(item.id, status_message="stop")
await self._finalize_piano_recording(item.id, notify_owner=True)
await self._broadcast_item_piano_note(
item,
sender_id=client.id,
@@ -1188,12 +1230,14 @@ class SignalingServer:
if existing.get("paused") is True:
existing["paused"] = False
existing["lastResumeMonotonic"] = time.monotonic()
await self._send_item_result(client, True, "use", "resume", item.id)
await self._send_piano_status(client, item_id=item.id, event="record_resumed", recording_state="recording")
await self._send_item_result(client, True, "use", "Recording resumed.", item.id)
else:
existing["elapsedMs"] = self._recording_elapsed_ms(existing)
existing["paused"] = True
existing.pop("lastResumeMonotonic", None)
await self._send_item_result(client, True, "use", "pause", item.id)
await self._send_piano_status(client, item_id=item.id, event="record_paused", recording_state="paused")
await self._send_item_result(client, True, "use", "Recording paused.", item.id)
return
self._cancel_piano_playback(item.id)
recording_state = {
@@ -1206,7 +1250,8 @@ class SignalingServer:
self.piano_recording_state_by_item[item.id] = recording_state
auto_stop_task = asyncio.create_task(self._auto_stop_piano_recording(item.id))
recording_state["autoStopTask"] = auto_stop_task
await self._send_item_result(client, True, "use", "record", item.id)
await self._send_piano_status(client, item_id=item.id, event="record_started", recording_state="recording")
await self._send_item_result(client, True, "use", "Recording started.", item.id)
return
if packet.action == "stop_record":
@@ -1215,9 +1260,10 @@ class SignalingServer:
await self._send_item_result(client, False, "use", "This piano is already recording.", item.id)
return
if existing and existing.get("ownerClientId") == client.id:
await self._finalize_piano_recording(item.id, status_message="stop")
await self._finalize_piano_recording(item.id, notify_owner=True)
return
await self._send_item_result(client, True, "use", "stop", item.id)
await self._send_piano_status(client, item_id=item.id, event="record_stopped", recording_state="idle")
await self._send_item_result(client, True, "use", "Recording stopped.", item.id)
return
if packet.action == "playback":
@@ -1232,12 +1278,14 @@ class SignalingServer:
self._cancel_piano_playback(item.id)
playback_task = asyncio.create_task(self._start_piano_playback(item))
self.piano_playback_tasks_by_item[item.id] = playback_task
await self._send_item_result(client, True, "use", "play", item.id)
await self._send_piano_status(client, item_id=item.id, event="playback_started", recording_state="playback")
await self._send_item_result(client, True, "use", "Playback started.", item.id)
return
if packet.action == "stop_playback":
self._cancel_piano_playback(item.id)
await self._send_item_result(client, True, "use", "stop", item.id)
await self._send_piano_status(client, item_id=item.id, event="playback_stopped", recording_state="idle")
await self._send_item_result(client, True, "use", "Playback stopped.", item.id)
return
return

View File

@@ -591,6 +591,8 @@ async def test_piano_recording_toggle_and_save(monkeypatch: pytest.MonkeyPatch)
client,
json.dumps({"type": "item_piano_recording", "itemId": item.id, "action": "toggle_record"}),
)
assert send_payloads[-2].type == "item_piano_status"
assert send_payloads[-2].event == "record_started"
assert send_payloads[-1].ok is True
assert item.id in server.piano_recording_state_by_item
@@ -606,16 +608,20 @@ async def test_piano_recording_toggle_and_save(monkeypatch: pytest.MonkeyPatch)
client,
json.dumps({"type": "item_piano_recording", "itemId": item.id, "action": "toggle_record"}),
)
assert send_payloads[-2].type == "item_piano_status"
assert send_payloads[-2].event == "record_paused"
assert send_payloads[-1].ok is True
assert send_payloads[-1].message == "pause"
assert send_payloads[-1].message == "Recording paused."
assert item.id in server.piano_recording_state_by_item
await server._handle_message(
client,
json.dumps({"type": "item_piano_recording", "itemId": item.id, "action": "stop_record"}),
)
assert send_payloads[-2].type == "item_piano_status"
assert send_payloads[-2].event == "record_stopped"
assert send_payloads[-1].ok is True
assert send_payloads[-1].message == "stop"
assert send_payloads[-1].message == "Recording stopped."
assert item.id not in server.piano_recording_state_by_item
song_id = item.params.get("songId")
assert isinstance(song_id, str)
@@ -665,6 +671,8 @@ async def test_piano_playback_starts_task(monkeypatch: pytest.MonkeyPatch) -> No
client,
json.dumps({"type": "item_piano_recording", "itemId": item.id, "action": "playback"}),
)
assert send_payloads[-2].type == "item_piano_status"
assert send_payloads[-2].event == "playback_started"
assert send_payloads[-1].ok is True
task = server.piano_playback_tasks_by_item.get(item.id)
assert task is not None