feat(net): client subscriber + send method for custom-modifier broadcast
Wires the T24 server-side custom-modifier.register handler all the way through to per-engine custom registries on every connected client. net/types.ts: - new CustomModifierDescriptorWire interface mirroring the chess-side CustomModifierDescriptor (structurally identical; Zod-mirrored across the v3/v4 boundary). - new CustomModifierRegisterPayload + CustomModifierRegisteredPayload. - ServerMessage union extended with custom-modifier.registered envelope. - ClientMessage union extended with custom-modifier.register envelope. net/client.ts: - GameClientEvent union extended with custom-modifier.registered. - handleMessage dispatch switch routes the event to listeners. - new GameClient.sendRegisterCustomModifier(descriptor) helper that ships the message under the active room code; silent no-op when the client isn't in a room (mirrors sendMove's pre-connect guard). net/prediction.ts: - PredictionManager subscribes to custom-modifier.registered. On receipt, registers the descriptor onto BOTH baseEngine.customModifiers AND predictedEngine.customModifiers (when present) so subsequent profile applies and reconciliation from a future game.state can resolve the kind. Triggers an onStateChange so the UI re-renders. Two e2e fixmes remain — both depend on a CustomModifierEditor button that calls sendRegisterCustomModifier when the editor is opened from a multiplayer game. The wire is fully implemented; only the editor's multiplayer-aware send-button surface is missing. Documented as a T3.1 follow-up in the e2e fixme comments.
This commit is contained in:
parent
747d0fb728
commit
63c46a3f9e
3 changed files with 80 additions and 1 deletions
|
|
@ -12,6 +12,7 @@
|
|||
|
||||
import type {
|
||||
ClientMessage,
|
||||
CustomModifierRegisteredPayload,
|
||||
ErrorPayload,
|
||||
GameDeltaPayload,
|
||||
GameEndPayload,
|
||||
|
|
@ -46,6 +47,7 @@ export type GameClientEvent =
|
|||
| { type: "modifier-profile.consent-received"; payload: ModifierProfileConsentReceivedPayload }
|
||||
| { type: "modifier-profile.queued"; payload: ModifierProfileQueuedPayload }
|
||||
| { type: "modifier-profile.updated"; payload: ModifierProfileUpdatedPayload }
|
||||
| { type: "custom-modifier.registered"; payload: CustomModifierRegisteredPayload }
|
||||
| { type: "error"; payload: ErrorPayload }
|
||||
| { type: "connected" }
|
||||
| { type: "disconnected"; willReconnect: boolean };
|
||||
|
|
@ -280,6 +282,30 @@ export class GameClient {
|
|||
this.send({ type: "room.setPresets", payload });
|
||||
}
|
||||
|
||||
/**
|
||||
* T3: register a custom modifier descriptor on the current room.
|
||||
* Server validates structurally, stores in the per-room registry
|
||||
* (capped at 10 distinct descriptors), and broadcasts
|
||||
* `custom-modifier.registered` to every connected client. Clients
|
||||
* (including the sender) mirror the descriptor onto their local
|
||||
* engine's customModifiers registry via the predicate manager
|
||||
* subscriber, so subsequent profile applies can resolve the kind.
|
||||
*/
|
||||
sendRegisterCustomModifier(
|
||||
descriptor: import("./types.js").CustomModifierDescriptorWire,
|
||||
): void {
|
||||
if (this.code === null) {
|
||||
// No active room — silently no-op rather than throw, mirroring
|
||||
// sendMove's behaviour when called pre-connect.
|
||||
return;
|
||||
}
|
||||
const payload: import("./types.js").CustomModifierRegisterPayload = {
|
||||
roomCode: this.code,
|
||||
descriptor,
|
||||
};
|
||||
this.send({ type: "custom-modifier.register", payload });
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Accessors (primarily for tests & reconnect logic)
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
@ -458,6 +484,9 @@ export class GameClient {
|
|||
case "modifier-profile.updated":
|
||||
this.emit({ type, payload: payload as ModifierProfileUpdatedPayload });
|
||||
return;
|
||||
case "custom-modifier.registered":
|
||||
this.emit({ type, payload: payload as CustomModifierRegisteredPayload });
|
||||
return;
|
||||
case "error":
|
||||
this.emit({ type, payload: payload as ErrorPayload });
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -125,6 +125,23 @@ export class PredictionManager {
|
|||
}
|
||||
this.onStateChange(this.baseEngine);
|
||||
});
|
||||
// T3: when the server broadcasts a custom modifier registration,
|
||||
// mirror it onto every local engine's per-instance custom registry
|
||||
// so subsequent profile applies (or reconciliation from a future
|
||||
// game.state) can resolve the kind. The descriptor is sent
|
||||
// verbatim; we cast to the chess-side type since the wire shape
|
||||
// is structurally identical (Zod-mirrored across the v3/v4 boundary).
|
||||
this.client.on("custom-modifier.registered", (e) => {
|
||||
const descriptor = e.payload
|
||||
.descriptor as unknown as Parameters<
|
||||
typeof this.baseEngine.customModifiers.register
|
||||
>[0];
|
||||
this.baseEngine.customModifiers.register(descriptor);
|
||||
if (this.predictedEngine) {
|
||||
this.predictedEngine.customModifiers.register(descriptor);
|
||||
}
|
||||
this.onStateChange(this.baseEngine);
|
||||
});
|
||||
this.client.on("error", (e) => {
|
||||
// Fatal errors tear down the session; the app restarts from a fresh
|
||||
// `game.state`. Non-fatal errors (ILLEGAL_MOVE / NOT_YOUR_TURN / …)
|
||||
|
|
|
|||
|
|
@ -261,6 +261,37 @@ export interface ModifierProfileConsentReceivedPayload {
|
|||
roomCode: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire shape of a custom modifier descriptor. Mirrors the chess
|
||||
* package's CustomModifierDescriptor structurally; the schema
|
||||
* validation lives on the server (Zod v3) and on the client (Zod v4).
|
||||
*/
|
||||
export interface CustomModifierDescriptorWire {
|
||||
type: "data";
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
version: 1;
|
||||
primitives: ReadonlyArray<{ kind: string; params: unknown }>;
|
||||
targetAttrs: readonly string[];
|
||||
uiForm: "primitive-composer";
|
||||
source: "custom";
|
||||
author?: string;
|
||||
createdAt?: number;
|
||||
}
|
||||
|
||||
/** Client → server: register a custom modifier descriptor on a room. */
|
||||
export interface CustomModifierRegisterPayload {
|
||||
roomCode: string;
|
||||
descriptor: CustomModifierDescriptorWire;
|
||||
}
|
||||
|
||||
/** Server → all clients in room: a new custom modifier was registered. */
|
||||
export interface CustomModifierRegisteredPayload {
|
||||
roomCode: string;
|
||||
descriptor: CustomModifierDescriptorWire;
|
||||
}
|
||||
|
||||
export interface ErrorPayload {
|
||||
code: string;
|
||||
message: string;
|
||||
|
|
@ -353,6 +384,7 @@ export type ServerMessage =
|
|||
"modifier-profile.consent-received",
|
||||
ModifierProfileConsentReceivedPayload
|
||||
>
|
||||
| MessageEnvelope<"custom-modifier.registered", CustomModifierRegisteredPayload>
|
||||
| MessageEnvelope<"error", ErrorPayload>;
|
||||
|
||||
export type ClientMessage =
|
||||
|
|
@ -363,4 +395,5 @@ export type ClientMessage =
|
|||
| MessageEnvelope<"room.setPresets", RoomSetPresetsPayload>
|
||||
| MessageEnvelope<"modifier-profile.update", ModifierProfileUpdatePayload>
|
||||
| MessageEnvelope<"modifier-profile.propose", ModifierProfileProposePayload>
|
||||
| MessageEnvelope<"modifier-profile.consent", ModifierProfileConsentPayload>;
|
||||
| MessageEnvelope<"modifier-profile.consent", ModifierProfileConsentPayload>
|
||||
| MessageEnvelope<"custom-modifier.register", CustomModifierRegisterPayload>;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue