@genex-ai/multiplayer 0.16.1 → 0.17.0

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/bridge.js ADDED
@@ -0,0 +1,558 @@
1
+ import {
2
+ connect,
3
+ createPrivate,
4
+ joinPrivate,
5
+ matchmake,
6
+ roomOf
7
+ } from "./chunk-CTDU4IJ5.js";
8
+ import {
9
+ Limits,
10
+ RESERVED_SHARED_PREFIX,
11
+ ServerMessage,
12
+ isReservedEventType
13
+ } from "./chunk-C52XFIB6.js";
14
+
15
+ // src/bridge.ts
16
+ import { getColyseusAuth, getColyseusUrls, initEmbed, waitForPlayer } from "@genex-ai/embed-sdk";
17
+ var version = 1;
18
+ var embedIdentity = {
19
+ ready(gameId) {
20
+ const cfg = globalThis.__GENEX__;
21
+ if (gameId && cfg && typeof cfg.apiUrl === "string" && Array.isArray(cfg.dashboardOrigins)) {
22
+ initEmbed({
23
+ slug: gameId,
24
+ apiUrl: cfg.apiUrl,
25
+ dashboardOrigins: cfg.dashboardOrigins.filter((o) => typeof o === "string")
26
+ });
27
+ }
28
+ return waitForPlayer();
29
+ },
30
+ auth: () => getColyseusAuth(),
31
+ urls: () => getColyseusUrls()
32
+ };
33
+ var identity = embedIdentity;
34
+ function setIdentity(next) {
35
+ identity = next ?? embedIdentity;
36
+ }
37
+ var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
38
+ function parseOptions(optsJson) {
39
+ const o = JSON.parse(optsJson);
40
+ if (!isObject(o)) throw new TypeError("genex bridge: options must be a JSON object");
41
+ const gameId = typeof o.gameId === "string" ? o.gameId : "";
42
+ if (!gameId) throw new TypeError("genex bridge: `gameId` (the game slug) is required");
43
+ const urls = Array.isArray(o.urls) ? o.urls.filter((u) => typeof u === "string" && u !== "") : [];
44
+ let room = "play";
45
+ if (o.room === "queue" || o.room === "private") room = o.room;
46
+ else if (isObject(o.room) && typeof o.room.joinById === "string" && o.room.joinById) room = { joinById: o.room.joinById };
47
+ else if (o.room !== void 0 && o.room !== null && o.room !== "play") {
48
+ throw new TypeError('genex bridge: `room` must be "play", "queue", "private" or {"joinById": code}');
49
+ }
50
+ const timeout = typeof o.controlTimeoutMs === "number" && Number.isFinite(o.controlTimeoutMs) ? o.controlTimeoutMs : void 0;
51
+ const smoothing = o.smoothing === "genex" || o.smoothing === "predict" || isObject(o.smoothing) ? o.smoothing : void 0;
52
+ return {
53
+ gameId,
54
+ name: typeof o.name === "string" && o.name ? o.name : void 0,
55
+ urls,
56
+ room,
57
+ pose: o.pose === true,
58
+ smoothing,
59
+ controlTimeoutMs: timeout,
60
+ options: isObject(o.options) ? o.options : void 0
61
+ };
62
+ }
63
+ var MAX_QUEUED_EVENTS = 4096;
64
+ var sessions = /* @__PURE__ */ new Map();
65
+ var nextHandle = 1;
66
+ function push(e, event) {
67
+ if (e.q.length >= MAX_QUEUED_EVENTS) {
68
+ if (e.overflowed) e.q.splice(1, 1);
69
+ else {
70
+ e.overflowed = true;
71
+ e.q.splice(0, 2, { type: "warning", message: "genex bridge: event queue full; drain it every frame (oldest events dropped)" });
72
+ }
73
+ }
74
+ e.q.push(event);
75
+ }
76
+ var current = (e) => e.closed ? null : e.mm ? e.mm.session : e.session;
77
+ var errorCode = (err) => {
78
+ const c = isObject(err) || err instanceof Error ? err.code : void 0;
79
+ return typeof c === "number" && Number.isFinite(c) ? c : 0;
80
+ };
81
+ var errorMessage = (err) => err instanceof Error ? err.message : typeof err === "string" ? err : "join failed";
82
+ function end(e) {
83
+ e.closed = true;
84
+ e.connected = false;
85
+ for (const off of e.unwire.splice(0)) off();
86
+ e.wired = null;
87
+ e.session = null;
88
+ e.mm = null;
89
+ }
90
+ function wire(e, s) {
91
+ if (e.wired === s) return;
92
+ for (const off of e.unwire.splice(0)) off();
93
+ e.wired = s;
94
+ e.connected = true;
95
+ e.lastConnected.clear();
96
+ e.lastMatch = "";
97
+ const room = roomOf(s);
98
+ e.sessionId = s.id;
99
+ e.roomId = room.roomId;
100
+ const offs = e.unwire;
101
+ const connectedOf = (id) => room.state?.players?.get(id)?.connected !== false;
102
+ const noteConnection = (id) => {
103
+ const now = connectedOf(id);
104
+ const before = e.lastConnected.get(id);
105
+ e.lastConnected.set(id, now);
106
+ if (before !== void 0 && before !== now) push(e, { type: "playerConnection", id, connected: now });
107
+ };
108
+ push(e, { type: "joined", sessionId: s.id });
109
+ room.state?.players?.forEach((_p, id) => {
110
+ e.lastConnected.set(id, connectedOf(id));
111
+ push(e, { type: "playerJoined", id });
112
+ });
113
+ for (const id of s.objects.ids()) push(e, { type: "objectChanged", id, owner: s.objects.get(id)?.owner ?? null });
114
+ if (s.host !== void 0) push(e, { type: "hostChanged", host: s.host });
115
+ offs.push(
116
+ s.on("join", (id) => {
117
+ e.lastConnected.set(id, connectedOf(id));
118
+ push(e, { type: "playerJoined", id });
119
+ }),
120
+ s.on("change", (id) => {
121
+ noteConnection(id);
122
+ if (!e.changed.has(id)) {
123
+ e.changed.add(id);
124
+ push(e, { type: "playerChanged", id });
125
+ }
126
+ }),
127
+ s.on("leave", (id) => {
128
+ e.lastConnected.delete(id);
129
+ push(e, { type: "playerLeft", id });
130
+ }),
131
+ s.on("host", (host) => push(e, { type: "hostChanged", host: host ?? null })),
132
+ s.on("shared", (key, value) => push(e, { type: "sharedChanged", key, value: value ?? null })),
133
+ s.on("object", (id) => {
134
+ const o = s.objects.get(id);
135
+ push(e, o ? { type: "objectChanged", id, owner: o.owner ?? null } : { type: "objectRemoved", id });
136
+ }),
137
+ s.on("room:open", ({ open: open2 }) => push(e, { type: "roomOpen", open: open2 })),
138
+ s.on("reconnecting", ({ attempt, code }) => {
139
+ e.connected = false;
140
+ push(e, { type: "reconnecting", code, attempt });
141
+ }),
142
+ s.on("reconnected", () => {
143
+ e.connected = true;
144
+ push(e, { type: "reconnected" });
145
+ }),
146
+ s.on("disconnect", (code) => {
147
+ e.connected = false;
148
+ if (e.opts.room === "queue") return;
149
+ end(e);
150
+ push(e, { type: "disconnected", code, reason: "" });
151
+ }),
152
+ s.on(ServerMessage.SERVER_RESTART, () => push(e, { type: "serverRestart" })),
153
+ s.on(ServerMessage.MATCH_START, (m) => push(e, { type: "matchStart", roundId: isObject(m) && typeof m.roundId === "number" ? m.roundId : 0 })),
154
+ s.on(ServerMessage.MATCH_ENDED, (m) => push(e, { type: "matchEnded", data: m ?? null })),
155
+ s.on(
156
+ ServerMessage.MATCH_REQUEUE,
157
+ (m) => push(e, { type: "matchRequeue", reason: isObject(m) && typeof m.reason === "string" ? m.reason : null })
158
+ ),
159
+ s.inputs.on((from, data) => push(e, { type: "input", from, data: data ?? null })),
160
+ s.chat.on((m) => push(e, { type: "chat", id: m.id, from: m.from, name: m.name, text: m.text, at: m.at })),
161
+ // The relay's per-lane budget refusal (§5.1). The SDK only logs it; engines get an event.
162
+ room.onMessage(
163
+ ServerMessage.RATE_DROP,
164
+ (m) => push(e, { type: "rateDrop", dropped: isObject(m) && typeof m.dropped === "number" ? m.dropped : 0 })
165
+ ),
166
+ // Every game event: engines receive all types, as native does. The bridge subscribes to no
167
+ // game type by name (see `listen`), so Colyseus routes each one here exactly once.
168
+ room.onMessage("*", (type, data) => {
169
+ if (typeof type === "string" && !isReservedEventType(type)) push(e, { type: "message", kind: type, data: data ?? null });
170
+ })
171
+ );
172
+ }
173
+ function follow(e) {
174
+ const s = current(e);
175
+ if (s && s !== e.wired) wire(e, s);
176
+ }
177
+ async function join(e) {
178
+ const o = e.opts;
179
+ await identity.ready(o.gameId);
180
+ if (e.closed) return;
181
+ const urls = o.urls.length ? o.urls : identity.urls();
182
+ if (!urls.length) throw Object.assign(new Error("genex bridge: no relay URL (pass urls, or run on a Genex page)"), { code: 0 });
183
+ const auth = () => identity.auth();
184
+ const base = { urls, room: o.gameId, name: o.name, auth, pose: o.pose, smoothing: o.smoothing };
185
+ if (o.room === "play") {
186
+ const s = await connect({ ...base, options: o.options });
187
+ if (e.closed) return void s.leave();
188
+ e.session = s;
189
+ wire(e, s);
190
+ return;
191
+ }
192
+ const mmOpts = base;
193
+ const mm = o.room === "queue" ? await matchmake(mmOpts) : o.room === "private" ? await createPrivate(mmOpts) : await joinPrivate(o.room.joinById, mmOpts);
194
+ if (e.closed) return void mm.cancel();
195
+ e.mm = mm;
196
+ mm.on("queue", (m) => {
197
+ if (isObject(m)) push(e, { type: "queue", position: Number(m.position) || 0, size: Number(m.size) || 0 });
198
+ });
199
+ mm.on("matched", () => {
200
+ push(e, { type: "seated" });
201
+ follow(e);
202
+ });
203
+ mm.on("searching", () => {
204
+ if (e.wired) {
205
+ for (const off of e.unwire.splice(0)) off();
206
+ e.wired = null;
207
+ e.connected = false;
208
+ }
209
+ });
210
+ mm.on("error", (err) => {
211
+ const handle = e.mm;
212
+ end(e);
213
+ handle?.cancel();
214
+ push(e, { type: "joinFailed", code: errorCode(err), message: errorMessage(err) });
215
+ });
216
+ mm.on("disconnect", (code) => {
217
+ end(e);
218
+ push(e, { type: "disconnected", code: typeof code === "number" ? code : 0, reason: "" });
219
+ });
220
+ follow(e);
221
+ }
222
+ function start(optsJson) {
223
+ const opts = parseOptions(optsJson);
224
+ const handle = nextHandle++;
225
+ const e = {
226
+ opts,
227
+ q: [],
228
+ overflowed: false,
229
+ session: null,
230
+ mm: null,
231
+ wired: null,
232
+ unwire: [],
233
+ changed: /* @__PURE__ */ new Set(),
234
+ lastConnected: /* @__PURE__ */ new Map(),
235
+ lastMatch: "",
236
+ sessionId: "",
237
+ roomId: "",
238
+ connected: false,
239
+ closed: false
240
+ };
241
+ sessions.set(handle, e);
242
+ const done = join(e).catch((err) => {
243
+ end(e);
244
+ push(e, { type: "joinFailed", code: errorCode(err), message: errorMessage(err) });
245
+ throw err;
246
+ });
247
+ return { handle, done };
248
+ }
249
+ function open(optsJson) {
250
+ const { handle, done } = start(optsJson);
251
+ done.catch(() => {
252
+ });
253
+ return handle;
254
+ }
255
+ async function connect2(optsJson) {
256
+ const { handle, done } = start(optsJson);
257
+ try {
258
+ await done;
259
+ } catch (err) {
260
+ const out = new Error(errorMessage(err));
261
+ out.code = errorCode(err);
262
+ throw out;
263
+ }
264
+ return handle;
265
+ }
266
+ var entry = (handle) => sessions.get(handle);
267
+ var live = (handle) => {
268
+ const e = entry(handle);
269
+ if (!e) return null;
270
+ follow(e);
271
+ return current(e);
272
+ };
273
+ function parseJson(json, what) {
274
+ if (typeof json !== "string") throw new TypeError(`genex bridge: ${what} must be a JSON string`);
275
+ return JSON.parse(json);
276
+ }
277
+ function parseObject(json, what) {
278
+ const v = parseJson(json, what);
279
+ if (!isObject(v)) throw new TypeError(`genex bridge: ${what} must be a JSON object`);
280
+ return v;
281
+ }
282
+ function leave(handle) {
283
+ const e = entry(handle);
284
+ if (!e || e.closed) return;
285
+ const { mm, session } = e;
286
+ end(e);
287
+ if (mm) mm.cancel();
288
+ else session?.leave();
289
+ push(e, { type: "disconnected", code: 4e3, reason: "left" });
290
+ }
291
+ function dispose(handle) {
292
+ leave(handle);
293
+ if (typeof handle === "number") sessions.delete(handle);
294
+ }
295
+ function setMe(handle, json) {
296
+ const state = parseObject(json, "state");
297
+ const s = live(handle);
298
+ if (!s) return false;
299
+ s.me.set(state);
300
+ return true;
301
+ }
302
+ function snapMe(handle, json) {
303
+ const state = parseObject(json, "state");
304
+ const s = live(handle);
305
+ if (!s) return false;
306
+ s.me.snap(state);
307
+ return true;
308
+ }
309
+ function setShared(handle, key, json) {
310
+ const value = parseJson(json, "value");
311
+ if (typeof key !== "string" || !key || key.startsWith(RESERVED_SHARED_PREFIX)) return false;
312
+ const s = live(handle);
313
+ if (!s) return false;
314
+ s.shared.set(key, value);
315
+ return true;
316
+ }
317
+ function setObject(handle, id, json) {
318
+ const state = parseObject(json, "object state");
319
+ const s = live(handle);
320
+ if (!s || !s.objects.get(id)?.isMine) return false;
321
+ s.objects.set(id, state);
322
+ return true;
323
+ }
324
+ function snapObject(handle, id, json) {
325
+ const state = parseObject(json, "object state");
326
+ const s = live(handle);
327
+ if (!s || !s.objects.get(id)?.isMine) return false;
328
+ s.objects.snap(id, state);
329
+ return true;
330
+ }
331
+ function send(handle, type, json) {
332
+ const data = parseJson(json, "event data");
333
+ if (typeof type !== "string" || !type || type.length > Limits.MAX_EVENT_TYPE_CHARS || isReservedEventType(type)) return false;
334
+ const s = live(handle);
335
+ if (!s) return false;
336
+ s.send(type, data);
337
+ return true;
338
+ }
339
+ function listen(_handle, _type) {
340
+ }
341
+ function sendInput(handle, json) {
342
+ const data = parseJson(json, "input");
343
+ const s = live(handle);
344
+ if (!s) return false;
345
+ s.inputs.send(data);
346
+ return true;
347
+ }
348
+ function setRoomOpen(handle, open2) {
349
+ const s = live(handle);
350
+ if (!s) return false;
351
+ s.setRoomOpen(open2 === true);
352
+ return true;
353
+ }
354
+ function chatSend(handle, text) {
355
+ const s = live(handle);
356
+ if (!s || typeof text !== "string") return false;
357
+ s.chat.send(text);
358
+ return true;
359
+ }
360
+ function control(handle, operation, id, requestId, host = false) {
361
+ if (typeof id !== "string" || !id) throw new TypeError("genex bridge: object id must be a non-empty string");
362
+ const e = entry(handle);
363
+ const s = live(handle);
364
+ const settle = (r) => {
365
+ if (!e) return;
366
+ const event = {
367
+ type: "controlResult",
368
+ requestId: String(requestId),
369
+ operation,
370
+ id,
371
+ accepted: r.accepted,
372
+ owner: r.owner ?? null,
373
+ sequence: r.sequence,
374
+ reason: r.reason
375
+ };
376
+ if (typeof r.retryAfterMs === "number") event.retryAfterMs = r.retryAfterMs;
377
+ push(e, event);
378
+ };
379
+ if (!s) {
380
+ settle({ accepted: false, sequence: 0, reason: "disconnected" });
381
+ return;
382
+ }
383
+ const options = { timeoutMs: e?.opts.controlTimeoutMs, ...host ? { authority: "host" } : {} };
384
+ const p = operation === "claim" ? s.objects.claimConfirmed(id, options) : operation === "release" ? s.objects.releaseConfirmed(id, options) : s.objects.removeConfirmed(id, options);
385
+ p.then(settle, () => settle({ accepted: false, sequence: 0, reason: "disconnected" }));
386
+ }
387
+ function claim(handle, id, requestId) {
388
+ control(handle, "claim", id, requestId);
389
+ }
390
+ function claimAsHost(handle, id, requestId) {
391
+ control(handle, "claim", id, requestId, true);
392
+ }
393
+ function release(handle, id, requestId) {
394
+ control(handle, "release", id, requestId);
395
+ }
396
+ function destroy(handle, id, requestId) {
397
+ control(handle, "remove", id, requestId);
398
+ }
399
+ function reportEliminated(handle) {
400
+ const e = entry(handle);
401
+ if (!e?.mm || e.closed) return false;
402
+ e.mm.eliminated();
403
+ return true;
404
+ }
405
+ function reportScore(handle, delta = 1) {
406
+ const e = entry(handle);
407
+ if (!e?.mm || e.closed || typeof delta !== "number" || !Number.isFinite(delta)) return false;
408
+ e.mm.score(delta);
409
+ return true;
410
+ }
411
+ function reportFinish(handle) {
412
+ const e = entry(handle);
413
+ if (!e?.mm || e.closed) return false;
414
+ e.mm.finish();
415
+ return true;
416
+ }
417
+ function serverNow(handle) {
418
+ return live(handle)?.serverNow() ?? null;
419
+ }
420
+ function timeUntil(handle, epochMs) {
421
+ const s = live(handle);
422
+ return s ? s.timeUntil(epochMs) : epochMs - Date.now();
423
+ }
424
+ var finiteOrNull = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
425
+ function sessionJson(handle) {
426
+ const e = entry(handle);
427
+ const s = live(handle);
428
+ const room = s ? roomOf(s) : null;
429
+ const matchText = room?.state?.match;
430
+ let matchState = null;
431
+ if (matchText) {
432
+ try {
433
+ matchState = JSON.parse(matchText);
434
+ } catch {
435
+ matchState = null;
436
+ }
437
+ }
438
+ return JSON.stringify({
439
+ sessionId: s?.id ?? e?.sessionId ?? "",
440
+ roomId: room?.roomId ?? e?.roomId ?? "",
441
+ hostId: s?.host ?? null,
442
+ isHost: s?.isHost ?? false,
443
+ connected: !!s && !!e?.connected,
444
+ closed: !e || e.closed,
445
+ serverNow: finiteOrNull(s?.serverNow()),
446
+ rtt: finiteOrNull(s?.rtt),
447
+ clockSynced: s?.clockSynced ?? false,
448
+ clockOrigin: room?.state?.clockOrigin ?? 0,
449
+ matchState
450
+ });
451
+ }
452
+ function playersJson(handle) {
453
+ const s = live(handle);
454
+ if (!s) return "[]";
455
+ const raw = roomOf(s).state?.players;
456
+ const out = [];
457
+ for (const [id, p] of s.players) {
458
+ const wire2 = raw?.get(id);
459
+ out.push({
460
+ id,
461
+ name: p.name,
462
+ uid: p.uid,
463
+ team: p.team,
464
+ connected: p.connected,
465
+ avatarUrl: p.avatarUrl,
466
+ state: p.state ?? {},
467
+ stateRaw: p.stateRaw ?? {},
468
+ epoch: wire2?.epoch ?? 0,
469
+ isMe: id === s.id,
470
+ posed: wire2?.posed === true
471
+ });
472
+ }
473
+ return JSON.stringify(out);
474
+ }
475
+ function objectsJson(handle) {
476
+ const s = live(handle);
477
+ if (!s) return "[]";
478
+ const raw = roomOf(s).state?.objects;
479
+ const out = [];
480
+ for (const id of s.objects.ids()) {
481
+ const o = s.objects.get(id);
482
+ if (!o) continue;
483
+ out.push({
484
+ id,
485
+ owner: o.owner ?? null,
486
+ isMine: o.isMine,
487
+ seq: raw?.get(id)?.seq ?? 0,
488
+ epoch: o.epoch,
489
+ state: o.state ?? null,
490
+ stateRaw: o.stateRaw ?? null
491
+ });
492
+ }
493
+ return JSON.stringify(out);
494
+ }
495
+ function sharedJson(handle) {
496
+ const s = live(handle);
497
+ const out = {};
498
+ if (s) for (const key of s.shared.keys()) out[key] = s.shared.get(key) ?? null;
499
+ return JSON.stringify(out);
500
+ }
501
+ function drainEventsJson(handle) {
502
+ const e = entry(handle);
503
+ if (!e) return "[]";
504
+ try {
505
+ follow(e);
506
+ const s = current(e);
507
+ if (s) {
508
+ const text = roomOf(s).state?.match ?? "";
509
+ if (text !== e.lastMatch) {
510
+ e.lastMatch = text;
511
+ let state = null;
512
+ try {
513
+ state = text ? JSON.parse(text) : null;
514
+ } catch {
515
+ state = null;
516
+ }
517
+ push(e, { type: "matchState", state });
518
+ }
519
+ }
520
+ e.changed.clear();
521
+ e.overflowed = false;
522
+ return JSON.stringify(e.q.splice(0));
523
+ } catch (err) {
524
+ return JSON.stringify([{ type: "warning", message: `genex bridge: ${errorMessage(err)}` }]);
525
+ }
526
+ }
527
+ export {
528
+ chatSend,
529
+ claim,
530
+ claimAsHost,
531
+ connect2 as connect,
532
+ destroy,
533
+ dispose,
534
+ drainEventsJson,
535
+ leave,
536
+ listen,
537
+ objectsJson,
538
+ open,
539
+ playersJson,
540
+ release,
541
+ reportEliminated,
542
+ reportFinish,
543
+ reportScore,
544
+ send,
545
+ sendInput,
546
+ serverNow,
547
+ sessionJson,
548
+ setIdentity,
549
+ setMe,
550
+ setObject,
551
+ setRoomOpen,
552
+ setShared,
553
+ sharedJson,
554
+ snapMe,
555
+ snapObject,
556
+ timeUntil,
557
+ version
558
+ };