@genex-ai/multiplayer 0.17.0 → 0.17.6

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,286 @@
1
+ import {
2
+ drainQueueJson,
3
+ embedIdentity,
4
+ errorCode,
5
+ errorMessage,
6
+ parseObject,
7
+ push,
8
+ warningEvent
9
+ } from "./chunk-4R7C2TLH.js";
10
+ import {
11
+ connectAuthority
12
+ } from "./chunk-Y6XHXDIK.js";
13
+ import "./chunk-UX3F6YTV.js";
14
+ import {
15
+ firstpersonWorld
16
+ } from "./chunk-5JGRFT2N.js";
17
+ import "./chunk-K3SF5VDS.js";
18
+ import {
19
+ AuthorityTiming,
20
+ CloseCode,
21
+ ServerMessage,
22
+ isReservedEventType
23
+ } from "./chunk-ZSWF4VCN.js";
24
+
25
+ // src/bridge-authority.ts
26
+ import { getAuthorityRegions, getAuthorityUrls, getColyseusAuth } from "@genex-ai/embed-sdk";
27
+ var version = 3;
28
+ var embedAuthorityIdentity = {
29
+ ready: (gameId) => embedIdentity.ready(gameId),
30
+ auth: () => getColyseusAuth(),
31
+ urls: () => getAuthorityUrls(),
32
+ regions: () => getAuthorityRegions()
33
+ };
34
+ var identity = embedAuthorityIdentity;
35
+ function setAuthorityIdentity(next) {
36
+ identity = next ?? embedAuthorityIdentity;
37
+ }
38
+ var physicsCore;
39
+ function setAuthorityPhysicsCore(load) {
40
+ physicsCore = load ?? void 0;
41
+ }
42
+ var MAX_RENDER_DELAY_MS = AuthorityTiming.MAX_REWIND_MS;
43
+ var MAX_CAPACITY_RETRIES = 5;
44
+ var MAX_CAPACITY_RETRY_MS = 3e4;
45
+ var INPUT_FIELDS = ["moveX", "moveY", "moveZ", "yaw", "pitch", "buttons"];
46
+ var isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
47
+ function retrySchedule(v) {
48
+ if (!Array.isArray(v) || v.length > MAX_CAPACITY_RETRIES) return void 0;
49
+ const valid = v.every((ms) => isFiniteNumber(ms) && ms >= 0 && ms <= MAX_CAPACITY_RETRY_MS);
50
+ return valid ? v : void 0;
51
+ }
52
+ function parseOptions(optsJson) {
53
+ const o = parseObject(optsJson, "options");
54
+ const gameId = typeof o.gameId === "string" ? o.gameId : "";
55
+ if (!gameId) throw new TypeError("genex bridge: `gameId` (the game slug) is required");
56
+ const delay = o.renderDelayMs;
57
+ const delayOk = isFiniteNumber(delay) && delay >= 0 && delay <= MAX_RENDER_DELAY_MS;
58
+ return {
59
+ gameId,
60
+ name: typeof o.name === "string" && o.name ? o.name : void 0,
61
+ urls: Array.isArray(o.urls) ? o.urls.filter((u) => typeof u === "string" && u !== "") : [],
62
+ predict: o.predict !== false,
63
+ predictPhysics: o.predictPhysics !== false,
64
+ renderDelayMs: delayOk ? delay : void 0,
65
+ capacityRetryMs: retrySchedule(o.capacityRetryMs)
66
+ };
67
+ }
68
+ var sessions = /* @__PURE__ */ new Map();
69
+ var FIRST_HANDLE = 2 ** 30;
70
+ var lastHandle = FIRST_HANDLE;
71
+ var entry = (handle) => typeof handle === "number" ? sessions.get(handle) : void 0;
72
+ var live = (handle) => {
73
+ const e = entry(handle);
74
+ return e && !e.closed ? e.session : null;
75
+ };
76
+ function end(e) {
77
+ e.closed = true;
78
+ for (const off of e.unwire.splice(0)) off();
79
+ e.session = null;
80
+ }
81
+ function onServerMessage(e, type, data) {
82
+ if (!isReservedEventType(type)) push(e, { type: "message", kind: type, data: data ?? null });
83
+ else if (type === ServerMessage.SERVER_RESTART) push(e, { type: "serverRestart" });
84
+ }
85
+ async function join(e) {
86
+ const o = e.opts;
87
+ await identity.ready(o.gameId);
88
+ if (e.closed) return;
89
+ const urls = o.urls.length ? o.urls : identity.urls();
90
+ const regions = o.urls.length ? [] : identity.regions?.() ?? [];
91
+ if (!urls.length && !regions.length) {
92
+ throw Object.assign(new Error("genex bridge: no authority server URL (pass urls, or run on a Genex page that serves authority rooms)"), { code: 0 });
93
+ }
94
+ const s = await connectAuthority({
95
+ urls,
96
+ regions,
97
+ room: o.gameId,
98
+ name: o.name,
99
+ auth: () => identity.auth(),
100
+ predict: o.predict ? void 0 : false,
101
+ physicsCore,
102
+ predictPhysics: o.predictPhysics,
103
+ renderDelayMs: o.renderDelayMs,
104
+ capacityRetryMs: o.capacityRetryMs,
105
+ onEvent: (type, data) => {
106
+ if (!e.closed) onServerMessage(e, type, data);
107
+ }
108
+ });
109
+ if (e.closed) {
110
+ await s.leave();
111
+ return;
112
+ }
113
+ e.session = s;
114
+ e.sessionId = s.id;
115
+ if (e.world) s.setWorld(e.world);
116
+ e.unwire.push(
117
+ s.on("reconnecting", ({ code }) => push(e, { type: "reconnecting", code })),
118
+ s.on("reconnected", () => push(e, { type: "reconnected" })),
119
+ s.on("disconnect", (code) => {
120
+ end(e);
121
+ push(e, { type: "disconnected", code, reason: "" });
122
+ })
123
+ );
124
+ push(e, { type: "joined", sessionId: s.id });
125
+ }
126
+ function openAuthority(optsJson) {
127
+ const opts = parseOptions(optsJson);
128
+ const handle = ++lastHandle;
129
+ const e = {
130
+ opts,
131
+ q: [],
132
+ overflowed: false,
133
+ session: null,
134
+ unwire: [],
135
+ roster: /* @__PURE__ */ new Set(),
136
+ sessionId: "",
137
+ closed: false,
138
+ world: null
139
+ };
140
+ sessions.set(handle, e);
141
+ join(e).catch((err) => {
142
+ if (e.closed) return;
143
+ end(e);
144
+ push(e, { type: "joinFailed", code: errorCode(err), message: errorMessage(err) });
145
+ });
146
+ return handle;
147
+ }
148
+ function setInput(handle, json) {
149
+ const patch = parseObject(json, "input");
150
+ const s = live(handle);
151
+ if (!s) return false;
152
+ for (const field of INPUT_FIELDS) {
153
+ const value = patch[field];
154
+ if (isFiniteNumber(value)) s.input[field] = value;
155
+ }
156
+ return true;
157
+ }
158
+ function predictProjectile(handle, json) {
159
+ const o = parseObject(json, "projectile");
160
+ const s = live(handle);
161
+ if (!s) return "";
162
+ const num = (v) => isFiniteNumber(v) ? v : 0;
163
+ return s.predictProjectile({
164
+ kind: typeof o.kind === "string" ? o.kind : "",
165
+ x: num(o.x),
166
+ y: num(o.y),
167
+ z: num(o.z),
168
+ vx: num(o.vx),
169
+ vy: num(o.vy),
170
+ vz: num(o.vz),
171
+ ...isFiniteNumber(o.g) ? { g: o.g } : {}
172
+ });
173
+ }
174
+ function setWorld(handle, json) {
175
+ const raw = parseObject(json, "world");
176
+ const e = entry(handle);
177
+ if (!e || e.closed) return false;
178
+ let world;
179
+ try {
180
+ world = firstpersonWorld(raw);
181
+ } catch (err) {
182
+ push(e, warningEvent(errorMessage(err)));
183
+ return false;
184
+ }
185
+ e.world = world;
186
+ e.session?.setWorld(world);
187
+ return true;
188
+ }
189
+ var NO_FRAME = JSON.stringify({ tick: 0, sent: 0, connected: false, me: null, entities: [] });
190
+ function frame(handle, now) {
191
+ const s = live(handle);
192
+ if (!s) return NO_FRAME;
193
+ const sent = s.frame(isFiniteNumber(now) ? now : performance.now());
194
+ return JSON.stringify({
195
+ tick: s.tick,
196
+ sent,
197
+ connected: s.connected,
198
+ me: s.me() ?? null,
199
+ entities: [...s.entities().values()]
200
+ });
201
+ }
202
+ var BODY_F32_STRIDE = 8;
203
+ var BodyF32Flag = { PREDICTED: 1, ASLEEP: 2 };
204
+ var NO_BODIES = new Float32Array(0);
205
+ function drawnBodies(s) {
206
+ return [...s.entities().values()].filter((v) => v.body).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
207
+ }
208
+ function bodiesF32(handle) {
209
+ const s = live(handle);
210
+ if (!s) return NO_BODIES;
211
+ const bodies = drawnBodies(s);
212
+ const out = new Float32Array(bodies.length * BODY_F32_STRIDE);
213
+ bodies.forEach((b, i) => {
214
+ const o = i * BODY_F32_STRIDE;
215
+ out.set([b.x, b.y, b.z, b.qx, b.qy, b.qz, b.qw], o);
216
+ out[o + 7] = (b.predicted ? BodyF32Flag.PREDICTED : 0) | (b.sleep ? BodyF32Flag.ASLEEP : 0);
217
+ });
218
+ return out;
219
+ }
220
+ function bodyKeysJson(handle) {
221
+ const s = live(handle);
222
+ return JSON.stringify(s ? drawnBodies(s).map((b) => b.id) : []);
223
+ }
224
+ function authorityJson(handle) {
225
+ const e = entry(handle);
226
+ const s = live(handle);
227
+ return JSON.stringify({
228
+ sessionId: e?.sessionId ?? "",
229
+ connected: !!s && s.connected,
230
+ closed: !e || e.closed,
231
+ tick: s?.tick ?? 0,
232
+ queued: e?.q.length ?? 0,
233
+ players: s ? [...s.players().values()] : [],
234
+ telemetry: s ? { ...s.telemetry } : null,
235
+ prediction: s ? s.prediction : null,
236
+ physicsPrediction: s ? s.physicsPrediction : null
237
+ });
238
+ }
239
+ function noteRoster(e, s) {
240
+ const now = new Set(s.players().keys());
241
+ for (const id of now) if (!e.roster.has(id)) push(e, { type: "playerJoined", id });
242
+ for (const id of e.roster) if (!now.has(id)) push(e, { type: "playerLeft", id });
243
+ e.roster = now;
244
+ }
245
+ function drainAuthorityEventsJson(handle) {
246
+ const e = entry(handle);
247
+ if (!e) return "[]";
248
+ try {
249
+ const s = live(handle);
250
+ if (s) noteRoster(e, s);
251
+ } catch (err) {
252
+ push(e, warningEvent(errorMessage(err)));
253
+ }
254
+ return drainQueueJson(e);
255
+ }
256
+ function closeAuthority(handle) {
257
+ const e = entry(handle);
258
+ if (!e || e.closed) return;
259
+ const s = e.session;
260
+ end(e);
261
+ void s?.leave().catch(() => {
262
+ });
263
+ push(e, { type: "disconnected", code: CloseCode.CONSENTED, reason: "left" });
264
+ }
265
+ function disposeAuthority(handle) {
266
+ closeAuthority(handle);
267
+ if (typeof handle === "number") sessions.delete(handle);
268
+ }
269
+ export {
270
+ BODY_F32_STRIDE,
271
+ BodyF32Flag,
272
+ authorityJson,
273
+ bodiesF32,
274
+ bodyKeysJson,
275
+ closeAuthority,
276
+ disposeAuthority,
277
+ drainAuthorityEventsJson,
278
+ frame,
279
+ openAuthority,
280
+ predictProjectile,
281
+ setAuthorityIdentity,
282
+ setAuthorityPhysicsCore,
283
+ setInput,
284
+ setWorld,
285
+ version
286
+ };
@@ -0,0 +1,27 @@
1
+ type Json = Record<string, unknown>;
2
+ /** One queued event: `{ type, ...fields }` (BRIDGE.md, "Events"). */
3
+ type BridgeEvent = {
4
+ type: string;
5
+ } & Json;
6
+ /**
7
+ * Where the bridge gets the player's identity. The default is `@genex-ai/embed-sdk`; a host page
8
+ * or a test replaces it with `setIdentity` (relay) or `setAuthorityIdentity` (authority).
9
+ */
10
+ type BridgeIdentity = {
11
+ /** Resolves once a player exists (guest or signed in); rejects when there never will be one. */
12
+ ready(gameId: string): Promise<unknown> | unknown;
13
+ /** The join credential, read fresh for EVERY join (never cached). `undefined` = tokenless. */
14
+ auth(): {
15
+ embedToken: string;
16
+ } | undefined;
17
+ /** The server URLs to use when the engine passes none. */
18
+ urls(): string[];
19
+ /** The servers by region (authority rooms, PROTOCOL.md §17.1 *Regions*) to use when the engine
20
+ * passes no urls. Omitted: none. */
21
+ regions?(): {
22
+ region: string;
23
+ urls: string[];
24
+ }[];
25
+ };
26
+
27
+ export type { BridgeIdentity as B, BridgeEvent as a };
package/dist/bridge.d.ts CHANGED
@@ -1,24 +1,30 @@
1
- /** The contract version (BRIDGE.md). Bumped only on a breaking change to a signature or a shape. */
2
- declare const version = 1;
3
- type Json = Record<string, unknown>;
4
- /** One queued event: `{ type, ...fields }` (BRIDGE.md, "Events"). */
5
- type BridgeEvent = {
6
- type: string;
7
- } & Json;
1
+ import { B as BridgeIdentity } from './bridge-core-Dhc73GC8.js';
2
+ export { a as BridgeEvent } from './bridge-core-Dhc73GC8.js';
3
+
4
+ /**
5
+ * `@genex-ai/multiplayer/bridge` — a flat facade over this SDK for engines that compile to the web
6
+ * and cannot hold a JS object graph: Rust (wasm-bindgen), Godot web exports (`JavaScriptBridge`),
7
+ * Unity WebGL (`.jslib`). The normative contract is `sdks/rust/BRIDGE.md`.
8
+ *
9
+ * - Every value crosses as a JSON string, a number or a boolean: one crossing per call.
10
+ * - A session is an opaque numeric handle. `open()` returns one synchronously (the join outcome
11
+ * arrives as a `joined` / `joinFailed` event); `connect()` is the same join as a Promise.
12
+ * - Everything the session reports is queued and handed over by `drainEventsJson()`, which an
13
+ * engine calls once per frame. Event objects are exactly the serde form of the Rust `Event`
14
+ * enum, plus a few types only Godot reads (BRIDGE.md, "Events").
15
+ * - Identity stays in TypeScript: the bridge waits for `@genex-ai/embed-sdk`'s player and reads a
16
+ * FRESH embed token for every join. An engine never sees a token.
17
+ *
18
+ * The self-registering build (`dist/bridge.global.js`) sets `window.GenexMultiplayerBridge` to
19
+ * this module's exports, with the SDK and embed-sdk bundled in.
20
+ */
21
+
8
22
  /**
9
- * Where the bridge gets the player's identity. The default is `@genex-ai/embed-sdk`; a host page
10
- * or a test replaces it with {@link setIdentity}.
23
+ * The contract version (BRIDGE.md). Bumped only on a breaking change to a signature or a shape, or
24
+ * when calls are added: 2 added the authority calls (`bridge-authority.ts`, in the authority build
25
+ * only), and every version-1 call is unchanged.
11
26
  */
12
- type BridgeIdentity = {
13
- /** Resolves once a player exists (guest or signed in); rejects when there never will be one. */
14
- ready(gameId: string): Promise<unknown> | unknown;
15
- /** The join credential, read fresh for EVERY join (never cached). `undefined` = tokenless. */
16
- auth(): {
17
- embedToken: string;
18
- } | undefined;
19
- /** The relay URLs to use when the engine passes none. */
20
- urls(): string[];
21
- };
27
+ declare const version = 2;
22
28
  /**
23
29
  * Replace the identity source (`null` restores embed-sdk). For a host page that manages identity
24
30
  * itself, and for tests against a token-free local relay. Not part of the engine contract.
@@ -86,4 +92,4 @@ declare function sharedJson(handle: number): string;
86
92
  */
87
93
  declare function drainEventsJson(handle: number): string;
88
94
 
89
- export { type BridgeEvent, type BridgeIdentity, chatSend, claim, claimAsHost, connect, destroy, dispose, drainEventsJson, leave, listen, objectsJson, open, playersJson, release, reportEliminated, reportFinish, reportScore, send, sendInput, serverNow, sessionJson, setIdentity, setMe, setObject, setRoomOpen, setShared, sharedJson, snapMe, snapObject, timeUntil, version };
95
+ export { BridgeIdentity, chatSend, claim, claimAsHost, connect, destroy, dispose, drainEventsJson, leave, listen, objectsJson, open, playersJson, release, reportEliminated, reportFinish, reportScore, send, sendInput, serverNow, sessionJson, setIdentity, setMe, setObject, setRoomOpen, setShared, sharedJson, snapMe, snapObject, timeUntil, version };