@helixdev/helix-sdk 0.1.1-staging.7 → 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 +19 -2
- package/dist/index.js +92 -0
- package/dist/index.js.map +1 -1
- package/dist/multiplayer-contract/credential.d.ts +34 -0
- package/dist/multiplayer-contract/credential.js +5 -0
- package/dist/multiplayer-contract/credential.js.map +1 -0
- package/dist/multiplayer-contract/index.d.ts +10 -0
- package/dist/multiplayer-contract/index.js +26 -0
- package/dist/multiplayer-contract/index.js.map +1 -0
- package/dist/multiplayer-contract/messages.d.ts +171 -0
- package/dist/multiplayer-contract/messages.js +57 -0
- package/dist/multiplayer-contract/messages.js.map +1 -0
- package/dist/multiplayer-contract/room.d.ts +92 -0
- package/dist/multiplayer-contract/room.js +17 -0
- package/dist/multiplayer-contract/room.js.map +1 -0
- package/dist/multiplayer-contract/state.d.ts +46 -0
- package/dist/multiplayer-contract/state.js +15 -0
- package/dist/multiplayer-contract/state.js.map +1 -0
- package/dist/multiplayer.d.ts +96 -0
- package/dist/multiplayer.js +247 -0
- package/dist/multiplayer.js.map +1 -0
- package/dist/protocol.d.ts +11 -1
- package/dist/protocol.js +3 -2
- package/dist/protocol.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +14 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { HelixWorldContext, HelixUser } from './protocol';
|
|
2
|
-
|
|
1
|
+
import { HelixWorldContext, HelixUser, DebugLogEntry } from './protocol';
|
|
2
|
+
import { HelixMultiplayer } from './multiplayer';
|
|
3
|
+
export type { HelixWorldContext, HelixSession, HelixUser, DebugLogEntry, DebugLogLevel } from './protocol';
|
|
4
|
+
export type { HelixRoom, JoinRoomOptions, ReplicaInput } from './multiplayer';
|
|
5
|
+
export * from './multiplayer-contract';
|
|
3
6
|
export type HelixInitResult = {
|
|
4
7
|
embedded: boolean;
|
|
5
8
|
world: HelixWorldContext | null;
|
|
@@ -12,8 +15,19 @@ declare class HelixSdk {
|
|
|
12
15
|
private world;
|
|
13
16
|
private initialized;
|
|
14
17
|
private listeners;
|
|
18
|
+
private debugEnabled;
|
|
19
|
+
private debugRing;
|
|
20
|
+
private debugListeners;
|
|
21
|
+
private consoleHooked;
|
|
15
22
|
private pendingLogins;
|
|
16
23
|
init(): Promise<HelixInitResult>;
|
|
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
|
+
};
|
|
17
31
|
readonly auth: {
|
|
18
32
|
getUser: () => Promise<HelixUser | null>;
|
|
19
33
|
isAuthenticated: () => boolean;
|
|
@@ -26,6 +40,9 @@ declare class HelixSdk {
|
|
|
26
40
|
private waitForInit;
|
|
27
41
|
private onMessage;
|
|
28
42
|
private setSession;
|
|
43
|
+
private enableDebug;
|
|
44
|
+
private hookConsole;
|
|
45
|
+
private captureLog;
|
|
29
46
|
private post;
|
|
30
47
|
}
|
|
31
48
|
export declare const Helix: HelixSdk;
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { isShellMessage, PROTOCOL_VERSION, } from './protocol';
|
|
2
|
+
import { HelixMultiplayer } from './multiplayer';
|
|
3
|
+
// The multiplayer wire contract. Re-exported here so `@hypersoniclabs/helix-sdk` root consumers (and
|
|
4
|
+
// classic node-resolution type imports, e.g. the backend minting RoomCredentialClaims) get it; bundler
|
|
5
|
+
// consumers that want only the contract import the lighter '@hypersoniclabs/helix-sdk/multiplayer-contract'.
|
|
6
|
+
export * from './multiplayer-contract';
|
|
2
7
|
const INIT_TIMEOUT_MS = 3000;
|
|
3
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)
|
|
4
10
|
class HelixSdk {
|
|
5
11
|
shellOrigin = null;
|
|
6
12
|
session = null;
|
|
7
13
|
world = null;
|
|
8
14
|
initialized = false;
|
|
9
15
|
listeners = new Set();
|
|
16
|
+
debugEnabled = false;
|
|
17
|
+
debugRing = [];
|
|
18
|
+
debugListeners = new Set();
|
|
19
|
+
consoleHooked = false;
|
|
10
20
|
pendingLogins = new Map();
|
|
11
21
|
// Performs the shell handshake. Call once at world start, before any other
|
|
12
22
|
// Helix API. Resolves with embedded=false if no shell answers (local dev).
|
|
@@ -21,6 +31,28 @@ class HelixSdk {
|
|
|
21
31
|
this.initialized = true;
|
|
22
32
|
return this.snapshot();
|
|
23
33
|
}
|
|
34
|
+
// Multiplayer (pillar D): Helix.multiplayer.joinRoom(). Reads the world_session token + world id off
|
|
35
|
+
// this singleton; the heavy Colyseus client is dynamically imported only on join.
|
|
36
|
+
multiplayer = new HelixMultiplayer({
|
|
37
|
+
isInitialized: () => this.initialized,
|
|
38
|
+
getToken: () => this.session?.token ?? null,
|
|
39
|
+
getWorldId: () => this.world?.id ?? null,
|
|
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
|
+
};
|
|
24
56
|
auth = {
|
|
25
57
|
getUser: async () => {
|
|
26
58
|
this.assertInitialized();
|
|
@@ -111,6 +143,8 @@ class HelixSdk {
|
|
|
111
143
|
this.shellOrigin = event.origin;
|
|
112
144
|
this.world = msg.world;
|
|
113
145
|
this.setSession(msg.session);
|
|
146
|
+
if (msg.debug)
|
|
147
|
+
this.enableDebug();
|
|
114
148
|
break;
|
|
115
149
|
case 'helix:session':
|
|
116
150
|
this.setSession(msg.session);
|
|
@@ -137,10 +171,68 @@ class HelixSdk {
|
|
|
137
171
|
listener(session?.user ?? null);
|
|
138
172
|
}
|
|
139
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
|
+
}
|
|
140
218
|
post(message, targetOrigin) {
|
|
141
219
|
window.parent.postMessage(message, targetOrigin);
|
|
142
220
|
}
|
|
143
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
|
+
}
|
|
144
236
|
// Worlds import this singleton: `import { Helix } from '@hypersoniclabs/helix-sdk'`.
|
|
145
237
|
export const Helix = new HelixSdk();
|
|
146
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":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare const ROOM_CREDENTIAL_AUDIENCE: "helix-instant-room";
|
|
2
|
+
/**
|
|
3
|
+
* Claims the backend signs (HS384) and the room verifies. Mirrors the world_session claim shape
|
|
4
|
+
* (sub/displayName/worldId/scopes/jti) plus the immutable buildId the room loads its config from.
|
|
5
|
+
*/
|
|
6
|
+
export interface RoomCredentialClaims {
|
|
7
|
+
/** Player/user id. */
|
|
8
|
+
sub: string;
|
|
9
|
+
/** Sanitized display name for nameplates. */
|
|
10
|
+
displayName: string;
|
|
11
|
+
/** World the room belongs to. */
|
|
12
|
+
worldId: string;
|
|
13
|
+
/** Immutable build the room loads its declarative config (allowed messages, room-var shape) from. */
|
|
14
|
+
buildId: string;
|
|
15
|
+
/** Scopes from manifest.permissions; must include 'multiplayer' to join a room. */
|
|
16
|
+
scopes: string[];
|
|
17
|
+
/** Manifest maxPlayers (authoritative) → the room's maxClients cap, so it auto-locks + spills when full. */
|
|
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;
|
|
28
|
+
/** Always ROOM_CREDENTIAL_AUDIENCE. */
|
|
29
|
+
aud: typeof ROOM_CREDENTIAL_AUDIENCE;
|
|
30
|
+
iss: string;
|
|
31
|
+
exp: number;
|
|
32
|
+
iat: number;
|
|
33
|
+
jti: string;
|
|
34
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Room credential — the short-lived JWT the backend mints (createRoomSession) and the Colyseus room
|
|
2
|
+
// verifies in onAuth with a shared HMAC secret (ROOM_JWT_SECRET, Option C). Distinct audience from the
|
|
3
|
+
// world_session token (aud 'helix-instant-world') so a world token can't be replayed to join a room.
|
|
4
|
+
export const ROOM_CREDENTIAL_AUDIENCE = 'helix-instant-room';
|
|
5
|
+
//# sourceMappingURL=credential.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential.js","sourceRoot":"","sources":["../../src/multiplayer-contract/credential.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,uGAAuG;AACvG,qGAAqG;AAErG,MAAM,CAAC,MAAM,wBAAwB,GAAG,oBAA6B,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
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.
|
|
5
|
+
*/
|
|
6
|
+
export declare const CONTRACT_VERSION: 11;
|
|
7
|
+
export * from './state';
|
|
8
|
+
export * from './room';
|
|
9
|
+
export * from './messages';
|
|
10
|
+
export * from './credential';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// @hypersoniclabs/helix-sdk/multiplayer-contract — the multiplayer wire contract.
|
|
2
|
+
//
|
|
3
|
+
// The single source of truth for the data shapes three independently-built pieces must agree on:
|
|
4
|
+
// • the Colyseus room (@colyseus/schema state + message dispatch + runtime validation),
|
|
5
|
+
// • this SDK's Helix.multiplayer client (send path + room.state),
|
|
6
|
+
// • the engine's NetworkDriver adapter (consumes the replicated params → blackboard).
|
|
7
|
+
// Plus the backend (mints RoomCredentialClaims) and the room (verifies them).
|
|
8
|
+
//
|
|
9
|
+
// Pure types + constants, ZERO runtime deps — so the standalone three.js engine adapter can import
|
|
10
|
+
// the types at zero runtime cost. Runtime message validation lives in the room (the only place
|
|
11
|
+
// untrusted inbound messages arrive), not here.
|
|
12
|
+
//
|
|
13
|
+
// The state has two layers (see room.ts): players are FIXED (the engine character contract); roomVars,
|
|
14
|
+
// per-player vars, and entities are DECLARED per-world — that's how an agent syncs custom game state
|
|
15
|
+
// without writing server code. The generic 'action' message (messages.ts) is the channel for it.
|
|
16
|
+
/**
|
|
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.
|
|
20
|
+
*/
|
|
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)
|
|
22
|
+
export * from './state';
|
|
23
|
+
export * from './room';
|
|
24
|
+
export * from './messages';
|
|
25
|
+
export * from './credential';
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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;;;;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"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { Vec3 } from './state';
|
|
2
|
+
import type { RoomVarValue } from './room';
|
|
3
|
+
export declare const ClientMessageType: {
|
|
4
|
+
/** The player's predicted kinematic state for this tick (seq-tagged for reconciliation). */
|
|
5
|
+
readonly State: "state";
|
|
6
|
+
/** Activate or deactivate an ability on this player (the built-in, engine-level intent). */
|
|
7
|
+
readonly Ability: "ability";
|
|
8
|
+
/** A world-authored declared action (the generic extensibility channel — see ActionMessage). */
|
|
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";
|
|
21
|
+
};
|
|
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];
|
|
29
|
+
/** Per-tick state upload. Units match PlayerReplicaState (position m; all angles degrees, see *Deg). */
|
|
30
|
+
export interface StateMessage {
|
|
31
|
+
/** Monotonic per-client sequence; the room echoes the last applied seq for client reconciliation. */
|
|
32
|
+
seq: number;
|
|
33
|
+
position: Vec3;
|
|
34
|
+
facingYawDeg: number;
|
|
35
|
+
speed: number;
|
|
36
|
+
moveDirectionDeg: number;
|
|
37
|
+
verticalVelocity: number;
|
|
38
|
+
grounded: boolean;
|
|
39
|
+
crouched: boolean;
|
|
40
|
+
aimYawDeg: number;
|
|
41
|
+
aimPitchDeg: number;
|
|
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
|
+
}
|
|
95
|
+
/** Discrete ability activation intent. */
|
|
96
|
+
export interface AbilityMessage {
|
|
97
|
+
/** Ability id (must be one the world's build declares). */
|
|
98
|
+
ability: string;
|
|
99
|
+
/** true = activate, false = deactivate. */
|
|
100
|
+
active: boolean;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A world-authored declared action — the generic extensibility channel (e.g. 'captureFlag',
|
|
104
|
+
* 'scorePoint'). The room validates `name` against the world's declared action set and `args` against
|
|
105
|
+
* that action's declared arg schema before mutating authoritative state (table-driven dispatch); this
|
|
106
|
+
* module carries the envelope + arg vocabulary only, never the per-world rules.
|
|
107
|
+
*/
|
|
108
|
+
export interface ActionMessage {
|
|
109
|
+
/** Action name; must be one the world's build declares. */
|
|
110
|
+
name: string;
|
|
111
|
+
/** Primitive/Vec3 args, validated against the declared action's schema by the room. */
|
|
112
|
+
args?: Record<string, RoomVarValue>;
|
|
113
|
+
}
|
|
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. */
|
|
142
|
+
export interface ClientMessagePayloads {
|
|
143
|
+
[ClientMessageType.State]: StateMessage;
|
|
144
|
+
[ClientMessageType.Ability]: AbilityMessage;
|
|
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;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Per-connection rate ceilings (messages/second) the room enforces; over-rate clients are dropped.
|
|
156
|
+
* State is the throttled input channel (≤~10Hz, plan D3); ability is bursty but bounded.
|
|
157
|
+
*/
|
|
158
|
+
export declare const MESSAGE_RATE: {
|
|
159
|
+
readonly stateHz: 10;
|
|
160
|
+
readonly abilityHz: 20;
|
|
161
|
+
readonly actionHz: 20;
|
|
162
|
+
readonly entityStateHz: 10;
|
|
163
|
+
readonly pingHz: 4;
|
|
164
|
+
};
|
|
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;
|
|
@@ -0,0 +1,57 @@
|
|
|
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 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;
|
|
57
|
+
//# 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;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,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"}
|