Use structured piano status packets instead of message text matching
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// Maintainer-controlled web client version.
|
||||
// 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.
|
||||
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. */
|
||||
stopAllRemoteNotesForSender(senderId: string): void {
|
||||
for (const behavior of this.behaviors) {
|
||||
|
||||
@@ -11,17 +11,6 @@ export function createPianoBehavior(deps: ItemBehaviorDeps): ItemBehavior {
|
||||
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 {
|
||||
onInit: async () => {
|
||||
await controller.loadHelpFromUrl(deps.withBase('piano.json'));
|
||||
@@ -30,25 +19,10 @@ export function createPianoBehavior(deps: ItemBehaviorDeps): ItemBehavior {
|
||||
onCleanup: () => {
|
||||
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) => {
|
||||
if (message.action !== 'use') return false;
|
||||
if (!statusMessages.has(message.message)) return false;
|
||||
if (message.action !== 'use' || typeof message.itemId !== 'string') return false;
|
||||
const item = deps.state.items.get(message.itemId);
|
||||
if (item?.type !== 'piano') return false;
|
||||
deps.updateStatus(message.message);
|
||||
if (message.ok) {
|
||||
deps.audio.sfxUiBlip();
|
||||
@@ -57,6 +31,9 @@ export function createPianoBehavior(deps: ItemBehaviorDeps): ItemBehavior {
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onPianoStatus: (message) => {
|
||||
controller.onPianoStatus(message);
|
||||
},
|
||||
onPropertyPreviewChange: (item, key, value) => {
|
||||
controller.onPreviewPropertyChange(item, key, value);
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
isPianoInstrumentId,
|
||||
type PianoInstrumentId,
|
||||
} 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 { getItemPropertyOptionValues } from '../../itemRegistry';
|
||||
|
||||
@@ -494,25 +494,36 @@ export class PianoController {
|
||||
}
|
||||
}
|
||||
|
||||
/** Applies recording-state transitions from successful piano use result messages. */
|
||||
onUseResultMessage(message: IncomingMessage): void {
|
||||
if (
|
||||
message.type !== 'item_action_result' ||
|
||||
!message.ok ||
|
||||
message.action !== 'use' ||
|
||||
typeof message.itemId !== 'string' ||
|
||||
!this.activePianoItemId ||
|
||||
message.itemId !== this.activePianoItemId
|
||||
) {
|
||||
/** Applies server-reported piano mode/recording/playback state transitions. */
|
||||
onPianoStatus(message: {
|
||||
itemId: string;
|
||||
event:
|
||||
| 'use_mode_entered'
|
||||
| 'record_started'
|
||||
| 'record_paused'
|
||||
| 'record_resumed'
|
||||
| 'record_stopped'
|
||||
| 'playback_started'
|
||||
| 'playback_stopped';
|
||||
recordingState?: 'idle' | 'recording' | 'paused' | 'playback';
|
||||
}): void {
|
||||
if (message.event === 'use_mode_entered') {
|
||||
void this.startUseMode(message.itemId);
|
||||
return;
|
||||
}
|
||||
if (message.message === 'record' || message.message === 'resume') {
|
||||
this.activePianoRecordingState = 'recording';
|
||||
} else if (message.message === 'pause') {
|
||||
this.activePianoRecordingState = 'paused';
|
||||
} else if (message.message === 'stop') {
|
||||
this.activePianoRecordingState = 'idle';
|
||||
if (!this.activePianoItemId || message.itemId !== this.activePianoItemId) {
|
||||
return;
|
||||
}
|
||||
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. */
|
||||
|
||||
@@ -32,5 +32,6 @@ export type ItemBehavior = {
|
||||
handleModeInput?: (mode: GameMode, code: string) => boolean;
|
||||
handleModeKeyUp?: (mode: GameMode, code: string) => boolean;
|
||||
onRemotePianoNote?: (message: Extract<IncomingMessage, { type: 'item_piano_note' }>) => void;
|
||||
onPianoStatus?: (message: Extract<IncomingMessage, { type: 'item_piano_status' }>) => void;
|
||||
onStopAllRemoteNotesForSender?: (senderId: string) => void;
|
||||
};
|
||||
|
||||
@@ -1379,6 +1379,7 @@ const onAppMessage = createOnMessageHandler({
|
||||
},
|
||||
handleItemActionResultStatus: (message) => itemBehaviorRegistry.onActionResultStatus(message),
|
||||
handleRemotePianoNote: (message) => itemBehaviorRegistry.onRemotePianoNote(message),
|
||||
handlePianoStatus: (message) => itemBehaviorRegistry.onPianoStatus(message),
|
||||
stopAllRemoteNotesForSender: (senderId) => itemBehaviorRegistry.stopAllRemoteNotesForSender(senderId),
|
||||
TELEPORT_SOUND_URL,
|
||||
TELEPORT_START_SOUND_URL,
|
||||
|
||||
@@ -49,6 +49,7 @@ type MessageHandlerDeps = {
|
||||
playRemoteSpatialStepOrTeleport: (url: string, peerX: number, peerY: number) => void;
|
||||
handleItemActionResultStatus: (message: Extract<IncomingMessage, { type: 'item_action_result' }>) => boolean;
|
||||
handleRemotePianoNote: (message: Extract<IncomingMessage, { type: 'item_piano_note' }>) => void;
|
||||
handlePianoStatus: (message: Extract<IncomingMessage, { type: 'item_piano_status' }>) => void;
|
||||
stopAllRemoteNotesForSender: (senderId: string) => void;
|
||||
TELEPORT_SOUND_URL: string;
|
||||
TELEPORT_START_SOUND_URL: string;
|
||||
@@ -272,6 +273,11 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco
|
||||
deps.handleRemotePianoNote(message);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'item_piano_status': {
|
||||
deps.handlePianoStatus(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -180,6 +180,21 @@ export const itemPianoNoteSchema = z.object({
|
||||
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', [
|
||||
welcomeMessageSchema,
|
||||
signalMessageSchema,
|
||||
@@ -194,6 +209,7 @@ export const incomingMessageSchema = z.discriminatedUnion('type', [
|
||||
itemActionResultSchema,
|
||||
itemUseSoundSchema,
|
||||
itemPianoNoteSchema,
|
||||
itemPianoStatusSchema,
|
||||
]);
|
||||
|
||||
export type IncomingMessage = z.infer<typeof incomingMessageSchema>;
|
||||
|
||||
Reference in New Issue
Block a user