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 {
|
import type {
|
||||||
ClientMessage,
|
ClientMessage,
|
||||||
|
CustomModifierRegisteredPayload,
|
||||||
ErrorPayload,
|
ErrorPayload,
|
||||||
GameDeltaPayload,
|
GameDeltaPayload,
|
||||||
GameEndPayload,
|
GameEndPayload,
|
||||||
|
|
@ -46,6 +47,7 @@ export type GameClientEvent =
|
||||||
| { type: "modifier-profile.consent-received"; payload: ModifierProfileConsentReceivedPayload }
|
| { type: "modifier-profile.consent-received"; payload: ModifierProfileConsentReceivedPayload }
|
||||||
| { type: "modifier-profile.queued"; payload: ModifierProfileQueuedPayload }
|
| { type: "modifier-profile.queued"; payload: ModifierProfileQueuedPayload }
|
||||||
| { type: "modifier-profile.updated"; payload: ModifierProfileUpdatedPayload }
|
| { type: "modifier-profile.updated"; payload: ModifierProfileUpdatedPayload }
|
||||||
|
| { type: "custom-modifier.registered"; payload: CustomModifierRegisteredPayload }
|
||||||
| { type: "error"; payload: ErrorPayload }
|
| { type: "error"; payload: ErrorPayload }
|
||||||
| { type: "connected" }
|
| { type: "connected" }
|
||||||
| { type: "disconnected"; willReconnect: boolean };
|
| { type: "disconnected"; willReconnect: boolean };
|
||||||
|
|
@ -280,6 +282,30 @@ export class GameClient {
|
||||||
this.send({ type: "room.setPresets", payload });
|
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)
|
// Accessors (primarily for tests & reconnect logic)
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
@ -458,6 +484,9 @@ export class GameClient {
|
||||||
case "modifier-profile.updated":
|
case "modifier-profile.updated":
|
||||||
this.emit({ type, payload: payload as ModifierProfileUpdatedPayload });
|
this.emit({ type, payload: payload as ModifierProfileUpdatedPayload });
|
||||||
return;
|
return;
|
||||||
|
case "custom-modifier.registered":
|
||||||
|
this.emit({ type, payload: payload as CustomModifierRegisteredPayload });
|
||||||
|
return;
|
||||||
case "error":
|
case "error":
|
||||||
this.emit({ type, payload: payload as ErrorPayload });
|
this.emit({ type, payload: payload as ErrorPayload });
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,23 @@ export class PredictionManager {
|
||||||
}
|
}
|
||||||
this.onStateChange(this.baseEngine);
|
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) => {
|
this.client.on("error", (e) => {
|
||||||
// Fatal errors tear down the session; the app restarts from a fresh
|
// Fatal errors tear down the session; the app restarts from a fresh
|
||||||
// `game.state`. Non-fatal errors (ILLEGAL_MOVE / NOT_YOUR_TURN / …)
|
// `game.state`. Non-fatal errors (ILLEGAL_MOVE / NOT_YOUR_TURN / …)
|
||||||
|
|
|
||||||
|
|
@ -261,6 +261,37 @@ export interface ModifierProfileConsentReceivedPayload {
|
||||||
roomCode: string;
|
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 {
|
export interface ErrorPayload {
|
||||||
code: string;
|
code: string;
|
||||||
message: string;
|
message: string;
|
||||||
|
|
@ -353,6 +384,7 @@ export type ServerMessage =
|
||||||
"modifier-profile.consent-received",
|
"modifier-profile.consent-received",
|
||||||
ModifierProfileConsentReceivedPayload
|
ModifierProfileConsentReceivedPayload
|
||||||
>
|
>
|
||||||
|
| MessageEnvelope<"custom-modifier.registered", CustomModifierRegisteredPayload>
|
||||||
| MessageEnvelope<"error", ErrorPayload>;
|
| MessageEnvelope<"error", ErrorPayload>;
|
||||||
|
|
||||||
export type ClientMessage =
|
export type ClientMessage =
|
||||||
|
|
@ -363,4 +395,5 @@ export type ClientMessage =
|
||||||
| MessageEnvelope<"room.setPresets", RoomSetPresetsPayload>
|
| MessageEnvelope<"room.setPresets", RoomSetPresetsPayload>
|
||||||
| MessageEnvelope<"modifier-profile.update", ModifierProfileUpdatePayload>
|
| MessageEnvelope<"modifier-profile.update", ModifierProfileUpdatePayload>
|
||||||
| MessageEnvelope<"modifier-profile.propose", ModifierProfileProposePayload>
|
| 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