@helixdev/helix-sdk 0.1.1-staging.8 → 0.1.1-staging.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { HelixWorldContext, HelixUser } from './protocol';
1
+ import { HelixWorldContext, HelixUser, DebugLogEntry } from './protocol';
2
2
  import { HelixMultiplayer } from './multiplayer';
3
- export type { HelixWorldContext, HelixSession, HelixUser } from './protocol';
3
+ export type { HelixWorldContext, HelixSession, HelixUser, DebugLogEntry, DebugLogLevel } from './protocol';
4
4
  export type { HelixRoom, JoinRoomOptions, ReplicaInput } from './multiplayer';
5
5
  export * from './multiplayer-contract';
6
6
  export type HelixInitResult = {
@@ -15,9 +15,19 @@ declare class HelixSdk {
15
15
  private world;
16
16
  private initialized;
17
17
  private listeners;
18
+ private debugEnabled;
19
+ private debugRing;
20
+ private debugListeners;
21
+ private consoleHooked;
18
22
  private pendingLogins;
19
23
  init(): Promise<HelixInitResult>;
20
24
  readonly multiplayer: HelixMultiplayer;
25
+ readonly debug: {
26
+ enabled: () => boolean;
27
+ log: (...args: unknown[]) => void;
28
+ onLog: (cb: (entry: DebugLogEntry) => void) => (() => void);
29
+ recent: () => DebugLogEntry[];
30
+ };
21
31
  readonly auth: {
22
32
  getUser: () => Promise<HelixUser | null>;
23
33
  isAuthenticated: () => boolean;
@@ -30,6 +40,9 @@ declare class HelixSdk {
30
40
  private waitForInit;
31
41
  private onMessage;
32
42
  private setSession;
43
+ private enableDebug;
44
+ private hookConsole;
45
+ private captureLog;
33
46
  private post;
34
47
  }
35
48
  export declare const Helix: HelixSdk;
package/dist/index.js CHANGED
@@ -6,12 +6,17 @@ import { HelixMultiplayer } from './multiplayer';
6
6
  export * from './multiplayer-contract';
7
7
  const INIT_TIMEOUT_MS = 3000;
8
8
  const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
9
+ const DEBUG_LOG_MAX = 200; // bounded ring of recent log entries (overlay backfill + pre-init forward flush)
9
10
  class HelixSdk {
10
11
  shellOrigin = null;
11
12
  session = null;
12
13
  world = null;
13
14
  initialized = false;
14
15
  listeners = new Set();
16
+ debugEnabled = false;
17
+ debugRing = [];
18
+ debugListeners = new Set();
19
+ consoleHooked = false;
15
20
  pendingLogins = new Map();
16
21
  // Performs the shell handshake. Call once at world start, before any other
17
22
  // Helix API. Resolves with embedded=false if no shell answers (local dev).
@@ -33,6 +38,21 @@ class HelixSdk {
33
38
  getToken: () => this.session?.token ?? null,
34
39
  getWorldId: () => this.world?.id ?? null,
35
40
  });
41
+ // Debug surface. A world's own console lives in its cross-origin iframe — invisible to DevTools / an
42
+ // automation harness attached to the shell, and to teammates without that frame selected. debug.log feeds
43
+ // a bounded ring an in-world overlay can render (onLog/recent), and — when the shell enabled debug
44
+ // (?debug) — is mirrored to the shell frame's console via helix:log. enableDebug() also hooks console.*
45
+ // so ALL world output is captured, not just explicit debug.log calls. enabled() is a fn (the flag flips
46
+ // at init, after the field initializer runs).
47
+ debug = {
48
+ enabled: () => this.debugEnabled,
49
+ log: (...args) => this.captureLog('info', args),
50
+ onLog: (cb) => {
51
+ this.debugListeners.add(cb);
52
+ return () => this.debugListeners.delete(cb);
53
+ },
54
+ recent: () => this.debugRing.slice(),
55
+ };
36
56
  auth = {
37
57
  getUser: async () => {
38
58
  this.assertInitialized();
@@ -123,6 +143,8 @@ class HelixSdk {
123
143
  this.shellOrigin = event.origin;
124
144
  this.world = msg.world;
125
145
  this.setSession(msg.session);
146
+ if (msg.debug)
147
+ this.enableDebug();
126
148
  break;
127
149
  case 'helix:session':
128
150
  this.setSession(msg.session);
@@ -149,10 +171,68 @@ class HelixSdk {
149
171
  listener(session?.user ?? null);
150
172
  }
151
173
  }
174
+ enableDebug() {
175
+ if (this.debugEnabled)
176
+ return;
177
+ this.debugEnabled = true;
178
+ this.hookConsole();
179
+ // Flush anything buffered before the shell turned debug on (e.g. early lifecycle logs).
180
+ if (this.shellOrigin)
181
+ for (const entry of this.debugRing)
182
+ this.post({ type: 'helix:log', entry }, this.shellOrigin);
183
+ }
184
+ // Capture ALL console output once debug is on (three.js warnings, SDK errors, uncaught logs) — not just
185
+ // explicit debug.log calls. Pass-through preserves normal console behaviour in the iframe.
186
+ hookConsole() {
187
+ if (this.consoleHooked || typeof console === 'undefined')
188
+ return;
189
+ this.consoleHooked = true;
190
+ const levels = ['log', 'info', 'warn', 'error', 'debug'];
191
+ const c = console;
192
+ for (const level of levels) {
193
+ const orig = c[level]?.bind(console);
194
+ if (!orig)
195
+ continue;
196
+ c[level] = (...args) => {
197
+ this.captureLog(level, args);
198
+ orig(...args);
199
+ };
200
+ }
201
+ }
202
+ captureLog(level, args) {
203
+ const entry = { level, args: args.map(stringifyArg), t: Date.now() };
204
+ this.debugRing.push(entry);
205
+ if (this.debugRing.length > DEBUG_LOG_MAX)
206
+ this.debugRing.shift();
207
+ for (const cb of this.debugListeners) {
208
+ try {
209
+ cb(entry);
210
+ }
211
+ catch {
212
+ /* a listener must never break logging */
213
+ }
214
+ }
215
+ if (this.debugEnabled && this.shellOrigin)
216
+ this.post({ type: 'helix:log', entry }, this.shellOrigin);
217
+ }
152
218
  post(message, targetOrigin) {
153
219
  window.parent.postMessage(message, targetOrigin);
154
220
  }
155
221
  }
222
+ // Serialize a console arg to a string the shell can print: strings as-is, Errors to their stack, other
223
+ // objects to compact JSON (falling back to String() on cyclic/unserializable values).
224
+ function stringifyArg(a) {
225
+ if (typeof a === 'string')
226
+ return a;
227
+ if (a instanceof Error)
228
+ return a.stack ?? `${a.name}: ${a.message}`;
229
+ try {
230
+ return typeof a === 'object' && a !== null ? JSON.stringify(a) : String(a);
231
+ }
232
+ catch {
233
+ return String(a);
234
+ }
235
+ }
156
236
  // Worlds import this singleton: `import { Helix } from '@hypersoniclabs/helix-sdk'`.
157
237
  export const Helix = new HelixSdk();
158
238
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -16,6 +16,15 @@ export interface RoomCredentialClaims {
16
16
  scopes: string[];
17
17
  /** Manifest maxPlayers (authoritative) → the room's maxClients cap, so it auto-locks + spills when full. */
18
18
  maxPlayers: number;
19
+ /** Monotonic per-world build counter (1, 2, 3 …) of the active build — telemetry + config cache key. */
20
+ buildNumber: number;
21
+ /**
22
+ * Signed URL of this build's `multiplayer.json` — the validated declarative config baked into the
23
+ * published bundle. Derived server-side from the build, NEVER client-supplied, so the room fetches its
24
+ * config from an authentic, immutable location and caches the parsed result by buildId. The room
25
+ * tolerates its absence (older credentials / a build with no declared config) by running the fixed schema.
26
+ */
27
+ configUrl: string;
19
28
  /** Always ROOM_CREDENTIAL_AUDIENCE. */
20
29
  aud: typeof ROOM_CREDENTIAL_AUDIENCE;
21
30
  iss: string;
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Wire format version. Bump on any incompatible change to PlayerReplicaState, the message table, or
3
- * the credential claims, so the room/SDK/driver can detect a mismatch instead of misreading bytes.
2
+ * Wire format version. Bump on any incompatible change to PlayerReplicaState, the message table, the
3
+ * credential claims, or the realized declared-state shape (roomVars / per-player vars / entities), so the
4
+ * room/SDK/driver can detect a mismatch instead of misreading bytes.
4
5
  */
5
- export declare const CONTRACT_VERSION: 2;
6
+ export declare const CONTRACT_VERSION: 11;
6
7
  export * from './state';
7
8
  export * from './room';
8
9
  export * from './messages';
@@ -14,10 +14,11 @@
14
14
  // per-player vars, and entities are DECLARED per-world — that's how an agent syncs custom game state
15
15
  // without writing server code. The generic 'action' message (messages.ts) is the channel for it.
16
16
  /**
17
- * Wire format version. Bump on any incompatible change to PlayerReplicaState, the message table, or
18
- * the credential claims, so the room/SDK/driver can detect a mismatch instead of misreading bytes.
17
+ * Wire format version. Bump on any incompatible change to PlayerReplicaState, the message table, the
18
+ * credential claims, or the realized declared-state shape (roomVars / per-player vars / entities), so the
19
+ * room/SDK/driver can detect a mismatch instead of misreading bytes.
19
20
  */
20
- export const CONTRACT_VERSION = 2;
21
+ export const CONTRACT_VERSION = 11; // v11: EntityStateBatch — many owner-entity uploads in one message (Tier 2 Phase 4.10, beats the per-connection message-count cap)
21
22
  export * from './state';
22
23
  export * from './room';
23
24
  export * from './messages';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/multiplayer-contract/index.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,EAAE;AACF,iGAAiG;AACjG,0FAA0F;AAC1F,oEAAoE;AACpE,wFAAwF;AACxF,8EAA8E;AAC9E,EAAE;AACF,mGAAmG;AACnG,+FAA+F;AAC/F,gDAAgD;AAChD,EAAE;AACF,uGAAuG;AACvG,qGAAqG;AACrG,iGAAiG;AAEjG;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAU,CAAC;AAE3C,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/multiplayer-contract/index.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,EAAE;AACF,iGAAiG;AACjG,0FAA0F;AAC1F,oEAAoE;AACpE,wFAAwF;AACxF,8EAA8E;AAC9E,EAAE;AACF,mGAAmG;AACnG,+FAA+F;AAC/F,gDAAgD;AAChD,EAAE;AACF,uGAAuG;AACvG,qGAAqG;AACrG,iGAAiG;AAEjG;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAW,CAAC,CAAC,mIAAmI;AAEhL,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC"}
@@ -7,8 +7,25 @@ export declare const ClientMessageType: {
7
7
  readonly Ability: "ability";
8
8
  /** A world-authored declared action (the generic extensibility channel — see ActionMessage). */
9
9
  readonly Action: "action";
10
+ /** An owner-authoritative entity's client-simulated state for this tick (Tier 2 Phase 4.6c — see EntityStateMessage). */
11
+ readonly EntityState: "entityState";
12
+ /**
13
+ * A BATCH of owner-authoritative entity uploads for this tick — ALL the entities this connection hosts in one
14
+ * message (Tier 2 Phase 4.10 — see EntityStateBatchMessage). The per-connection rate cap counts MESSAGES, so a
15
+ * host of N entities must batch (one message/tick) instead of N single EntityState messages, or it starves its
16
+ * own upload budget. EntityScene uses this; the single EntityState stays for one-off uploads (e.g. a flag).
17
+ */
18
+ readonly EntityStateBatch: "entityStateBatch";
19
+ /** Clock-sync probe: the client's local timestamp, which the room echoes in a Pong (RTT + offset). */
20
+ readonly Ping: "ping";
10
21
  };
11
22
  export type ClientMessageType = (typeof ClientMessageType)[keyof typeof ClientMessageType];
23
+ /** Server → client messages (the room sends these; SDK exposes them via room.onMessage(type, cb)). */
24
+ export declare const ServerMessageType: {
25
+ /** Reply to a Ping: the client's echoed timestamp + the server's current time, for offset estimation. */
26
+ readonly Pong: "pong";
27
+ };
28
+ export type ServerMessageType = (typeof ServerMessageType)[keyof typeof ServerMessageType];
12
29
  /** Per-tick state upload. Units match PlayerReplicaState (position m; all angles degrees, see *Deg). */
13
30
  export interface StateMessage {
14
31
  /** Monotonic per-client sequence; the room echoes the last applied seq for client reconciliation. */
@@ -23,6 +40,58 @@ export interface StateMessage {
23
40
  aimYawDeg: number;
24
41
  aimPitchDeg: number;
25
42
  }
43
+ /**
44
+ * An owner-authoritative entity's client-simulated state for this tick (Tier 2 Phase 4.6c, spec §9/§10.4). The
45
+ * client that the server marked as the entity's `controller` (see EntityState.controller) simulates the entity
46
+ * and uploads its transform + declared vars; the room sanity-validates (it owns this entity? movement within
47
+ * its declared `maxSpeed`? vars within their declared bounds?) and relays. Cross-player consequences still go
48
+ * through server-validated rules/actions — never this channel (the publish-time firewall enforces that).
49
+ */
50
+ export interface EntityStateMessage {
51
+ /** The entity id this upload is for (the state.entities map key). Rejected unless the sender is its controller. */
52
+ entity: string;
53
+ /** Monotonic per-entity sequence; the room ignores a stale/out-of-order frame. */
54
+ seq: number;
55
+ /** The entity's world position, METERS — gated against the kind's declared maxSpeed. */
56
+ position: Vec3;
57
+ /** Declared per-kind vars to set (same vocabulary as roomVars); each is clamped to its declared type/bounds on ingest. */
58
+ vars?: Record<string, RoomVarValue>;
59
+ /**
60
+ * The authority EPOCH the client is simulating under (Tier 2 Phase 4.5.12, spec §9). Mirror the entity's synced
61
+ * `authorityEpoch`; the room bumps it on every controller handoff (host migration AND voluntary ownership
62
+ * transfer) and rejects an upload whose epoch ≠ the entity's current one — fencing a revoked/stale authority
63
+ * era even from a still-connected prior owner whose sessionId would otherwise pass. Omit only for the very
64
+ * first frame before the field is observed; the room tolerates absence (sessionId ownership still gates).
65
+ */
66
+ epoch?: number;
67
+ }
68
+ /**
69
+ * One entity's slot in an EntityStateBatchMessage. Compact, short keys (sent verbatim in msgpack), with position
70
+ * as a 3-tuple instead of a {x,y,z} object — the same authoritative meaning as EntityStateMessage's per-entity
71
+ * fields. `v`/`ep` are omitted when unchanged/unobserved to keep the batch small.
72
+ */
73
+ export interface EntityStateBatchEntry {
74
+ /** Entity id (the state.entities map key). Rejected unless the sender is its controller. */
75
+ e: string;
76
+ /** World position as [x, y, z], METERS — gated against the kind's declared maxSpeed. */
77
+ p: [number, number, number];
78
+ /** Declared per-kind vars to set (clamped to declared type/bounds on ingest). Omit when unchanged this tick. */
79
+ v?: Record<string, RoomVarValue>;
80
+ /** Authority epoch this entity is simulated under (mirror its synced authorityEpoch). Omit before observed. */
81
+ ep?: number;
82
+ }
83
+ /**
84
+ * A batch of owner-authoritative entity uploads (Tier 2 Phase 4.10, spec §10.4) — every entity this connection
85
+ * hosts, in ONE message. The per-connection rate cap (MESSAGE_RATE.entityStateHz) counts messages, so batching
86
+ * lets a host refresh many entities per tick within one token instead of spending one token per entity. The room
87
+ * applies each entry exactly like a single EntityStateMessage (ownership + epoch + maxSpeed gate + var clamp).
88
+ */
89
+ export interface EntityStateBatchMessage {
90
+ /** Monotonic per-connection batch sequence; the room ignores a stale/out-of-order batch per entity (gate seq). */
91
+ seq: number;
92
+ /** One entry per hosted entity changed this tick (bounded by the room's per-owner entity cap). */
93
+ states: EntityStateBatchEntry[];
94
+ }
26
95
  /** Discrete ability activation intent. */
27
96
  export interface AbilityMessage {
28
97
  /** Ability id (must be one the world's build declares). */
@@ -42,11 +111,45 @@ export interface ActionMessage {
42
111
  /** Primitive/Vec3 args, validated against the declared action's schema by the room. */
43
112
  args?: Record<string, RoomVarValue>;
44
113
  }
45
- /** Maps each message type to its payload, so senders/handlers stay in lockstep. */
114
+ /**
115
+ * Clock-sync probe. The client sends its local send time; the room replies (Pong) with this value echoed
116
+ * plus its own clock, so the client estimates RTT (recv − send) and offset (serverTimeMs − send − RTT/2).
117
+ */
118
+ export interface PingMessage {
119
+ /** The client's local clock (epoch ms) at send — echoed verbatim in the Pong. */
120
+ clientTimeMs: number;
121
+ }
122
+ /** Reply to a Ping — the echoed client time + the server's clock at reply, for offset/RTT estimation. */
123
+ export interface PongMessage {
124
+ clientTimeMs: number;
125
+ serverTimeMs: number;
126
+ }
127
+ /**
128
+ * A server→client broadcast event (Tier 2 Phase 2.5, spec §10.3). A world declares named events with a
129
+ * payload schema; a `broadcast` rule-effect sends one on the event's NAME (not a fixed ServerMessageType),
130
+ * received via `room.onMessage(name, cb)`. Payload values are the declared fields; a `ref` field serializes
131
+ * to its `playerKey` string on the wire (so `who:"self"` and `who:{aggregate argmax}` are the same type).
132
+ */
133
+ export type BroadcastPayload = Record<string, RoomVarValue>;
134
+ /**
135
+ * Wire message-type names a world's declared event/action names MUST NOT shadow — the fixed client→server
136
+ * and server→client channels. The manifest validator rejects a declared name in this set so a `broadcast`
137
+ * can't collide with `pong` (or confuse the fixed channels). Inlined in @hypersoniclabs/helix-manifest with
138
+ * a cross-ref (the manifest takes no SDK dependency); keep the two in sync.
139
+ */
140
+ export declare const RESERVED_MESSAGE_TYPES: readonly ["state", "ability", "action", "entityState", "entityStateBatch", "ping", "pong"];
141
+ /** Maps each client→server message type to its payload, so senders/handlers stay in lockstep. */
46
142
  export interface ClientMessagePayloads {
47
143
  [ClientMessageType.State]: StateMessage;
48
144
  [ClientMessageType.Ability]: AbilityMessage;
49
145
  [ClientMessageType.Action]: ActionMessage;
146
+ [ClientMessageType.EntityState]: EntityStateMessage;
147
+ [ClientMessageType.EntityStateBatch]: EntityStateBatchMessage;
148
+ [ClientMessageType.Ping]: PingMessage;
149
+ }
150
+ /** Maps each server→client message type to its payload. */
151
+ export interface ServerMessagePayloads {
152
+ [ServerMessageType.Pong]: PongMessage;
50
153
  }
51
154
  /**
52
155
  * Per-connection rate ceilings (messages/second) the room enforces; over-rate clients are dropped.
@@ -56,6 +159,13 @@ export declare const MESSAGE_RATE: {
56
159
  readonly stateHz: 10;
57
160
  readonly abilityHz: 20;
58
161
  readonly actionHz: 20;
162
+ readonly entityStateHz: 10;
163
+ readonly pingHz: 4;
59
164
  };
60
- /** Hard payload bound the room rejects above (defense against oversized frames). */
61
- export declare const MAX_MESSAGE_BYTES = 1024;
165
+ /**
166
+ * Hard payload bound the room rejects above (defense against oversized frames), measured on the JSON form (a
167
+ * cheap proxy; the msgpack wire is smaller). Sized to admit a full EntityStateBatch — up to MAX_ENTITIES_PER_OWNER
168
+ * entities in one message. MUST stay below the Colyseus transport `maxPayload` (set in helix-colyseus-server
169
+ * src/index.ts) in WIRE terms, so our clean reject fires before the transport drops the frame / closes the socket.
170
+ */
171
+ export declare const MAX_MESSAGE_BYTES = 8192;
@@ -12,7 +12,30 @@ export const ClientMessageType = {
12
12
  Ability: 'ability',
13
13
  /** A world-authored declared action (the generic extensibility channel — see ActionMessage). */
14
14
  Action: 'action',
15
+ /** An owner-authoritative entity's client-simulated state for this tick (Tier 2 Phase 4.6c — see EntityStateMessage). */
16
+ EntityState: 'entityState',
17
+ /**
18
+ * A BATCH of owner-authoritative entity uploads for this tick — ALL the entities this connection hosts in one
19
+ * message (Tier 2 Phase 4.10 — see EntityStateBatchMessage). The per-connection rate cap counts MESSAGES, so a
20
+ * host of N entities must batch (one message/tick) instead of N single EntityState messages, or it starves its
21
+ * own upload budget. EntityScene uses this; the single EntityState stays for one-off uploads (e.g. a flag).
22
+ */
23
+ EntityStateBatch: 'entityStateBatch',
24
+ /** Clock-sync probe: the client's local timestamp, which the room echoes in a Pong (RTT + offset). */
25
+ Ping: 'ping',
15
26
  };
27
+ /** Server → client messages (the room sends these; SDK exposes them via room.onMessage(type, cb)). */
28
+ export const ServerMessageType = {
29
+ /** Reply to a Ping: the client's echoed timestamp + the server's current time, for offset estimation. */
30
+ Pong: 'pong',
31
+ };
32
+ /**
33
+ * Wire message-type names a world's declared event/action names MUST NOT shadow — the fixed client→server
34
+ * and server→client channels. The manifest validator rejects a declared name in this set so a `broadcast`
35
+ * can't collide with `pong` (or confuse the fixed channels). Inlined in @hypersoniclabs/helix-manifest with
36
+ * a cross-ref (the manifest takes no SDK dependency); keep the two in sync.
37
+ */
38
+ export const RESERVED_MESSAGE_TYPES = ['state', 'ability', 'action', 'entityState', 'entityStateBatch', 'ping', 'pong'];
16
39
  /**
17
40
  * Per-connection rate ceilings (messages/second) the room enforces; over-rate clients are dropped.
18
41
  * State is the throttled input channel (≤~10Hz, plan D3); ability is bursty but bounded.
@@ -21,7 +44,14 @@ export const MESSAGE_RATE = {
21
44
  stateHz: 10,
22
45
  abilityHz: 20,
23
46
  actionHz: 20,
47
+ entityStateHz: 10, // owner-entity upload — same cadence as the player state channel (one bucket per connection)
48
+ pingHz: 4, // clock-sync probe — a few per second is ample to track offset/RTT
24
49
  };
25
- /** Hard payload bound the room rejects above (defense against oversized frames). */
26
- export const MAX_MESSAGE_BYTES = 1024;
50
+ /**
51
+ * Hard payload bound the room rejects above (defense against oversized frames), measured on the JSON form (a
52
+ * cheap proxy; the msgpack wire is smaller). Sized to admit a full EntityStateBatch — up to MAX_ENTITIES_PER_OWNER
53
+ * entities in one message. MUST stay below the Colyseus transport `maxPayload` (set in helix-colyseus-server
54
+ * src/index.ts) in WIRE terms, so our clean reject fires before the transport drops the frame / closes the socket.
55
+ */
56
+ export const MAX_MESSAGE_BYTES = 8192;
27
57
  //# sourceMappingURL=messages.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"messages.js","sourceRoot":"","sources":["../../src/multiplayer-contract/messages.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,mGAAmG;AACnG,kGAAkG;AAClG,EAAE;AACF,kGAAkG;AAClG,qGAAqG;AACrG,0FAA0F;AAK1F,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,4FAA4F;IAC5F,KAAK,EAAE,OAAO;IACd,4FAA4F;IAC5F,OAAO,EAAE,SAAS;IAClB,gGAAgG;IAChG,MAAM,EAAE,QAAQ;CACR,CAAC;AA8CX;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,OAAO,EAAE,EAAE;IACX,SAAS,EAAE,EAAE;IACb,QAAQ,EAAE,EAAE;CACJ,CAAC;AAEX,oFAAoF;AACpF,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC"}
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../../src/multiplayer-contract/messages.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,mGAAmG;AACnG,kGAAkG;AAClG,EAAE;AACF,kGAAkG;AAClG,qGAAqG;AACrG,0FAA0F;AAK1F,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,4FAA4F;IAC5F,KAAK,EAAE,OAAO;IACd,4FAA4F;IAC5F,OAAO,EAAE,SAAS;IAClB,gGAAgG;IAChG,MAAM,EAAE,QAAQ;IAChB,yHAAyH;IACzH,WAAW,EAAE,aAAa;IAC1B;;;;;OAKG;IACH,gBAAgB,EAAE,kBAAkB;IACpC,sGAAsG;IACtG,IAAI,EAAE,MAAM;CACJ,CAAC;AAGX,sGAAsG;AACtG,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,yGAAyG;IACzG,IAAI,EAAE,MAAM;CACJ,CAAC;AAqHX;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,CAAU,CAAC;AAiBjI;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,OAAO,EAAE,EAAE;IACX,SAAS,EAAE,EAAE;IACb,QAAQ,EAAE,EAAE;IACZ,aAAa,EAAE,EAAE,EAAE,6FAA6F;IAChH,MAAM,EAAE,CAAC,EAAE,mEAAmE;CACtE,CAAC;AAEX;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC"}
@@ -1,13 +1,32 @@
1
1
  import type { Vec3 } from './state';
2
2
  import type { PlayerIdentity, PlayerReplicaState } from './state';
3
- /** The value vocabulary an agent may declare for custom state (Tier 1). Tier 2 widens this. */
4
- export type RoomVarValue = number | string | boolean | Vec3;
3
+ /**
4
+ * The runtime value vocabulary for declared custom state. A `ref`-typed var's value is the **id** of a live
5
+ * member (a player's `playerKey` or an entity id) as a string, or `null` when unset/dangling — hence `null`
6
+ * is included. The DECLARATION vocabulary (`VarType`: number/string/boolean/vec3/ref + bounds) lives in
7
+ * `@hypersoniclabs/helix-manifest` (authored config, validated at publish); this is the wire value side.
8
+ */
9
+ export type RoomVarValue = number | string | boolean | Vec3 | null | RoomVarCollection;
10
+ /**
11
+ * Per-player/room COLLECTION values (Tier 2 Phase 4.5.13, spec §5). A `list` var reads as an ordered array of
12
+ * its declared scalar element type (realized server-side as a colyseus ArraySchema); a `counterMap` var reads as
13
+ * a string→number record over its declared key enum (a MapSchema<number>). Mutated only via the dedicated
14
+ * append/clear/addCount effects + read via listLength/listAt/count ops — never a normal scalar expr/payload/arg.
15
+ */
16
+ export type RoomVarCollection = number[] | string[] | boolean[] | Record<string, number>;
5
17
  /** A declared bag of custom state — names + types are declared per-world; used for both room-level and per-player vars. */
6
18
  export type RoomVars = Record<string, RoomVarValue>;
7
19
  /** Full authoritative per-player state: the FIXED character contract + identity + DECLARED game vars. */
8
20
  export interface PlayerState extends PlayerIdentity, PlayerReplicaState {
9
21
  /** Declared per-player game state (e.g. team, health, score). Same vocabulary as roomVars; declared per-world. */
10
22
  vars: RoomVars;
23
+ /**
24
+ * Presence (Tier 2 Phase 3, spec §8): `false` while this seat sits in the reconnection grace window after an
25
+ * unexpected drop — the seat (and its refs) persists, but the server excludes it from reductions. The seat is
26
+ * removed (an `onRemove('players')`) only at grace expiry; until then the engine should render the replica idle.
27
+ * This layers on Colyseus's own reconnection — it annotates the seat, it does not drive the reconnect handshake.
28
+ */
29
+ connected: boolean;
11
30
  }
12
31
  /**
13
32
  * RESERVED (Tier 2): a server-spawned object (pickup, projectile, NPC). Empty in v1 — the shape is
@@ -19,6 +38,19 @@ export interface EntityState {
19
38
  /** Author-defined kind (e.g. 'flag', 'pickup'). */
20
39
  kind: string;
21
40
  position?: Vec3;
41
+ /**
42
+ * Who simulates this entity (Tier 2 Phase 4.6c, spec §9). '' = server-authoritative (deterministic kinematics).
43
+ * A player id (a state.players key) = an owner-authoritative entity that client simulates + uploads (the others
44
+ * render it); a client reads this to know which entities it should be uploading via uploadEntity.
45
+ */
46
+ controller: string;
47
+ /**
48
+ * Monotonic authority epoch (Tier 2 Phase 4.5.12, spec §9), bumped on every controller handoff — host
49
+ * migration AND voluntary ownership transfer. A controller client mirrors this into each EntityStateMessage;
50
+ * the room rejects an upload whose epoch ≠ the current one, fencing a stale authority era (a still-connected
51
+ * prior owner's in-flight frames after ownership moved). 0 at spawn; clients only read it.
52
+ */
53
+ authorityEpoch: number;
22
54
  /** Declared custom vars (same vocabulary as roomVars). */
23
55
  vars: RoomVars;
24
56
  }
@@ -30,4 +62,31 @@ export interface RoomState {
30
62
  roomVars: RoomVars;
31
63
  /** RESERVED (Tier 2): server-spawned entities. Empty in v1. */
32
64
  entities: Record<string, EntityState>;
65
+ /** Authoritative server clock: a monotonic counter advanced once per fixed-rate sim tick. */
66
+ serverTick: number;
67
+ /**
68
+ * Authoritative server wall-clock (epoch ms) sampled at the last tick. Clients align to it (offset via the
69
+ * ping/pong echo, see messages.ts) and interpolate between patches, so timer/phase deadlines expressed
70
+ * against server time agree across clients without a per-tick countdown var.
71
+ */
72
+ serverTimeMs: number;
73
+ /**
74
+ * The current phase of the world's room-scoped state machine (Tier 2 Phase 3, spec §8), or `''` when the
75
+ * world declares no `states`. Authored phase names; changed only by the server's `transitionTo`. A
76
+ * late-joiner reads it off synced state to know whether to spawn in or spectate (the join policy).
77
+ */
78
+ phase: string;
79
+ /**
80
+ * The `serverTick` at which the room entered its current `phase`. With `serverTick`/`serverTimeMs` a client
81
+ * computes time-in-phase locally (e.g. a round countdown) without a per-tick countdown var.
82
+ */
83
+ phaseStartTick: number;
84
+ /**
85
+ * Active timers (Tier 2 Phase 3, spec §8) → absolute deadline as `serverTimeMs` (epoch ms). The map key is
86
+ * the timer name for a room-scoped timer, or `"<timer>|<playerKey>"` for a per-player **keyed** timer (e.g.
87
+ * read your own cooldown at `"cooldown|" + room.sessionId`). A client computes the seconds remaining as
88
+ * `(deadline − estimatedServerTimeMs) / 1000` using its ping/pong offset, so a countdown interpolates locally
89
+ * without a per-tick countdown var. An entry is removed when the timer fires or is cancelled.
90
+ */
91
+ timerDeadlines: Record<string, number>;
33
92
  }
@@ -11,7 +11,7 @@ export declare const UNITS: {
11
11
  };
12
12
  /** Stable per-player identity (set at join from the room credential; not part of the per-tick churn). */
13
13
  export interface PlayerIdentity {
14
- /** Player/user id (the credential `sub`). */
14
+ /** Opaque per-connection room playerKey (= the state.players map key). NOT the credential `sub` — that stays server-side (§4). */
15
15
  id: string;
16
16
  /** Sanitized display name for nameplates (sanitized before it enters room state — see plan H4). */
17
17
  displayName: string;
@@ -1,4 +1,4 @@
1
- import { type StateMessage, type RoomState, type PlayerState, type EntityState, type RoomVarValue } from './multiplayer-contract';
1
+ import { type StateMessage, type RoomState, type PlayerState, type EntityState, type RoomVarValue, type Vec3 } from './multiplayer-contract';
2
2
  export type ReplicaInput = Omit<StateMessage, 'seq'>;
3
3
  /** Options for joinRoom — only needed where the API base can't be derived from the token (local dev/tests). */
4
4
  export interface JoinRoomOptions {
@@ -26,6 +26,27 @@ export interface HelixRoom {
26
26
  sendAbility(ability: string, active: boolean): void;
27
27
  /** A world-authored declared action (the generic extensibility channel). */
28
28
  sendAction(name: string, args?: Record<string, RoomVarValue>): void;
29
+ /**
30
+ * Upload an owner-authoritative entity's client-simulated state (Tier 2 Phase 4.6c). Call only for an entity
31
+ * whose `controller` is this client (state.entities[id].controller === sessionId); the room rejects others.
32
+ * Sent immediately, per-entity seq-tagged; the room gates the position against the kind's maxSpeed + clamps vars.
33
+ */
34
+ uploadEntity(entityId: string, input: {
35
+ position: Vec3;
36
+ vars?: Record<string, RoomVarValue>;
37
+ }): void;
38
+ /**
39
+ * Upload MANY owner-authoritative entities in ONE message (Tier 2 Phase 4.10). Prefer this over per-entity
40
+ * uploadEntity when hosting multiple entities: the per-connection rate cap counts messages, so N single uploads
41
+ * spend N tokens/tick and starve, while one batch spends one. Position is sent as a compact [x,y,z] tuple; each
42
+ * entry is gated/clamped server-side exactly like uploadEntity. Pass only entities this client controls.
43
+ */
44
+ uploadEntities(entities: ReadonlyArray<{
45
+ id: string;
46
+ position: Vec3;
47
+ vars?: Record<string, RoomVarValue>;
48
+ epoch?: number;
49
+ }>): void;
29
50
  /** Sent-but-unacknowledged states — the reconciliation tail the engine NetworkDriver replays (D4). */
30
51
  pendingInputs(): readonly StateMessage[];
31
52
  /** Prune the reconciliation buffer once the server confirms it processed up to `seq` (D4). */
@@ -50,10 +71,12 @@ export declare class HelixMultiplayer {
50
71
  private room;
51
72
  private callbacks;
52
73
  private seq;
74
+ private entitySeq;
75
+ private entityBatchSeq;
53
76
  private pendingInput;
54
77
  private flushTimer;
55
78
  private inputBuffer;
56
- private onUnload;
79
+ private worldId;
57
80
  constructor(host: MultiplayerHost);
58
81
  configure(options: {
59
82
  apiBaseUrl: string;
@@ -63,7 +86,11 @@ export declare class HelixMultiplayer {
63
86
  private makeHandle;
64
87
  private startFlush;
65
88
  private flush;
66
- private attachUnload;
89
+ private persistReconnect;
90
+ private readReconnect;
91
+ private reconnectWsUrl;
92
+ private clearReconnect;
93
+ private tryReconnect;
67
94
  private leave;
68
95
  private teardown;
69
96
  }