Use structured piano status packets instead of message text matching
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
// Maintainer-controlled web client version.
|
// Maintainer-controlled web client version.
|
||||||
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
||||||
window.CHGRID_WEB_VERSION = "2026.02.25 R231";
|
window.CHGRID_WEB_VERSION = "2026.02.25 R232";
|
||||||
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
||||||
window.CHGRID_TIME_ZONE = "America/Detroit";
|
window.CHGRID_TIME_ZONE = "America/Detroit";
|
||||||
|
|||||||
@@ -83,6 +83,13 @@ export class ItemBehaviorRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Routes incoming item-piano-status packets to behavior modules that track piano runtime state. */
|
||||||
|
onPianoStatus(message: Extract<IncomingMessage, { type: 'item_piano_status' }>): void {
|
||||||
|
for (const behavior of this.behaviors) {
|
||||||
|
behavior.onPianoStatus?.(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Stops all remote notes for one sender across behavior modules that own remote note runtimes. */
|
/** Stops all remote notes for one sender across behavior modules that own remote note runtimes. */
|
||||||
stopAllRemoteNotesForSender(senderId: string): void {
|
stopAllRemoteNotesForSender(senderId: string): void {
|
||||||
for (const behavior of this.behaviors) {
|
for (const behavior of this.behaviors) {
|
||||||
|
|||||||
@@ -11,17 +11,6 @@ export function createPianoBehavior(deps: ItemBehaviorDeps): ItemBehavior {
|
|||||||
openHelpViewer: deps.openHelpViewer,
|
openHelpViewer: deps.openHelpViewer,
|
||||||
});
|
});
|
||||||
|
|
||||||
const statusMessages = new Set([
|
|
||||||
'record',
|
|
||||||
'pause',
|
|
||||||
'resume',
|
|
||||||
'play',
|
|
||||||
'stop',
|
|
||||||
'No recording saved on this piano.',
|
|
||||||
'Stop recording before playback.',
|
|
||||||
'This piano is already recording.',
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onInit: async () => {
|
onInit: async () => {
|
||||||
await controller.loadHelpFromUrl(deps.withBase('piano.json'));
|
await controller.loadHelpFromUrl(deps.withBase('piano.json'));
|
||||||
@@ -30,25 +19,10 @@ export function createPianoBehavior(deps: ItemBehaviorDeps): ItemBehavior {
|
|||||||
onCleanup: () => {
|
onCleanup: () => {
|
||||||
controller.cleanup();
|
controller.cleanup();
|
||||||
},
|
},
|
||||||
onUseResultMessage: (message) => {
|
|
||||||
controller.onUseResultMessage(message);
|
|
||||||
if (
|
|
||||||
message.type === 'item_action_result' &&
|
|
||||||
message.ok &&
|
|
||||||
message.action === 'use' &&
|
|
||||||
typeof message.itemId === 'string' &&
|
|
||||||
typeof message.message === 'string' &&
|
|
||||||
message.message.toLowerCase().includes('begin playing')
|
|
||||||
) {
|
|
||||||
const item = deps.state.items.get(message.itemId);
|
|
||||||
if (item?.type === 'piano') {
|
|
||||||
void controller.startUseMode(item.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onActionResultStatus: (message) => {
|
onActionResultStatus: (message) => {
|
||||||
if (message.action !== 'use') return false;
|
if (message.action !== 'use' || typeof message.itemId !== 'string') return false;
|
||||||
if (!statusMessages.has(message.message)) return false;
|
const item = deps.state.items.get(message.itemId);
|
||||||
|
if (item?.type !== 'piano') return false;
|
||||||
deps.updateStatus(message.message);
|
deps.updateStatus(message.message);
|
||||||
if (message.ok) {
|
if (message.ok) {
|
||||||
deps.audio.sfxUiBlip();
|
deps.audio.sfxUiBlip();
|
||||||
@@ -57,6 +31,9 @@ export function createPianoBehavior(deps: ItemBehaviorDeps): ItemBehavior {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
onPianoStatus: (message) => {
|
||||||
|
controller.onPianoStatus(message);
|
||||||
|
},
|
||||||
onPropertyPreviewChange: (item, key, value) => {
|
onPropertyPreviewChange: (item, key, value) => {
|
||||||
controller.onPreviewPropertyChange(item, key, value);
|
controller.onPreviewPropertyChange(item, key, value);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
isPianoInstrumentId,
|
isPianoInstrumentId,
|
||||||
type PianoInstrumentId,
|
type PianoInstrumentId,
|
||||||
} from '../../../audio/pianoSynth';
|
} from '../../../audio/pianoSynth';
|
||||||
import { type IncomingMessage, type OutgoingMessage } from '../../../network/protocol';
|
import { type OutgoingMessage } from '../../../network/protocol';
|
||||||
import { type GameMode, type WorldItem } from '../../../state/gameState';
|
import { type GameMode, type WorldItem } from '../../../state/gameState';
|
||||||
import { getItemPropertyOptionValues } from '../../itemRegistry';
|
import { getItemPropertyOptionValues } from '../../itemRegistry';
|
||||||
|
|
||||||
@@ -494,25 +494,36 @@ export class PianoController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Applies recording-state transitions from successful piano use result messages. */
|
/** Applies server-reported piano mode/recording/playback state transitions. */
|
||||||
onUseResultMessage(message: IncomingMessage): void {
|
onPianoStatus(message: {
|
||||||
if (
|
itemId: string;
|
||||||
message.type !== 'item_action_result' ||
|
event:
|
||||||
!message.ok ||
|
| 'use_mode_entered'
|
||||||
message.action !== 'use' ||
|
| 'record_started'
|
||||||
typeof message.itemId !== 'string' ||
|
| 'record_paused'
|
||||||
!this.activePianoItemId ||
|
| 'record_resumed'
|
||||||
message.itemId !== this.activePianoItemId
|
| 'record_stopped'
|
||||||
) {
|
| 'playback_started'
|
||||||
|
| 'playback_stopped';
|
||||||
|
recordingState?: 'idle' | 'recording' | 'paused' | 'playback';
|
||||||
|
}): void {
|
||||||
|
if (message.event === 'use_mode_entered') {
|
||||||
|
void this.startUseMode(message.itemId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (message.message === 'record' || message.message === 'resume') {
|
if (!this.activePianoItemId || message.itemId !== this.activePianoItemId) {
|
||||||
this.activePianoRecordingState = 'recording';
|
return;
|
||||||
} else if (message.message === 'pause') {
|
|
||||||
this.activePianoRecordingState = 'paused';
|
|
||||||
} else if (message.message === 'stop') {
|
|
||||||
this.activePianoRecordingState = 'idle';
|
|
||||||
}
|
}
|
||||||
|
const state = message.recordingState;
|
||||||
|
if (state === 'recording') {
|
||||||
|
this.activePianoRecordingState = 'recording';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state === 'paused') {
|
||||||
|
this.activePianoRecordingState = 'paused';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.activePianoRecordingState = 'idle';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Exits piano mode if the active piano item disappears from local world state. */
|
/** Exits piano mode if the active piano item disappears from local world state. */
|
||||||
|
|||||||
@@ -32,5 +32,6 @@ export type ItemBehavior = {
|
|||||||
handleModeInput?: (mode: GameMode, code: string) => boolean;
|
handleModeInput?: (mode: GameMode, code: string) => boolean;
|
||||||
handleModeKeyUp?: (mode: GameMode, code: string) => boolean;
|
handleModeKeyUp?: (mode: GameMode, code: string) => boolean;
|
||||||
onRemotePianoNote?: (message: Extract<IncomingMessage, { type: 'item_piano_note' }>) => void;
|
onRemotePianoNote?: (message: Extract<IncomingMessage, { type: 'item_piano_note' }>) => void;
|
||||||
|
onPianoStatus?: (message: Extract<IncomingMessage, { type: 'item_piano_status' }>) => void;
|
||||||
onStopAllRemoteNotesForSender?: (senderId: string) => void;
|
onStopAllRemoteNotesForSender?: (senderId: string) => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1379,6 +1379,7 @@ const onAppMessage = createOnMessageHandler({
|
|||||||
},
|
},
|
||||||
handleItemActionResultStatus: (message) => itemBehaviorRegistry.onActionResultStatus(message),
|
handleItemActionResultStatus: (message) => itemBehaviorRegistry.onActionResultStatus(message),
|
||||||
handleRemotePianoNote: (message) => itemBehaviorRegistry.onRemotePianoNote(message),
|
handleRemotePianoNote: (message) => itemBehaviorRegistry.onRemotePianoNote(message),
|
||||||
|
handlePianoStatus: (message) => itemBehaviorRegistry.onPianoStatus(message),
|
||||||
stopAllRemoteNotesForSender: (senderId) => itemBehaviorRegistry.stopAllRemoteNotesForSender(senderId),
|
stopAllRemoteNotesForSender: (senderId) => itemBehaviorRegistry.stopAllRemoteNotesForSender(senderId),
|
||||||
TELEPORT_SOUND_URL,
|
TELEPORT_SOUND_URL,
|
||||||
TELEPORT_START_SOUND_URL,
|
TELEPORT_START_SOUND_URL,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ type MessageHandlerDeps = {
|
|||||||
playRemoteSpatialStepOrTeleport: (url: string, peerX: number, peerY: number) => void;
|
playRemoteSpatialStepOrTeleport: (url: string, peerX: number, peerY: number) => void;
|
||||||
handleItemActionResultStatus: (message: Extract<IncomingMessage, { type: 'item_action_result' }>) => boolean;
|
handleItemActionResultStatus: (message: Extract<IncomingMessage, { type: 'item_action_result' }>) => boolean;
|
||||||
handleRemotePianoNote: (message: Extract<IncomingMessage, { type: 'item_piano_note' }>) => void;
|
handleRemotePianoNote: (message: Extract<IncomingMessage, { type: 'item_piano_note' }>) => void;
|
||||||
|
handlePianoStatus: (message: Extract<IncomingMessage, { type: 'item_piano_status' }>) => void;
|
||||||
stopAllRemoteNotesForSender: (senderId: string) => void;
|
stopAllRemoteNotesForSender: (senderId: string) => void;
|
||||||
TELEPORT_SOUND_URL: string;
|
TELEPORT_SOUND_URL: string;
|
||||||
TELEPORT_START_SOUND_URL: string;
|
TELEPORT_START_SOUND_URL: string;
|
||||||
@@ -272,6 +273,11 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco
|
|||||||
deps.handleRemotePianoNote(message);
|
deps.handleRemotePianoNote(message);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'item_piano_status': {
|
||||||
|
deps.handlePianoStatus(message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,6 +180,21 @@ export const itemPianoNoteSchema = z.object({
|
|||||||
emitRange: z.number().int().min(1),
|
emitRange: z.number().int().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const itemPianoStatusSchema = z.object({
|
||||||
|
type: z.literal('item_piano_status'),
|
||||||
|
itemId: z.string(),
|
||||||
|
event: z.enum([
|
||||||
|
'use_mode_entered',
|
||||||
|
'record_started',
|
||||||
|
'record_paused',
|
||||||
|
'record_resumed',
|
||||||
|
'record_stopped',
|
||||||
|
'playback_started',
|
||||||
|
'playback_stopped',
|
||||||
|
]),
|
||||||
|
recordingState: z.enum(['idle', 'recording', 'paused', 'playback']).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const incomingMessageSchema = z.discriminatedUnion('type', [
|
export const incomingMessageSchema = z.discriminatedUnion('type', [
|
||||||
welcomeMessageSchema,
|
welcomeMessageSchema,
|
||||||
signalMessageSchema,
|
signalMessageSchema,
|
||||||
@@ -194,6 +209,7 @@ export const incomingMessageSchema = z.discriminatedUnion('type', [
|
|||||||
itemActionResultSchema,
|
itemActionResultSchema,
|
||||||
itemUseSoundSchema,
|
itemUseSoundSchema,
|
||||||
itemPianoNoteSchema,
|
itemPianoNoteSchema,
|
||||||
|
itemPianoStatusSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type IncomingMessage = z.infer<typeof incomingMessageSchema>;
|
export type IncomingMessage = z.infer<typeof incomingMessageSchema>;
|
||||||
|
|||||||
@@ -31,11 +31,14 @@ This is a behavior guide for packet semantics beyond raw schemas.
|
|||||||
- `item_action_result`: action success/failure and user-facing message.
|
- `item_action_result`: action success/failure and user-facing message.
|
||||||
- `item_use_sound`: spatial one-shot sound on successful item use (if `useSound` configured).
|
- `item_use_sound`: spatial one-shot sound on successful item use (if `useSound` configured).
|
||||||
- `item_piano_note`: broadcast piano note on/off with resolved instrument/envelope/spatial params.
|
- `item_piano_note`: broadcast piano note on/off with resolved instrument/envelope/spatial params.
|
||||||
|
- `item_piano_status`: structured piano mode/record/playback state events for client runtime control.
|
||||||
|
|
||||||
## Item Packet Behavior
|
## Item Packet Behavior
|
||||||
|
|
||||||
- `item_upsert` is full-state replacement for one item, not partial patch.
|
- `item_upsert` is full-state replacement for one item, not partial patch.
|
||||||
- `item_action_result` messages are intended for direct screen-reader/user status feedback.
|
- `item_action_result` messages are intended for direct screen-reader/user status feedback.
|
||||||
|
- Piano runtime control no longer depends on parsing `item_action_result.message` text.
|
||||||
|
- `item_piano_status` carries machine-readable piano events (`use_mode_entered`, record/playback transitions).
|
||||||
- `item_use_sound` contains absolute item world coordinates (`x`, `y`) and sound path.
|
- `item_use_sound` contains absolute item world coordinates (`x`, `y`) and sound path.
|
||||||
- `item_piano_note` contains:
|
- `item_piano_note` contains:
|
||||||
- `itemId`, `senderId`, `keyId`, `midi`, `on`
|
- `itemId`, `senderId`, `keyId`, `midi`, `on`
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ Core incoming message effects:
|
|||||||
- `item_action_result`: success/error status for actions.
|
- `item_action_result`: success/error status for actions.
|
||||||
- `item_use_sound`: play one-shot spatial sample (world layer gated).
|
- `item_use_sound`: play one-shot spatial sample (world layer gated).
|
||||||
- `item_piano_note`: start/stop synthesized piano notes from remote users (item layer gated).
|
- `item_piano_note`: start/stop synthesized piano notes from remote users (item layer gated).
|
||||||
|
- `item_piano_status`: structured piano mode/record/playback transitions (client runtime state).
|
||||||
- `pong`:
|
- `pong`:
|
||||||
- positive `clientSentAt`: user ping response (`P` command)
|
- positive `clientSentAt`: user ping response (`P` command)
|
||||||
- negative `clientSentAt`: internal heartbeat response
|
- negative `clientSentAt`: internal heartbeat response
|
||||||
|
|||||||
@@ -247,3 +247,18 @@ class ItemPianoNoteBroadcastPacket(BasePacket):
|
|||||||
x: int
|
x: int
|
||||||
y: int
|
y: int
|
||||||
emitRange: 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
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ from .models import (
|
|||||||
ItemPianoNoteBroadcastPacket,
|
ItemPianoNoteBroadcastPacket,
|
||||||
ItemPianoNotePacket,
|
ItemPianoNotePacket,
|
||||||
ItemPianoRecordingPacket,
|
ItemPianoRecordingPacket,
|
||||||
|
ItemPianoStatusPacket,
|
||||||
ItemPickupPacket,
|
ItemPickupPacket,
|
||||||
ItemRemovePacket,
|
ItemRemovePacket,
|
||||||
ItemUpdatePacket,
|
ItemUpdatePacket,
|
||||||
@@ -328,7 +329,7 @@ class SignalingServer:
|
|||||||
elapsed_ms += max(0, int((now_value - float(last_resume)) * 1000))
|
elapsed_ms += max(0, int((now_value - float(last_resume)) * 1000))
|
||||||
return max(0, elapsed_ms)
|
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."""
|
"""Persist and broadcast one active recording session, then clear runtime state."""
|
||||||
|
|
||||||
session = self.piano_recording_state_by_item.pop(item_id, None)
|
session = self.piano_recording_state_by_item.pop(item_id, None)
|
||||||
@@ -414,8 +415,14 @@ class SignalingServer:
|
|||||||
await self._broadcast_item(item)
|
await self._broadcast_item(item)
|
||||||
owner_id = str(session.get("ownerClientId", ""))
|
owner_id = str(session.get("ownerClientId", ""))
|
||||||
owner = self._get_client_by_id(owner_id) if owner_id else None
|
owner = self._get_client_by_id(owner_id) if owner_id else None
|
||||||
if owner and status_message:
|
if owner and notify_owner:
|
||||||
await self._send_item_result(owner, True, "use", status_message, item.id)
|
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:
|
async def _auto_stop_piano_recording(self, item_id: str) -> None:
|
||||||
"""Stop a recording automatically at the max recording duration."""
|
"""Stop a recording automatically at the max recording duration."""
|
||||||
@@ -426,7 +433,7 @@ class SignalingServer:
|
|||||||
if not isinstance(session, dict):
|
if not isinstance(session, dict):
|
||||||
return
|
return
|
||||||
if self._recording_elapsed_ms(session) >= PIANO_RECORDING_MAX_MS:
|
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
|
return
|
||||||
await asyncio.sleep(0.25)
|
await asyncio.sleep(0.25)
|
||||||
except asyncio.CancelledError:
|
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:
|
async def _broadcast_item(self, item: WorldItem) -> None:
|
||||||
"""Broadcast a full item snapshot update to all connected clients."""
|
"""Broadcast a full item snapshot update to all connected clients."""
|
||||||
|
|
||||||
@@ -1100,6 +1135,13 @@ class SignalingServer:
|
|||||||
y=item.y,
|
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)
|
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:
|
if use_result.delayed_self_message is not None and use_result.delayed_others_message is not None:
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
@@ -1156,7 +1198,7 @@ class SignalingServer:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
if elapsed_ms >= PIANO_RECORDING_MAX_MS:
|
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(
|
await self._broadcast_item_piano_note(
|
||||||
item,
|
item,
|
||||||
sender_id=client.id,
|
sender_id=client.id,
|
||||||
@@ -1188,12 +1230,14 @@ class SignalingServer:
|
|||||||
if existing.get("paused") is True:
|
if existing.get("paused") is True:
|
||||||
existing["paused"] = False
|
existing["paused"] = False
|
||||||
existing["lastResumeMonotonic"] = time.monotonic()
|
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:
|
else:
|
||||||
existing["elapsedMs"] = self._recording_elapsed_ms(existing)
|
existing["elapsedMs"] = self._recording_elapsed_ms(existing)
|
||||||
existing["paused"] = True
|
existing["paused"] = True
|
||||||
existing.pop("lastResumeMonotonic", None)
|
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
|
return
|
||||||
self._cancel_piano_playback(item.id)
|
self._cancel_piano_playback(item.id)
|
||||||
recording_state = {
|
recording_state = {
|
||||||
@@ -1206,7 +1250,8 @@ class SignalingServer:
|
|||||||
self.piano_recording_state_by_item[item.id] = recording_state
|
self.piano_recording_state_by_item[item.id] = recording_state
|
||||||
auto_stop_task = asyncio.create_task(self._auto_stop_piano_recording(item.id))
|
auto_stop_task = asyncio.create_task(self._auto_stop_piano_recording(item.id))
|
||||||
recording_state["autoStopTask"] = auto_stop_task
|
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
|
return
|
||||||
|
|
||||||
if packet.action == "stop_record":
|
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)
|
await self._send_item_result(client, False, "use", "This piano is already recording.", item.id)
|
||||||
return
|
return
|
||||||
if existing and existing.get("ownerClientId") == client.id:
|
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
|
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
|
return
|
||||||
|
|
||||||
if packet.action == "playback":
|
if packet.action == "playback":
|
||||||
@@ -1232,12 +1278,14 @@ class SignalingServer:
|
|||||||
self._cancel_piano_playback(item.id)
|
self._cancel_piano_playback(item.id)
|
||||||
playback_task = asyncio.create_task(self._start_piano_playback(item))
|
playback_task = asyncio.create_task(self._start_piano_playback(item))
|
||||||
self.piano_playback_tasks_by_item[item.id] = playback_task
|
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
|
return
|
||||||
|
|
||||||
if packet.action == "stop_playback":
|
if packet.action == "stop_playback":
|
||||||
self._cancel_piano_playback(item.id)
|
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
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -591,6 +591,8 @@ async def test_piano_recording_toggle_and_save(monkeypatch: pytest.MonkeyPatch)
|
|||||||
client,
|
client,
|
||||||
json.dumps({"type": "item_piano_recording", "itemId": item.id, "action": "toggle_record"}),
|
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 send_payloads[-1].ok is True
|
||||||
assert item.id in server.piano_recording_state_by_item
|
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,
|
client,
|
||||||
json.dumps({"type": "item_piano_recording", "itemId": item.id, "action": "toggle_record"}),
|
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].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
|
assert item.id in server.piano_recording_state_by_item
|
||||||
|
|
||||||
await server._handle_message(
|
await server._handle_message(
|
||||||
client,
|
client,
|
||||||
json.dumps({"type": "item_piano_recording", "itemId": item.id, "action": "stop_record"}),
|
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].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
|
assert item.id not in server.piano_recording_state_by_item
|
||||||
song_id = item.params.get("songId")
|
song_id = item.params.get("songId")
|
||||||
assert isinstance(song_id, str)
|
assert isinstance(song_id, str)
|
||||||
@@ -665,6 +671,8 @@ async def test_piano_playback_starts_task(monkeypatch: pytest.MonkeyPatch) -> No
|
|||||||
client,
|
client,
|
||||||
json.dumps({"type": "item_piano_recording", "itemId": item.id, "action": "playback"}),
|
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
|
assert send_payloads[-1].ok is True
|
||||||
task = server.piano_playback_tasks_by_item.get(item.id)
|
task = server.piano_playback_tasks_by_item.get(item.id)
|
||||||
assert task is not None
|
assert task is not None
|
||||||
|
|||||||
Reference in New Issue
Block a user