@usions/sdk 2.1.5 → 2.10.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.
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Usion SDK Game Netcode — convenience wiring of the netcode toolkit onto the
3
+ * game module. These helpers ride on the existing realtime/action channels, so
4
+ * they work in every connection mode with no host/backend change (the one
5
+ * exception is RTT measurement — see game.ping below).
6
+ */
7
+ import { SnapshotInterpolation } from './netcode/interpolation.js';
8
+ import { Predictor } from './netcode/prediction.js';
9
+ import { Coalescer } from './netcode/sender.js';
10
+ import { PingMeter } from './netcode/ping.js';
11
+ import { MeshConnection } from './netcode/mesh.js';
12
+ import { MeshNetwork } from './netcode/mesh-network.js';
13
+ import { WebTransportConnection } from './netcode/webtransport.js';
14
+ import { NetworkSim } from './netcode/netsim.js';
15
+ import { Lockstep } from './netcode/lockstep.js';
16
+ import { LagCompensator } from './netcode/lagcomp.js';
17
+ import { diff, patch, quantize } from './netcode/delta.js';
18
+ import { encode, decode } from './netcode/binary.js';
19
+
20
+ export function applyGameNetcode(game, Usion) {
21
+ game.diff = diff;
22
+ game.patch = patch;
23
+ game.quantize = quantize;
24
+ game.encode = encode;
25
+ game.decode = decode;
26
+
27
+ game.createInterpolation = function (opts) { return new SnapshotInterpolation(opts || {}); };
28
+ game.createPredictor = function (opts) { return new Predictor(opts || {}); };
29
+ game.createLagCompensator = function (opts) { return new LagCompensator(opts || {}); };
30
+ game.createLockstep = function (opts) { return new Lockstep(opts || {}); };
31
+
32
+ // ── Realtime channel router ──────────────────────────────────────────────
33
+ // Multiplexes realtime by action_type so replicate / mesh / mesh-network and
34
+ // the game's own onRealtime can coexist. Also the inject point for the
35
+ // network simulator's inbound side.
36
+ game._channels = game._channels || {};
37
+ game._userRealtime = game._userRealtime || null;
38
+ game._inboundDispatch = null;
39
+ function dispatchRealtime(data) {
40
+ const at = data && data.action_type;
41
+ if (at && game._channels[at]) { game._channels[at](data && (data.action_data !== undefined ? data.action_data : data), data); return; }
42
+ if (game._userRealtime) game._userRealtime(data);
43
+ }
44
+ game._dispatchRealtime = dispatchRealtime;
45
+ game._installRealtimeRouter = function () {
46
+ if (game._routerInstalled) return;
47
+ if (game._eventHandlers.realtime && game._eventHandlers.realtime !== game._router) game._userRealtime = game._eventHandlers.realtime;
48
+ game._router = function (data) { (game._inboundDispatch || game._dispatchRealtime)(data); };
49
+ game._eventHandlers.realtime = game._router;
50
+ game._routerInstalled = true;
51
+ };
52
+ /** Register a handler for one realtime action_type. Returns an unsubscribe fn. */
53
+ game._onChannel = function (type, handler) { game._channels[type] = handler; game._installRealtimeRouter(); return function () { delete game._channels[type]; }; };
54
+ // Route onRealtime through the router so channels keep working.
55
+ game.onRealtime = function (cb) { game._userRealtime = cb; game._installRealtimeRouter(); };
56
+
57
+ /** Fixed-rate outbound sender (defaults to game.realtime). */
58
+ game.createSender = function (opts) {
59
+ opts = opts || {};
60
+ const self = this;
61
+ const send = opts.send || function (type, data) { self.realtime(type, data); };
62
+ return new Coalescer({
63
+ hz: opts.hz || 20,
64
+ autoStart: opts.autoStart !== false,
65
+ onFlush: function (entries) { for (let i = 0; i < entries.length; i++) send(entries[i].type, entries[i].data); },
66
+ });
67
+ };
68
+
69
+ /**
70
+ * Coalesced, sequence-guarded, delta-compressed snapshot sender.
71
+ *
72
+ * Deltas are computed against the **last keyframe** (not the previous frame),
73
+ * so a single lost delta on an unreliable channel never desyncs — the next
74
+ * delta still applies to the keyframe the receiver holds. Each message
75
+ * carries a monotonic seq (`s`) and, for deltas, the keyframe seq it's based
76
+ * on (`b`); the receiver uses these to drop stale/out-of-order frames and to
77
+ * hold until it has the right keyframe. Optional float `precision`
78
+ * (quantization) and binary `encode` shrink the wire further.
79
+ *
80
+ * Pass `source` (a getter) to auto-read the state each tick — the basis for
81
+ * game.replicate(). Pair with game.createSnapshotReceiver().
82
+ */
83
+ game.createSnapshotSender = function (opts) {
84
+ opts = opts || {};
85
+ const self = this;
86
+ const channel = opts.channel || 'state';
87
+ const useDelta = opts.delta !== false;
88
+ const keyframeEvery = opts.keyframeEvery != null ? opts.keyframeEvery : 30;
89
+ const precision = opts.precision;
90
+ const enc = opts.encode === true ? encode : (typeof opts.encode === 'function' ? opts.encode : null);
91
+ const send = opts.send || function (type, data) { self.realtime(type, data); };
92
+ const source = typeof opts.source === 'function' ? opts.source : null;
93
+ let keyframeState = null; // an immutable clone (deltas diff against this)
94
+ let keyframeSeq = 0;
95
+ let seq = 0;
96
+ let sinceKey = 0;
97
+
98
+ function snapshotClone(v) { return v == null ? v : JSON.parse(JSON.stringify(v)); }
99
+
100
+ function emit(raw) {
101
+ if (raw === undefined || raw === null) return;
102
+ const state = precision != null ? quantize(raw, precision) : raw;
103
+ let payload;
104
+ const wantKey = !useDelta || keyframeState === null || sinceKey >= keyframeEvery;
105
+ if (wantKey) {
106
+ seq += 1; keyframeState = snapshotClone(state); keyframeSeq = seq; sinceKey = 0;
107
+ payload = { s: seq, f: state };
108
+ } else {
109
+ const d = diff(keyframeState, state);
110
+ if (d === undefined) return; // nothing changed — don't burn a seq
111
+ seq += 1; sinceKey += 1;
112
+ payload = { s: seq, b: keyframeSeq, d: d };
113
+ }
114
+ send(channel, enc ? enc(payload) : payload);
115
+ }
116
+
117
+ // Two drive modes: a `source` getter (read every tick) or queued send().
118
+ const hz = opts.hz || 20;
119
+ let queued;
120
+ const co = source ? null : new Coalescer({
121
+ hz: hz, autoStart: opts.autoStart !== false,
122
+ onFlush: function (entries) { for (let i = 0; i < entries.length; i++) if (entries[i].type === '__snap') queued = entries[i].data; emit(queued); },
123
+ });
124
+ let timer = null;
125
+ function start() {
126
+ if (co) { co.start(); return; }
127
+ if (timer || typeof setInterval === 'undefined') return;
128
+ timer = setInterval(function () { emit(source()); }, Math.max(1, Math.round(1000 / hz)));
129
+ }
130
+ function stop() { if (co) co.stop(); if (timer) { clearInterval(timer); timer = null; } }
131
+ if (source && opts.autoStart !== false) start();
132
+
133
+ return {
134
+ send: function (state) { if (co) co.queue('__snap', state); else emit(state); },
135
+ flush: function () { if (co) co.flush(); else if (source) emit(source()); },
136
+ start: start,
137
+ stop: stop,
138
+ reset: function () { keyframeState = null; keyframeSeq = 0; seq = 0; sinceKey = 0; },
139
+ };
140
+ };
141
+
142
+ /**
143
+ * Receiver for createSnapshotSender. Holds the keyframe baseline + last
144
+ * applied seq, so it safely drops stale/out-of-order frames and ignores
145
+ * deltas whose keyframe it missed (until the next keyframe arrives).
146
+ * @returns {{ receive:(msg:any)=>any, state:any, stats:object, reset:()=>void }}
147
+ */
148
+ game.createSnapshotReceiver = function (opts) {
149
+ opts = opts || {};
150
+ const dec = opts.decode === true ? decode : (typeof opts.decode === 'function' ? opts.decode : null);
151
+ let base = null, baseSeq = -1, current = null, appliedSeq = -1, dropped = 0;
152
+ return {
153
+ receive: function (msg) {
154
+ if (dec && (msg instanceof ArrayBuffer || ArrayBuffer.isView(msg))) msg = dec(msg);
155
+ if (!msg) return current;
156
+ if (msg.f !== undefined) {
157
+ if (msg.s !== undefined && msg.s <= appliedSeq) { dropped += 1; return current; }
158
+ base = msg.f; baseSeq = msg.s !== undefined ? msg.s : 0; current = msg.f; appliedSeq = baseSeq;
159
+ return current;
160
+ }
161
+ if (msg.d !== undefined) {
162
+ if (msg.b !== undefined && msg.b !== baseSeq) { dropped += 1; return current; } // missed keyframe
163
+ if (msg.s !== undefined && msg.s <= appliedSeq) { dropped += 1; return current; } // stale/dup
164
+ current = patch(base, msg.d);
165
+ if (msg.s !== undefined) appliedSeq = msg.s;
166
+ return current;
167
+ }
168
+ return current;
169
+ },
170
+ get state() { return current; },
171
+ get stats() { return { appliedSeq: appliedSeq, baseSeq: baseSeq, dropped: dropped }; },
172
+ reset: function () { base = null; baseSeq = -1; current = null; appliedSeq = -1; dropped = 0; },
173
+ };
174
+ };
175
+
176
+ /**
177
+ * Declarative state replication (host side). Mutate the plain object you pass
178
+ * in; the SDK auto-diffs and broadcasts it every tick (sequence-guarded,
179
+ * delta-compressed, optional quantize/binary). Clients read it with
180
+ * game.replica(). This is the ~40-lines-into-2 ergonomic.
181
+ * @returns {{ state:any, flush:()=>void, start:()=>void, stop:()=>void }}
182
+ */
183
+ game.replicate = function (obj, opts) {
184
+ opts = opts || {};
185
+ let state = obj != null ? obj : {};
186
+ const sender = this.createSnapshotSender(Object.assign({}, opts, { source: function () { return state; } }));
187
+ return {
188
+ get state() { return state; },
189
+ set state(v) { state = v; },
190
+ flush: function () { sender.flush(); },
191
+ start: function () { sender.start(); },
192
+ stop: function () { sender.stop(); },
193
+ };
194
+ };
195
+
196
+ /**
197
+ * Declarative state replication (client side). Receives what a host
198
+ * replicate()s on the same channel, drops stale/out-of-order frames, and
199
+ * (optionally) interpolates. Read `.state` for the latest authoritative state,
200
+ * or `.view()` for smoothed entities when `interpolate` keys are given.
201
+ * @returns {{ state:any, view:()=>any, onChange:(cb)=>void, stop:()=>void }}
202
+ */
203
+ game.replica = function (opts) {
204
+ opts = opts || {};
205
+ const channel = opts.channel || 'state';
206
+ const rx = this.createSnapshotReceiver({ decode: opts.decode });
207
+ const interp = opts.interpolate ? this.createInterpolation(typeof opts.interpolate === 'object' ? opts.interpolate : {}) : null;
208
+ const keys = typeof opts.interpolate === 'string' ? opts.interpolate : (opts.keys || (opts.interpolate && opts.interpolate.keys));
209
+ const group = opts.group || (opts.interpolate && opts.interpolate.group);
210
+ let onChange = null;
211
+ const off = this._onChannel(channel, function (data) {
212
+ const next = rx.receive(data);
213
+ if (interp && next != null) interp.add(next);
214
+ if (onChange) onChange(next);
215
+ });
216
+ return {
217
+ get state() { return rx.state; },
218
+ view: function () { return interp ? interp.calc(keys || 'x y', group) : rx.state; },
219
+ onChange: function (cb) { onChange = cb; },
220
+ stop: function () { off(); },
221
+ };
222
+ };
223
+
224
+ /**
225
+ * One-line WebRTC peer-to-peer setup (2 peers). Signaling rides the realtime
226
+ * channel via the router; gameplay then flows directly peer-to-peer over UDP.
227
+ */
228
+ game.createMesh = function (opts) {
229
+ opts = opts || {};
230
+ const self = this;
231
+ const channel = opts.signalChannel || 'signal';
232
+ const mesh = new MeshConnection(Object.assign({}, opts, {
233
+ sendSignal: function (payload) { self.realtime(channel, payload); },
234
+ }));
235
+ this._onChannel(channel, function (payload) { if (payload && payload.type) mesh.handleSignal(payload); });
236
+ return mesh;
237
+ };
238
+
239
+ /**
240
+ * N-peer full mesh. Signaling routed per-peer over the realtime channel (each
241
+ * message carries {to, from}); messages addressed to us are dispatched to the
242
+ * right peer connection. selfId defaults to the current user id.
243
+ * @returns {MeshNetwork}
244
+ */
245
+ game.createMeshNetwork = function (opts) {
246
+ opts = opts || {};
247
+ const self = this;
248
+ const channel = opts.signalChannel || 'mesh';
249
+ const selfId = opts.selfId || (Usion.user && Usion.user.getId && Usion.user.getId());
250
+ const net = new MeshNetwork(Object.assign({}, opts, {
251
+ selfId: selfId,
252
+ sendSignal: function (toPeerId, payload) { self.realtime(channel, { to: toPeerId, from: selfId, payload: payload }); },
253
+ }));
254
+ this._onChannel(channel, function (ad) { if (ad && ad.to === selfId && ad.from) net.handleSignal(ad.from, ad.payload); });
255
+ return net;
256
+ };
257
+
258
+ /**
259
+ * Inject artificial latency / jitter / packet loss locally to test how the
260
+ * game feels on a bad network (every studio has this). Affects both outbound
261
+ * realtime sends and inbound realtime dispatch. Call with null/false to turn
262
+ * it off. No-op safety: realtime must exist on the game module.
263
+ * @param {{latencyMs?:number,jitterMs?:number,lossPct?:number,dupPct?:number}|null} opts
264
+ */
265
+ game.simulateNetwork = function (opts) {
266
+ const self = this;
267
+ self._installRealtimeRouter();
268
+ if (!self._realtimeRaw && typeof self.realtime === 'function') self._realtimeRaw = self.realtime.bind(self);
269
+ if (!opts) {
270
+ if (self._realtimeRaw) self.realtime = self._realtimeRaw;
271
+ self._inboundDispatch = null;
272
+ if (self._sim) self._sim.flush();
273
+ self._sim = null;
274
+ return;
275
+ }
276
+ if (!self._sim) self._sim = new NetworkSim(opts);
277
+ else self._sim.set(opts);
278
+ if (self._realtimeRaw) self.realtime = self._sim.wrap(self._realtimeRaw);
279
+ self._inboundDispatch = self._sim.wrap(self._dispatchRealtime);
280
+ return self._sim;
281
+ };
282
+
283
+ /**
284
+ * Create a WebTransport (HTTP/3) connection — the lowest-latency client-server
285
+ * path (UDP-like datagrams, no TCP head-of-line blocking). `url` defaults to
286
+ * Usion.config.webTransportUrl. Call `connect()`, then `send()` (datagram) /
287
+ * `sendReliable()`; feed `onMessage` into a snapshot receiver. Direct/standalone
288
+ * games only (the iframe/WebView proxy relay can't carry datagrams).
289
+ * @returns {WebTransportConnection}
290
+ */
291
+ game.createWebTransport = function (opts) {
292
+ opts = opts || {};
293
+ const url = opts.url || (Usion.config && (Usion.config.webTransportUrl || Usion.config.wtUrl));
294
+ return new WebTransportConnection(Object.assign({}, opts, { url: url }));
295
+ };
296
+
297
+ /**
298
+ * Measure round-trip time once (single outstanding probe), updating the
299
+ * rolling estimate (game.getRtt). Direct mode uses the protocol ping/pong;
300
+ * platform mode uses a lightweight server ack. Resolves to ms, or null if
301
+ * unavailable (proxy mode, or not connected).
302
+ * @returns {Promise<number|null>}
303
+ */
304
+ game.ping = function () {
305
+ const self = this;
306
+ if (!self._pingMeter) self._pingMeter = new PingMeter();
307
+ if (self._pingInFlight) return self._pingInFlight; // single outstanding probe
308
+
309
+ let promise;
310
+ if (self.directMode) {
311
+ if (!self.directSocket || self.directSocket.readyState !== 1) return Promise.resolve(null);
312
+ promise = new Promise(function (resolve) {
313
+ const id = self._pingMeter.begin();
314
+ let done = false;
315
+ const finish = function () { if (done) return; done = true; resolve(self._pingMeter.end(id)); };
316
+ self._pongWaiters.push(finish);
317
+ self._sendDirect('ping', { t: Date.now() });
318
+ setTimeout(function () {
319
+ if (done) return; done = true;
320
+ const idx = self._pongWaiters.indexOf(finish);
321
+ if (idx >= 0) self._pongWaiters.splice(idx, 1);
322
+ delete self._pingMeter._outstanding[id];
323
+ resolve(null);
324
+ }, 3000);
325
+ });
326
+ } else if (self.socket && self.connected && !self._useProxy) {
327
+ promise = new Promise(function (resolve) {
328
+ const start = Date.now();
329
+ let done = false;
330
+ try {
331
+ self.socket.emit('game:ping', { t: start }, function () { if (done) return; done = true; resolve(self._pingMeter.sample(Date.now() - start)); });
332
+ } catch (e) { resolve(null); return; }
333
+ setTimeout(function () { if (done) return; done = true; resolve(null); }, 3000);
334
+ });
335
+ } else {
336
+ return Promise.resolve(null); // proxy mode has no point-to-point ack
337
+ }
338
+
339
+ self._pingInFlight = promise;
340
+ promise.then(function () { self._pingInFlight = null; }, function () { self._pingInFlight = null; });
341
+ return promise;
342
+ };
343
+
344
+ /** Latest smoothed round-trip time in ms (null until first ping). */
345
+ game.getRtt = function () { return this._pingMeter ? this._pingMeter.rtt : null; };
346
+ }
@@ -2,6 +2,8 @@
2
2
  * Usion SDK Game Proxy — postMessage relay through parent app
3
3
  */
4
4
 
5
+ import { isTrustedMessageSource } from './core.js';
6
+
5
7
  /**
6
8
  * Add proxy connection methods to game module
7
9
  * @param {object} game - The game module object
@@ -58,6 +60,8 @@ export function applyGameProxy(game, Usion) {
58
60
  self._proxyListenerSetup = true;
59
61
 
60
62
  window.addEventListener('message', function(event) {
63
+ if (!isTrustedMessageSource(event)) return;
64
+
61
65
  var data;
62
66
  try {
63
67
  data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
@@ -40,6 +40,10 @@ export function applyGameSocket(game, Usion) {
40
40
  reconnectionDelayMax: 10000
41
41
  });
42
42
 
43
+ // Route allow-listed backend server events (lobby, etc.) to the unified
44
+ // backend channel — robust to listener registration order.
45
+ if (Usion._bindBackendSocket) Usion._bindBackendSocket(self.socket);
46
+
43
47
  self.socket.on('connect', function() {
44
48
  self.connected = true;
45
49
  self._connecting = false;
@@ -25,6 +25,11 @@ import { uiMethods } from './ui.js';
25
25
  import { miscMethods } from './misc.js';
26
26
  import { createFileStorageModule } from './file-storage.js';
27
27
  import { createGameModule } from './game-core.js';
28
+ import { createLobbyModule } from './lobby.js';
29
+ import { createLeaderboardModule } from './leaderboard.js';
30
+ import { createMatchmakingModule } from './matchmaking.js';
31
+ import { applyBackendChannel } from './backend-channel.js';
32
+ import { netcode } from './netcode/index.js';
28
33
 
29
34
  // Build the Usion object from core
30
35
  const Usion = Object.assign({}, core);
@@ -38,6 +43,14 @@ Usion.chat = createChatModule(Usion);
38
43
  Usion.bot = createBotModule(Usion);
39
44
  Usion.fileStorage = createFileStorageModule(Usion);
40
45
  Usion.game = createGameModule(Usion);
46
+ // Unified backend channel (used by lobby etc.; works standalone + embedded).
47
+ applyBackendChannel(Usion);
48
+ Usion.lobby = createLobbyModule(Usion);
49
+ Usion.leaderboard = createLeaderboardModule(Usion);
50
+ Usion.matchmaking = createMatchmakingModule(Usion);
51
+
52
+ // Netcode toolkit (transport-agnostic, zero-dependency).
53
+ Usion.netcode = netcode;
41
54
 
42
55
  // Attach results methods directly on Usion
43
56
  Object.assign(Usion, createResultsMethods(Usion));
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Usion SDK Leaderboard — scores for mini-games.
3
+ *
4
+ * Opt-in per game (the service must have `leaderboard.enabled`). Rides the
5
+ * unified backend channel, so it works in standalone AND embedded games.
6
+ *
7
+ * await Usion.leaderboard.submit(1500); // your score
8
+ * const friends = await Usion.leaderboard.friends(); // people you've messaged + you
9
+ * const top = await Usion.leaderboard.top(); // global top N
10
+ * const me = await Usion.leaderboard.me(); // { score, rank, total }
11
+ *
12
+ * The **friends** board is scoped to users you've messaged (your conversations)
13
+ * plus yourself — that's the "see the scores of people I've chatted with" view.
14
+ * Entries: { user_id, name, avatar, score, rank, is_me, metadata }.
15
+ */
16
+ export function createLeaderboardModule(Usion) {
17
+ function serviceId(opts) {
18
+ return (opts && opts.serviceId) || (Usion.config && Usion.config.serviceId);
19
+ }
20
+
21
+ return {
22
+ /** Submit a score (best score is kept, per the service's order config). */
23
+ submit: function (score, metadata, opts) {
24
+ return Usion._backendEmit('lb:submit', { service_id: serviceId(opts), score: score, metadata: metadata != null ? metadata : null });
25
+ },
26
+
27
+ /** Leaderboard of people you've messaged (plus you). Returns entries[]. */
28
+ friends: function (opts) {
29
+ opts = opts || {};
30
+ return Usion._backendEmit('lb:friends', { service_id: serviceId(opts), limit: opts.limit || 50 })
31
+ .then(function (r) { return (r && r.entries) || []; });
32
+ },
33
+
34
+ /** Global top N. Returns entries[]. */
35
+ top: function (opts) {
36
+ opts = opts || {};
37
+ return Usion._backendEmit('lb:top', { service_id: serviceId(opts), limit: opts.limit || 20 })
38
+ .then(function (r) { return (r && r.entries) || []; });
39
+ },
40
+
41
+ /** Your own score + global rank: { score, rank, total }. */
42
+ me: function (opts) {
43
+ return Usion._backendEmit('lb:me', { service_id: serviceId(opts) });
44
+ },
45
+ };
46
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Usion SDK Lobby — parties, ready-up, and matchmaking.
3
+ *
4
+ * The social rendezvous layer that game rooms can't provide: form a party by
5
+ * code, ready up together, and start as a group — plus a thin wrapper over the
6
+ * platform's stranger matchmaking. Rides the platform Socket.IO connection
7
+ * (Usion.game.socket), so connect first with Usion.game.connect().
8
+ *
9
+ * const { code } = await Usion.lobby.create({ maxPlayers: 4 });
10
+ * Usion.lobby.onUpdate(({ members }) => renderLobby(members));
11
+ * await Usion.lobby.setReady(true);
12
+ * // host, once everyone's ready: create a room then start the party in it
13
+ * const room = await Usion.lobby.queue(serviceId); // or your own room API
14
+ * await Usion.lobby.start(room.id);
15
+ * Usion.lobby.onStarted(({ room_id }) => Usion.game.join(room_id));
16
+ *
17
+ * Works in every mode: it rides the unified backend channel (Usion._backendEmit
18
+ * / _backendOn), which uses the SDK's own socket when standalone and relays
19
+ * through the parent app when embedded (iframe/WebView).
20
+ */
21
+ export function createLobbyModule(Usion) {
22
+ const state = { code: null, host: null, status: null, members: [] };
23
+ const handlers = {};
24
+ let bound = false;
25
+
26
+ function bind() {
27
+ if (bound) return;
28
+ bound = true;
29
+ Usion._backendOn('lobby:update', function (d) {
30
+ state.code = d.code; state.host = d.host; state.status = d.status; state.members = d.members || [];
31
+ if (handlers.update) handlers.update(d);
32
+ });
33
+ Usion._backendOn('lobby:started', function (d) {
34
+ state.status = 'started';
35
+ if (handlers.started) handlers.started(d);
36
+ });
37
+ }
38
+
39
+ function ack(event, data) {
40
+ bind();
41
+ return Usion._backendEmit(event, data);
42
+ }
43
+
44
+ return {
45
+ get state() { return state; },
46
+
47
+ /** Register a handler for lobby membership/ready changes. */
48
+ onUpdate: function (cb) { handlers.update = cb; bind(); },
49
+ /** Register a handler for when the host starts the party. */
50
+ onStarted: function (cb) { handlers.started = cb; bind(); },
51
+
52
+ /** Create a party. Resolves with { code }. You become the host. */
53
+ create: async function (opts) {
54
+ opts = opts || {};
55
+ const r = await ack('lobby:create', { max_players: opts.maxPlayers || 8, public: !!opts.public });
56
+ if (r && r.code) state.code = r.code;
57
+ return r;
58
+ },
59
+
60
+ /** Join a party by code. */
61
+ join: async function (code) {
62
+ const r = await ack('lobby:join', { code: String(code || '').toUpperCase() });
63
+ if (r && r.code) state.code = r.code;
64
+ return r;
65
+ },
66
+
67
+ /** Leave the current party. */
68
+ leave: function () { state.code = null; return ack('lobby:leave', {}).catch(function () {}); },
69
+
70
+ /** Set your ready state. */
71
+ setReady: function (ready) { return ack('lobby:ready', { ready: ready !== false }); },
72
+
73
+ /** True when every member is ready. */
74
+ allReady: function () { return state.members.length > 0 && state.members.every(function (m) { return m.ready; }); },
75
+
76
+ /** Whether the current user is the party host. */
77
+ isHost: function () {
78
+ const id = Usion.user && Usion.user.getId && Usion.user.getId();
79
+ return !!(state.host && id && state.host === id);
80
+ },
81
+
82
+ /** Host: start the party in an already-created game room. */
83
+ start: function (roomId) { return ack('lobby:start', { room_id: roomId }); },
84
+
85
+ /**
86
+ * Stranger matchmaking — find or create a game room for a service via the
87
+ * platform's REST matchmaker. Resolves with the room.
88
+ */
89
+ queue: async function (serviceId, opts) {
90
+ opts = opts || {};
91
+ const apiUrl = (Usion.config && Usion.config.apiUrl) || '';
92
+ const token = Usion.user && Usion.user.getToken && Usion.user.getToken();
93
+ if (!apiUrl) throw new Error('No apiUrl configured');
94
+ const res = await fetch(apiUrl.replace(/\/$/, '') + '/games/matchmake', {
95
+ method: 'POST',
96
+ headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
97
+ body: JSON.stringify({ service_id: serviceId, conversation_id: opts.conversationId || ('standalone_' + serviceId) }),
98
+ });
99
+ if (!res.ok) throw new Error('Matchmake failed: HTTP ' + res.status);
100
+ return res.json();
101
+ },
102
+ };
103
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Usion SDK Matchmaking — pair up with online strangers ("quick match").
3
+ *
4
+ * Lets a mini-app connect players who don't know each other: join a queue, and
5
+ * when enough players are waiting the platform creates a room and matches you.
6
+ * Rides the unified backend channel, so it works standalone AND embedded.
7
+ *
8
+ * const m = await Usion.matchmaking.find(); // resolves when matched
9
+ * await Usion.game.connect(); await Usion.game.join(m.roomId);
10
+ * // ...or cancel while waiting:
11
+ * Usion.matchmaking.cancel();
12
+ * Usion.matchmaking.onMatch(({ roomId, players }) => { ... });
13
+ */
14
+ export function createMatchmakingModule(Usion) {
15
+ let pending = null; // { resolve, reject } for an in-flight find()
16
+ let onMatchCb = null;
17
+ let bound = false;
18
+
19
+ function normalize(d) {
20
+ return { roomId: d && d.room_id, players: (d && d.player_ids) || [], serviceId: d && d.service_id };
21
+ }
22
+
23
+ function bind() {
24
+ if (bound) return;
25
+ bound = true;
26
+ Usion._backendOn('mm:matched', function (d) {
27
+ const r = normalize(d);
28
+ if (onMatchCb) onMatchCb(r);
29
+ if (pending) { const p = pending; pending = null; p.resolve(r); }
30
+ });
31
+ }
32
+
33
+ return {
34
+ /** Register a handler called whenever a match is found. */
35
+ onMatch: function (cb) { onMatchCb = cb; bind(); },
36
+
37
+ /**
38
+ * Join the queue for `serviceId` (defaults to the current game) and resolve
39
+ * when matched with { roomId, players, serviceId }. Stays pending until a
40
+ * match (use cancel() to stop waiting).
41
+ */
42
+ find: function (serviceId, opts) {
43
+ opts = opts || {};
44
+ const sid = serviceId || (Usion.config && Usion.config.serviceId);
45
+ bind();
46
+ const self = this;
47
+ return Usion._backendEmit('mm:join', { service_id: sid, size: opts.size || 2 }).then(function () {
48
+ return new Promise(function (resolve, reject) {
49
+ if (pending) pending.reject(new Error('superseded'));
50
+ pending = { resolve: resolve, reject: reject };
51
+ });
52
+ });
53
+ },
54
+
55
+ /** Leave the queue / stop waiting. */
56
+ cancel: function () {
57
+ if (pending) { pending.reject(new Error('cancelled')); pending = null; }
58
+ return Usion._backendEmit('mm:cancel', {});
59
+ },
60
+ };
61
+ }
@@ -2,6 +2,8 @@
2
2
  * Usion SDK Misc — submit, error, exit, share, log, on, requestPayment (legacy)
3
3
  */
4
4
 
5
+ import { isTrustedMessageSource } from './core.js';
6
+
5
7
  export const miscMethods = {
6
8
  /**
7
9
  * Request payment from user (legacy method)
@@ -108,6 +110,8 @@ export const miscMethods = {
108
110
  */
109
111
  on: function(type, callback) {
110
112
  window.addEventListener('message', function(event) {
113
+ if (!isTrustedMessageSource(event)) return;
114
+
111
115
  let data;
112
116
  try {
113
117
  data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;