@helixdev/helix-sdk 0.1.1-staging.10

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.
@@ -0,0 +1,59 @@
1
+ // Client → server messages. The SDK send path emits these; the room dispatches them through a
2
+ // table keyed by ClientMessageType and validates every payload before mutating authoritative state
3
+ // (the room owns runtime validation — this module is types + limits only, no validator, no deps).
4
+ //
5
+ // Tier 1 (lean authoritative, no server physics): the client predicts locally and sends its state
6
+ // each tick (seq-tagged so the room can echo for reconciliation); the room validates bounds/rate and
7
+ // writes the authoritative copy. Discrete intents (ability on/off) are their own message.
8
+ export const ClientMessageType = {
9
+ /** The player's predicted kinematic state for this tick (seq-tagged for reconciliation). */
10
+ State: 'state',
11
+ /** Activate or deactivate an ability on this player (the built-in, engine-level intent). */
12
+ Ability: 'ability',
13
+ /** A world-authored declared action (the generic extensibility channel — see ActionMessage). */
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',
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'];
39
+ /**
40
+ * Per-connection rate ceilings (messages/second) the room enforces; over-rate clients are dropped.
41
+ * State is the throttled input channel (≤~10Hz, plan D3); ability is bursty but bounded.
42
+ */
43
+ export const MESSAGE_RATE = {
44
+ stateHz: 10,
45
+ abilityHz: 20,
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
49
+ };
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 (P1.B4) to admit a full EntityStateBatch — MAX_ENTITIES_PER_OWNER
53
+ * (128) entities with modest vars in one message (≈75 JSON bytes/entry) — so the byte cap and the per-owner entity
54
+ * cap don't disagree. MUST stay below the Colyseus transport `maxPayload` (16 KB, helix-colyseus-server src/index.ts)
55
+ * in WIRE terms, so the room's reject fires before the transport drops the frame / closes the socket. A var-heavy
56
+ * batch past this is a CLEAN DROP (the room's guard does not strike on an over-byte payload), never a kick.
57
+ */
58
+ export const MAX_MESSAGE_BYTES = 12288;
59
+ //# sourceMappingURL=messages.js.map
@@ -0,0 +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;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;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,CAAC"}
@@ -0,0 +1,92 @@
1
+ import type { Vec3 } from './state';
2
+ import type { PlayerIdentity, PlayerReplicaState } from './state';
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>;
17
+ /** A declared bag of custom state — names + types are declared per-world; used for both room-level and per-player vars. */
18
+ export type RoomVars = Record<string, RoomVarValue>;
19
+ /** Full authoritative per-player state: the FIXED character contract + identity + DECLARED game vars. */
20
+ export interface PlayerState extends PlayerIdentity, PlayerReplicaState {
21
+ /** Declared per-player game state (e.g. team, health, score). Same vocabulary as roomVars; declared per-world. */
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;
30
+ }
31
+ /**
32
+ * RESERVED (Tier 2): a server-spawned object (pickup, projectile, NPC). Empty in v1 — the shape is
33
+ * reserved so adding server-spawned entities later is additive. Tier 2 gives these author-defined
34
+ * behavior; v1 only freezes the slot in RoomState.
35
+ */
36
+ export interface EntityState {
37
+ id: string;
38
+ /** Author-defined kind (e.g. 'flag', 'pickup'). */
39
+ kind: string;
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;
54
+ /** Declared custom vars (same vocabulary as roomVars). */
55
+ vars: RoomVars;
56
+ }
57
+ /** The authoritative shared state. Always carries all three collections (entities empty until Tier 2). */
58
+ export interface RoomState {
59
+ /** Per-player authoritative state, keyed by player id. Realized as a colyseus MapSchema. */
60
+ players: Record<string, PlayerState>;
61
+ /** Room-level declared game state (scores, timers, objectives). Names/types declared per-world. */
62
+ roomVars: RoomVars;
63
+ /** RESERVED (Tier 2): server-spawned entities. Empty in v1. */
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>;
92
+ }
@@ -0,0 +1,17 @@
1
+ // The authoritative room state shape — the FRAMEWORK for what a world syncs. The room holds this as
2
+ // @colyseus/schema (the Record<string, T> maps below are realized as MapSchema); the SDK exposes it as
3
+ // room.state. Two layers, and the distinction is the whole answer to "how does an agent sync more?":
4
+ //
5
+ // • players — FIXED. Each player is the engine's character contract (PlayerReplicaState) + identity.
6
+ // Universal across every world; an agent never changes its fields.
7
+ // • roomVars / per-player vars / entities — DECLARED PER-WORLD. This is where an agent's game state
8
+ // lives (scores, timers, teams, objectives). The agent DECLARES names + types in the
9
+ // world's multiplayer config (loaded by buildId at onCreate); the one generic HelixRoom
10
+ // interprets the declaration — no agent-authored server code (Tier 1).
11
+ //
12
+ // Tier-1 vocabulary is intentionally bounded (primitives + Vec3). Rich/nested custom state and
13
+ // author-defined entity behavior are Tier 2 (a reflection-based dynamic schema + behavior DSL),
14
+ // deferred — the `entities` collection and this declared-bag shape are the reserved seams that keep
15
+ // Tier 2 additive rather than a rewrite.
16
+ export {};
17
+ //# sourceMappingURL=room.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room.js","sourceRoot":"","sources":["../../src/multiplayer-contract/room.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,uGAAuG;AACvG,qGAAqG;AACrG,EAAE;AACF,wGAAwG;AACxG,kFAAkF;AAClF,sGAAsG;AACtG,oGAAoG;AACpG,uGAAuG;AACvG,sFAAsF;AACtF,EAAE;AACF,+FAA+F;AAC/F,gGAAgG;AAChG,oGAAoG;AACpG,yCAAyC"}
@@ -0,0 +1,46 @@
1
+ export type Vec3 = {
2
+ x: number;
3
+ y: number;
4
+ z: number;
5
+ };
6
+ /** Pinned units. Linear in meters / meters-per-second; every angle is degrees (see the *Deg suffixes). */
7
+ export declare const UNITS: {
8
+ readonly position: "meters";
9
+ readonly velocity: "meters/second";
10
+ readonly angles: "degrees";
11
+ };
12
+ /** Stable per-player identity (set at join from the room credential; not part of the per-tick churn). */
13
+ export interface PlayerIdentity {
14
+ /** Opaque per-connection room playerKey (= the state.players map key). NOT the credential `sub` — that stays server-side (§4). */
15
+ id: string;
16
+ /** Sanitized display name for nameplates (sanitized before it enters room state — see plan H4). */
17
+ displayName: string;
18
+ }
19
+ /**
20
+ * The per-tick replicated kinematic + animation parameters for one player. The room holds the
21
+ * authoritative copy (a @colyseus/schema mirror of these fields); the NetworkDriver maps each field
22
+ * onto the named character blackboard param. FIRST DRAFT — the exact injected set is confirmed by
23
+ * spike S2 (engine headless-replica write-path); changing the field set bumps CONTRACT_VERSION.
24
+ */
25
+ export interface PlayerReplicaState {
26
+ /** World feet position, METERS. Interpolated by the remote replica. */
27
+ position: Vec3;
28
+ /** Body facing, DEGREES. The adapter converts to radians for body.setFacingYaw. */
29
+ facingYawDeg: number;
30
+ /** Horizontal planar speed, M/S. → blackboard 'speed' (blend-space input). */
31
+ speed: number;
32
+ /** Signed movement direction relative to facing, DEGREES (−180..180). → blackboard 'direction'. */
33
+ moveDirectionDeg: number;
34
+ /** Vertical velocity, M/S (+up). → blackboard 'verticalVelocity' (jump/fall states). */
35
+ verticalVelocity: number;
36
+ /** Ground contact. → blackboard 'isGrounded' (grounded/air transitions). */
37
+ grounded: boolean;
38
+ /** Crouch stance. → routes the locomotion anim graph to its crouched states. */
39
+ crouched: boolean;
40
+ /** Aim yaw, DEGREES (world). → blackboard 'cameraYaw' (aim-additive source). */
41
+ aimYawDeg: number;
42
+ /** Aim pitch, DEGREES. → blackboard 'cameraPitch' (aim-additive source). */
43
+ aimPitchDeg: number;
44
+ /** Ability ids currently active (e.g. ['locomotion','fly']). Drives remote ability activation. */
45
+ activeAbilities: string[];
46
+ }
@@ -0,0 +1,15 @@
1
+ // Replicated player state — the parameters the room broadcasts and the engine's NetworkDriver feeds
2
+ // into the character blackboard. We replicate PARAMETERS, never bone transforms: animation runs fully
3
+ // client-side off these values (see character-architecture §3).
4
+ //
5
+ // ALL ANGLES ARE DEGREES — every angular field is suffixed *Deg. One unit on the wire, so there is no
6
+ // radians/degrees ambiguity to forget. The engine mixes units internally (the body is radians, the
7
+ // animation params are degrees); the engine adapter owns the single deg↔rad conversion at the body
8
+ // boundary (body.setFacingYaw) — the contract itself stays all-degrees.
9
+ /** Pinned units. Linear in meters / meters-per-second; every angle is degrees (see the *Deg suffixes). */
10
+ export const UNITS = {
11
+ position: 'meters',
12
+ velocity: 'meters/second',
13
+ angles: 'degrees',
14
+ };
15
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../../src/multiplayer-contract/state.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,sGAAsG;AACtG,gEAAgE;AAChE,EAAE;AACF,sGAAsG;AACtG,mGAAmG;AACnG,mGAAmG;AACnG,wEAAwE;AAIxE,0GAA0G;AAC1G,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,QAAQ,EAAE,QAAQ;IAClB,QAAQ,EAAE,eAAe;IACzB,MAAM,EAAE,SAAS;CACT,CAAC"}
@@ -0,0 +1,97 @@
1
+ import { type StateMessage, type RoomState, type PlayerState, type EntityState, type RoomVarValue, type Vec3 } from './multiplayer-contract';
2
+ export type ReplicaInput = Omit<StateMessage, 'seq'>;
3
+ /** Options for joinRoom — only needed where the API base can't be derived from the token (local dev/tests). */
4
+ export interface JoinRoomOptions {
5
+ /** Override the platform API base URL. Default: the world_session token's `iss`, else configure({ apiBaseUrl }). */
6
+ apiBaseUrl?: string;
7
+ }
8
+ /** The curated live-room surface handed to world authors. Colyseus's API, re-exposed under Helix.*. */
9
+ export interface HelixRoom {
10
+ readonly roomId: string;
11
+ readonly sessionId: string;
12
+ /** Authoritative shared state (a @colyseus/schema instance shaped like the contract RoomState). */
13
+ readonly state: RoomState;
14
+ /** Fires after every applied patch from the server. */
15
+ onStateChange(cb: (state: RoomState) => void): void;
16
+ /** Subscribe to a collection's adds (Tier-2 seam: players today, entities reserved). Fires for existing entries too. */
17
+ onAdd(collection: 'players', cb: (player: PlayerState, id: string) => void): void;
18
+ onAdd(collection: 'entities', cb: (entity: EntityState, id: string) => void): void;
19
+ onRemove(collection: 'players', cb: (player: PlayerState, id: string) => void): void;
20
+ onRemove(collection: 'entities', cb: (entity: EntityState, id: string) => void): void;
21
+ /** A world-authored server→client message (the room broadcasts these for declared events). */
22
+ onMessage<Payload = unknown>(type: string, cb: (payload: Payload) => void): void;
23
+ /** Per-tick predicted state. Coalesced + flushed at MESSAGE_RATE.stateHz, seq-tagged (D3/D4). */
24
+ sendState(input: ReplicaInput): void;
25
+ /** Activate/deactivate a built-in ability (sent immediately; server enforces the rate ceiling). */
26
+ sendAbility(ability: string, active: boolean): void;
27
+ /** A world-authored declared action (the generic extensibility channel). */
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
+ epoch?: number;
38
+ }): void;
39
+ /**
40
+ * Upload MANY owner-authoritative entities in ONE message (Tier 2 Phase 4.10). Prefer this over per-entity
41
+ * uploadEntity when hosting multiple entities: the per-connection rate cap counts messages, so N single uploads
42
+ * spend N tokens/tick and starve, while one batch spends one. Position is sent as a compact [x,y,z] tuple; each
43
+ * entry is gated/clamped server-side exactly like uploadEntity. Pass only entities this client controls.
44
+ */
45
+ uploadEntities(entities: ReadonlyArray<{
46
+ id: string;
47
+ position: Vec3;
48
+ vars?: Record<string, RoomVarValue>;
49
+ epoch?: number;
50
+ }>): void;
51
+ /** Sent-but-unacknowledged states — the reconciliation tail the engine NetworkDriver replays (D4). */
52
+ pendingInputs(): readonly StateMessage[];
53
+ /** Prune the reconciliation buffer once the server confirms it processed up to `seq` (D4). */
54
+ acknowledge(seq: number): void;
55
+ /** Connection dropped; the client is auto-reconnecting (built into @colyseus/sdk). */
56
+ onDrop(cb: () => void): void;
57
+ /** Auto-reconnection succeeded. */
58
+ onReconnect(cb: () => void): void;
59
+ /** Left for good (kicked, room disposed, or after we leave) — `code` is the close code. */
60
+ onLeave(cb: (code: number) => void): void;
61
+ /** Leave intentionally (no reconnection grace held server-side). */
62
+ leave(): Promise<void>;
63
+ }
64
+ export interface MultiplayerHost {
65
+ isInitialized(): boolean;
66
+ getToken(): string | null;
67
+ getWorldId(): string | null;
68
+ }
69
+ export declare class HelixMultiplayer {
70
+ private readonly host;
71
+ private apiBaseUrl;
72
+ private room;
73
+ private callbacks;
74
+ private seq;
75
+ private entitySeq;
76
+ private entityBatchSeq;
77
+ private pendingInput;
78
+ private flushTimer;
79
+ private inputBuffer;
80
+ private worldId;
81
+ constructor(host: MultiplayerHost);
82
+ configure(options: {
83
+ apiBaseUrl: string;
84
+ }): void;
85
+ joinRoom(worldId?: string, options?: JoinRoomOptions): Promise<HelixRoom>;
86
+ private requestJoin;
87
+ private makeHandle;
88
+ private startFlush;
89
+ private flush;
90
+ private persistReconnect;
91
+ private readReconnect;
92
+ private reconnectWsUrl;
93
+ private clearReconnect;
94
+ private tryReconnect;
95
+ private leave;
96
+ private teardown;
97
+ }
@@ -0,0 +1,250 @@
1
+ // Helix.multiplayer — the client half of HELIX multiplayer (plan pillar D). Wraps the official
2
+ // Colyseus browser client (@colyseus/sdk, schema v4 — the renamed colyseus.js, paired with the 0.17
3
+ // room server) so world authors never import it directly. Flow: POST the platform
4
+ // /api/v1/instant-worlds/:worldId/join with the world_session token → { wsUrl, roomName, credential }
5
+ // → Client.joinOrCreate(token) → a curated HelixRoom handle. The engine NetworkDriver consumes the
6
+ // room state; this module owns the connect handshake, the throttled seq-tagged send path (D3), the
7
+ // reconciliation buffer (D4), and teardown/reconnection wiring (D5).
8
+ import { ClientMessageType, MESSAGE_RATE, CONTRACT_VERSION, } from './multiplayer-contract';
9
+ // ~12s of inputs at 10Hz — generous reconciliation window; bounded so a missing server ack can't grow it forever.
10
+ const INPUT_BUFFER_MAX = 120;
11
+ // sessionStorage key (per world) for the colyseus reconnectionToken + its wsUrl. sessionStorage is per-tab and
12
+ // SURVIVES A RELOAD but not a tab close — exactly the lifetime we want: a reload resumes the held seat; a new
13
+ // tab / post-close start does a fresh join. This is what makes a reload RECONNECT (bypassing the room's
14
+ // onAuth one-seat-per-account gate, which would otherwise reject the reload's fresh join during the grace
15
+ // window) instead of falling back to single-player.
16
+ const RECONNECT_KEY = (worldId) => `helix:mp:reconnect:${worldId}`;
17
+ export class HelixMultiplayer {
18
+ host;
19
+ apiBaseUrl = null;
20
+ room = null;
21
+ callbacks = null;
22
+ seq = 0;
23
+ entitySeq = {}; // per-entity monotonic upload seq (4.6c owner-entity channel)
24
+ entityBatchSeq = 0; // per-connection monotonic batch seq (4.10 owner-entity batch channel)
25
+ pendingInput = null;
26
+ flushTimer = null;
27
+ inputBuffer = [];
28
+ worldId = null; // the joined world (the reconnect-token storage key)
29
+ constructor(host) {
30
+ this.host = host;
31
+ }
32
+ // Pin the platform API base once (local dev / staging where the token issuer isn't the live host).
33
+ configure(options) {
34
+ this.apiBaseUrl = options.apiBaseUrl.replace(/\/$/, '');
35
+ }
36
+ // Join (or create) this world's room. worldId defaults to the current world. Requires login: a guest
37
+ // has no world_session and can't reach /join — multiplayer is logged-in only (single-player for guests).
38
+ async joinRoom(worldId, options = {}) {
39
+ if (!this.host.isInitialized())
40
+ throw new Error('Helix: call Helix.init() before Helix.multiplayer.joinRoom()');
41
+ if (this.room)
42
+ throw new Error('Helix.multiplayer: already in a room — call leave() first');
43
+ const token = this.host.getToken();
44
+ if (!token)
45
+ throw new Error('Helix.multiplayer: multiplayer requires login (no session) — call Helix.auth.requestLogin() first');
46
+ const id = worldId ?? this.host.getWorldId();
47
+ if (!id)
48
+ throw new Error('Helix.multiplayer: no world id — pass joinRoom(worldId) when running outside a HELIX shell');
49
+ const apiBase = options.apiBaseUrl?.replace(/\/$/, '') ?? this.apiBaseUrl ?? issuerOf(token);
50
+ if (!apiBase)
51
+ throw new Error('Helix.multiplayer: cannot resolve the API base URL — call Helix.multiplayer.configure({ apiBaseUrl })');
52
+ const colyseus = (await import('@colyseus/sdk'));
53
+ // RESUME FIRST: on a reload, a seat persisted in sessionStorage is still held in the server's grace window —
54
+ // reconnect to it (resumes the same seat + its vars, bypassing onAuth) instead of a fresh join that onAuth
55
+ // would reject as a duplicate. On any failure (grace expired / clean despawn / new tab) fall back to /join.
56
+ let room = await this.tryReconnect(colyseus, id);
57
+ if (!room) {
58
+ const reservation = await this.requestJoin(apiBase, id, token);
59
+ const client = new colyseus.Client(reservation.wsUrl);
60
+ // buildId is the matchmaking filter (room filterBy(['buildId'])) — it groups players per active build.
61
+ // The room re-validates it against the verified credential in onAuth, so a spoofed value can't cross builds.
62
+ // contractVersion is the wire-protocol version baked into THIS bundle's SDK — onAuth logs a mismatch against
63
+ // the room's version (diagnostics for cross-version play) but never rejects, since old bundles are frozen.
64
+ room = await client.joinOrCreate(reservation.roomName, { token: reservation.credential, buildId: reservation.buildId, contractVersion: CONTRACT_VERSION });
65
+ this.persistReconnect(id, reservation.wsUrl, room.reconnectionToken);
66
+ }
67
+ this.worldId = id;
68
+ this.room = room;
69
+ this.callbacks = colyseus.getStateCallbacks(room);
70
+ this.startFlush();
71
+ // Keep the persisted token fresh as colyseus rotates it (e.g. after an in-page auto-reconnect).
72
+ room.onReconnect(() => this.persistReconnect(id, this.reconnectWsUrl(id), room.reconnectionToken));
73
+ room.onLeave(() => this.teardown());
74
+ return this.makeHandle(room);
75
+ }
76
+ async requestJoin(apiBase, worldId, token) {
77
+ const res = await fetch(`${apiBase}/api/v1/instant-worlds/${encodeURIComponent(worldId)}/join`, {
78
+ method: 'POST',
79
+ headers: { authorization: `Bearer ${token}` },
80
+ });
81
+ if (!res.ok) {
82
+ const detail = await res.json().catch(() => null);
83
+ const message = (detail && typeof detail.message === 'string' && detail.message) || `HTTP ${res.status}`;
84
+ throw new Error(`Helix.multiplayer: join failed — ${message}`);
85
+ }
86
+ return (await res.json());
87
+ }
88
+ makeHandle(room) {
89
+ const collection = (name) => {
90
+ if (!this.callbacks)
91
+ throw new Error('Helix.multiplayer: room not ready');
92
+ return this.callbacks(room.state)[name];
93
+ };
94
+ return {
95
+ roomId: room.roomId,
96
+ sessionId: room.sessionId,
97
+ get state() {
98
+ return room.state;
99
+ },
100
+ onStateChange: (cb) => void room.onStateChange((s) => cb(s)),
101
+ onAdd: (name, cb) => void collection(name).onAdd((v, k) => cb(v, k), true),
102
+ onRemove: (name, cb) => void collection(name).onRemove((v, k) => cb(v, k)),
103
+ onMessage: (type, cb) => void room.onMessage(type, (p) => cb(p)),
104
+ sendState: (input) => {
105
+ this.pendingInput = input;
106
+ },
107
+ sendAbility: (ability, active) => room.send(ClientMessageType.Ability, { ability, active }),
108
+ sendAction: (name, args) => room.send(ClientMessageType.Action, { name, args }),
109
+ uploadEntity: (entityId, input) => {
110
+ const seq = (this.entitySeq[entityId] = (this.entitySeq[entityId] ?? 0) + 1);
111
+ // (P1.C1) forward the authority epoch (when the caller has it) so the server fence can drop a stale-era frame.
112
+ room.send(ClientMessageType.EntityState, { entity: entityId, seq, position: input.position, vars: input.vars, ...(input.epoch !== undefined ? { epoch: input.epoch } : {}) });
113
+ },
114
+ uploadEntities: (entities) => {
115
+ if (entities.length === 0)
116
+ return;
117
+ const seq = ++this.entityBatchSeq;
118
+ const states = entities.map((e) => ({
119
+ e: e.id,
120
+ p: [e.position.x, e.position.y, e.position.z],
121
+ ...(e.vars !== undefined ? { v: e.vars } : {}),
122
+ ...(e.epoch !== undefined ? { ep: e.epoch } : {}),
123
+ }));
124
+ room.send(ClientMessageType.EntityStateBatch, { seq, states });
125
+ },
126
+ pendingInputs: () => this.inputBuffer.slice(),
127
+ acknowledge: (seq) => {
128
+ this.inputBuffer = this.inputBuffer.filter((m) => m.seq > seq);
129
+ },
130
+ onDrop: (cb) => void room.onDrop(() => cb()),
131
+ onReconnect: (cb) => void room.onReconnect(() => cb()),
132
+ onLeave: (cb) => void room.onLeave((code) => cb(code)),
133
+ leave: () => this.leave(),
134
+ };
135
+ }
136
+ // D3: coalesce per-frame sendState calls and flush the latest at most stateHz times/second, tagging a
137
+ // monotonic seq. D4: buffer each sent state (bounded) so the engine can replay the unacknowledged tail.
138
+ startFlush() {
139
+ const periodMs = Math.round(1000 / MESSAGE_RATE.stateHz);
140
+ this.flushTimer = setInterval(() => this.flush(), periodMs);
141
+ }
142
+ flush() {
143
+ if (!this.pendingInput || !this.room)
144
+ return;
145
+ const message = { ...this.pendingInput, seq: ++this.seq };
146
+ this.pendingInput = null;
147
+ this.room.send(ClientMessageType.State, message);
148
+ this.inputBuffer.push(message);
149
+ if (this.inputBuffer.length > INPUT_BUFFER_MAX)
150
+ this.inputBuffer.shift();
151
+ }
152
+ // D5 RECONNECTION (sessionStorage-backed): persist / read / clear the colyseus reconnectionToken + its wsUrl
153
+ // per world. sessionStorage is per-tab and survives a RELOAD but not a close — so a reload resumes the held
154
+ // seat (below) while a new tab does a fresh join. We deliberately do NOT consent-leave on tab unload anymore:
155
+ // letting the socket drop holds the seat in the server's grace window so a reload can RECONNECT to it (and
156
+ // keep its vars), instead of a fresh join that the room's one-seat-per-account onAuth gate would reject. A
157
+ // genuine close just lets the grace window expire (the seat despawns after RECONNECT_GRACE_SEC).
158
+ persistReconnect(worldId, wsUrl, reconnectionToken) {
159
+ if (typeof sessionStorage === 'undefined' || !wsUrl || !reconnectionToken)
160
+ return;
161
+ try {
162
+ sessionStorage.setItem(RECONNECT_KEY(worldId), JSON.stringify({ wsUrl, reconnectionToken }));
163
+ }
164
+ catch { /* storage unavailable */ }
165
+ }
166
+ readReconnect(worldId) {
167
+ if (typeof sessionStorage === 'undefined')
168
+ return null;
169
+ try {
170
+ const raw = sessionStorage.getItem(RECONNECT_KEY(worldId));
171
+ const v = raw ? JSON.parse(raw) : null;
172
+ return v?.wsUrl && v.reconnectionToken ? { wsUrl: v.wsUrl, reconnectionToken: v.reconnectionToken } : null;
173
+ }
174
+ catch {
175
+ return null;
176
+ }
177
+ }
178
+ reconnectWsUrl(worldId) {
179
+ return this.readReconnect(worldId)?.wsUrl ?? '';
180
+ }
181
+ clearReconnect(worldId) {
182
+ if (typeof sessionStorage === 'undefined')
183
+ return;
184
+ try {
185
+ sessionStorage.removeItem(RECONNECT_KEY(worldId));
186
+ }
187
+ catch { /* ignore */ }
188
+ }
189
+ // Resume a grace-held seat (a reload). Returns the resumed room, or null to fall through to a fresh join
190
+ // (no saved token / grace expired / seat despawned / stale wsUrl — reconnect() rejects and we drop the token).
191
+ async tryReconnect(colyseus, worldId) {
192
+ const saved = this.readReconnect(worldId);
193
+ if (!saved)
194
+ return null;
195
+ try {
196
+ const client = new colyseus.Client(saved.wsUrl);
197
+ const room = await client.reconnect(saved.reconnectionToken);
198
+ this.persistReconnect(worldId, saved.wsUrl, room.reconnectionToken); // the token may rotate on resume
199
+ return room;
200
+ }
201
+ catch {
202
+ this.clearReconnect(worldId);
203
+ return null;
204
+ }
205
+ }
206
+ // Leave for good: consented (frees the seat immediately, no grace) + drop the saved token so we don't try to
207
+ // resume a seat we deliberately left. (A reload does NOT call this — it just unloads.)
208
+ async leave() {
209
+ const room = this.room;
210
+ if (this.worldId)
211
+ this.clearReconnect(this.worldId);
212
+ this.teardown();
213
+ if (room)
214
+ await room.leave(true);
215
+ }
216
+ // Reset in-memory state. Called on a drop too (onLeave) — it does NOT clear the saved reconnect token, so a
217
+ // later reload can still resume the grace-held seat.
218
+ teardown() {
219
+ if (this.flushTimer)
220
+ clearInterval(this.flushTimer);
221
+ this.flushTimer = null;
222
+ this.room = null;
223
+ this.callbacks = null;
224
+ this.pendingInput = null;
225
+ this.inputBuffer = [];
226
+ this.worldId = null;
227
+ this.seq = 0;
228
+ this.entitySeq = {};
229
+ this.entityBatchSeq = 0;
230
+ }
231
+ }
232
+ // Read the `iss` claim from a JWT without verifying (the world_session issuer is the platform API
233
+ // origin). Verification isn't this side's job; the room verifies the credential against JWKS. Returns
234
+ // null if there's no usable http(s) issuer — in which case the world must configure({ apiBaseUrl }).
235
+ // NOTE: the new backend does not set `iss` on the world_session yet, so configure()/options is the
236
+ // resolution path until the join lane adds it (tracked in MIGRATION.md, backend step).
237
+ function issuerOf(token) {
238
+ const segment = token.split('.')[1];
239
+ if (!segment)
240
+ return null;
241
+ try {
242
+ const json = atob(segment.replace(/-/g, '+').replace(/_/g, '/'));
243
+ const iss = JSON.parse(json).iss;
244
+ return typeof iss === 'string' && /^https?:\/\//.test(iss) ? iss.replace(/\/$/, '') : null;
245
+ }
246
+ catch {
247
+ return null;
248
+ }
249
+ }
250
+ //# sourceMappingURL=multiplayer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"multiplayer.js","sourceRoot":"","sources":["../src/multiplayer.ts"],"names":[],"mappings":"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"}