@usions/sdk 2.2.0 → 2.11.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/README.md +118 -0
- package/package.json +3 -2
- package/src/browser.js +2554 -122
- package/src/modules/backend-channel.js +76 -0
- package/src/modules/cloud.js +67 -0
- package/src/modules/core.js +6 -0
- package/src/modules/game-core.js +4 -0
- package/src/modules/game-direct.js +45 -0
- package/src/modules/game-netcode.js +346 -0
- package/src/modules/game-socket.js +4 -0
- package/src/modules/index.js +15 -0
- package/src/modules/leaderboard.js +46 -0
- package/src/modules/lobby.js +103 -0
- package/src/modules/matchmaking.js +61 -0
- package/src/modules/netcode/binary.js +113 -0
- package/src/modules/netcode/delta.js +236 -0
- package/src/modules/netcode/index.js +41 -0
- package/src/modules/netcode/interpolation.js +235 -0
- package/src/modules/netcode/lagcomp.js +104 -0
- package/src/modules/netcode/lockstep.js +103 -0
- package/src/modules/netcode/mesh-network.js +103 -0
- package/src/modules/netcode/mesh.js +188 -0
- package/src/modules/netcode/netsim.js +77 -0
- package/src/modules/netcode/ping.js +69 -0
- package/src/modules/netcode/prediction.js +113 -0
- package/src/modules/netcode/sender.js +102 -0
- package/src/modules/netcode/webtransport.js +195 -0
- package/types/index.d.ts +449 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usion SDK Netcode — WebTransport (HTTP/3 / QUIC) client transport.
|
|
3
|
+
*
|
|
4
|
+
* The lowest-latency *client-server* path for browser games (Baseline across
|
|
5
|
+
* major browsers since 2026). Unlike WebSocket (TCP → head-of-line blocking)
|
|
6
|
+
* it offers UDP-like unreliable **datagrams** — the newest snapshot always
|
|
7
|
+
* gets through — plus reliable **streams** for must-arrive events, over a
|
|
8
|
+
* single QUIC connection, without WebRTC's ICE/STUN/TURN/SDP complexity.
|
|
9
|
+
*
|
|
10
|
+
* Uses the native `WebTransport` API (zero dependency). On the server, pair
|
|
11
|
+
* with an HTTP/3 server such as the open-source `@fails-components/webtransport`
|
|
12
|
+
* (Node). Same interface shape as MeshConnection so it drops into the same
|
|
13
|
+
* snapshot sender/receiver + interpolation pipeline.
|
|
14
|
+
*
|
|
15
|
+
* Framing:
|
|
16
|
+
* - datagram = [seq:uint32 BE][type:uint8][payload] (one datagram = one msg)
|
|
17
|
+
* - stream = [len:uint32 BE][type:uint8][payload] (len = 1 + payload bytes)
|
|
18
|
+
* - type 0 = binary (Uint8Array), 1 = JSON (utf8). Sequenced datagrams drop
|
|
19
|
+
* stale/out-of-order frames (receiver only advances).
|
|
20
|
+
*/
|
|
21
|
+
const _enc = typeof TextEncoder !== 'undefined' ? new TextEncoder() : null;
|
|
22
|
+
const _dec = typeof TextDecoder !== 'undefined' ? new TextDecoder() : null;
|
|
23
|
+
|
|
24
|
+
function toBytes(s) { return _enc ? _enc.encode(s) : Uint8Array.from(Buffer.from(s, 'utf8')); }
|
|
25
|
+
function fromBytes(b) { return _dec ? _dec.decode(b) : Buffer.from(b).toString('utf8'); }
|
|
26
|
+
function isBinary(d) { return d instanceof ArrayBuffer || ArrayBuffer.isView(d); }
|
|
27
|
+
|
|
28
|
+
/** Encode a value to { type, bytes }: binary passthrough or JSON utf8. */
|
|
29
|
+
function encodePayload(data) {
|
|
30
|
+
if (isBinary(data)) return { type: 0, bytes: data instanceof Uint8Array ? data : new Uint8Array(data.buffer || data) };
|
|
31
|
+
return { type: 1, bytes: toBytes(JSON.stringify(data)) };
|
|
32
|
+
}
|
|
33
|
+
function decodePayload(type, bytes) {
|
|
34
|
+
if (type === 1) { try { return JSON.parse(fromBytes(bytes)); } catch (_) { return null; } }
|
|
35
|
+
return bytes;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** datagram frame: [seq:4][type:1][payload] */
|
|
39
|
+
export function encodeDatagram(seq, data) {
|
|
40
|
+
const { type, bytes } = encodePayload(data);
|
|
41
|
+
const out = new Uint8Array(5 + bytes.length);
|
|
42
|
+
new DataView(out.buffer).setUint32(0, seq >>> 0, false);
|
|
43
|
+
out[4] = type;
|
|
44
|
+
out.set(bytes, 5);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
export function decodeDatagram(frame) {
|
|
48
|
+
const b = frame instanceof Uint8Array ? frame : new Uint8Array(frame);
|
|
49
|
+
if (b.length < 5) return null;
|
|
50
|
+
const seq = new DataView(b.buffer, b.byteOffset, b.byteLength).getUint32(0, false);
|
|
51
|
+
return { seq: seq, value: decodePayload(b[4], b.subarray(5)) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** stream frame: [len:4][type:1][payload], len = 1 + payload length */
|
|
55
|
+
export function encodeStreamFrame(data) {
|
|
56
|
+
const { type, bytes } = encodePayload(data);
|
|
57
|
+
const out = new Uint8Array(4 + 1 + bytes.length);
|
|
58
|
+
new DataView(out.buffer).setUint32(0, 1 + bytes.length, false);
|
|
59
|
+
out[4] = type;
|
|
60
|
+
out.set(bytes, 5);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Stateful deframer for the reliable byte stream. push(bytes) → [values]. */
|
|
65
|
+
export class StreamDeframer {
|
|
66
|
+
constructor() { this._buf = new Uint8Array(0); }
|
|
67
|
+
push(chunk) {
|
|
68
|
+
const c = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
|
|
69
|
+
const merged = new Uint8Array(this._buf.length + c.length);
|
|
70
|
+
merged.set(this._buf, 0); merged.set(c, this._buf.length);
|
|
71
|
+
this._buf = merged;
|
|
72
|
+
const out = [];
|
|
73
|
+
while (this._buf.length >= 4) {
|
|
74
|
+
const len = new DataView(this._buf.buffer, this._buf.byteOffset, 4).getUint32(0, false);
|
|
75
|
+
if (this._buf.length < 4 + len) break;
|
|
76
|
+
const type = this._buf[4];
|
|
77
|
+
const payload = this._buf.subarray(5, 4 + len);
|
|
78
|
+
out.push(decodePayload(type, payload.slice()));
|
|
79
|
+
this._buf = this._buf.subarray(4 + len);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class WebTransportConnection {
|
|
86
|
+
/**
|
|
87
|
+
* @param {object} opts
|
|
88
|
+
* @param {string} opts.url https:// HTTP/3 endpoint
|
|
89
|
+
* @param {Array} [opts.serverCertificateHashes] for self-signed dev certs
|
|
90
|
+
* @param {boolean}[opts.sequenced=true] drop stale datagrams
|
|
91
|
+
* @param {Function}[opts.WebTransport] injectable for tests
|
|
92
|
+
*/
|
|
93
|
+
constructor(opts = {}) {
|
|
94
|
+
if (!opts.url) throw new Error('WebTransportConnection requires a url');
|
|
95
|
+
this._url = opts.url;
|
|
96
|
+
this._opts = opts;
|
|
97
|
+
this._sequenced = opts.sequenced !== false;
|
|
98
|
+
this._WT = opts.WebTransport || (typeof WebTransport !== 'undefined' ? WebTransport : null);
|
|
99
|
+
|
|
100
|
+
this.connected = false;
|
|
101
|
+
this._t = null;
|
|
102
|
+
this._dgWriter = null;
|
|
103
|
+
this._streamWriter = null;
|
|
104
|
+
this._deframer = new StreamDeframer();
|
|
105
|
+
this._sendSeq = 0;
|
|
106
|
+
this._recvSeq = 0;
|
|
107
|
+
|
|
108
|
+
this.onOpen = null; // () => void
|
|
109
|
+
this.onMessage = null; // (data, channel:'datagram'|'reliable') => void
|
|
110
|
+
this.onClose = null; // () => void
|
|
111
|
+
this.onError = null; // (err) => void
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async connect() {
|
|
115
|
+
if (!this._WT) throw new Error('WebTransport unavailable in this environment');
|
|
116
|
+
const init = {};
|
|
117
|
+
if (this._opts.serverCertificateHashes) init.serverCertificateHashes = this._opts.serverCertificateHashes;
|
|
118
|
+
const t = new this._WT(this._url, init);
|
|
119
|
+
this._t = t;
|
|
120
|
+
if (t.ready && typeof t.ready.then === 'function') await t.ready;
|
|
121
|
+
|
|
122
|
+
if (t.datagrams && t.datagrams.writable) this._dgWriter = t.datagrams.writable.getWriter();
|
|
123
|
+
if (typeof t.createBidirectionalStream === 'function') {
|
|
124
|
+
try {
|
|
125
|
+
const s = await t.createBidirectionalStream();
|
|
126
|
+
this._streamWriter = s.writable.getWriter();
|
|
127
|
+
this._pumpReadable(s.readable, 'reliable');
|
|
128
|
+
} catch (e) { /* reliable stream optional */ }
|
|
129
|
+
}
|
|
130
|
+
if (t.datagrams && t.datagrams.readable) this._pumpDatagrams(t.datagrams.readable);
|
|
131
|
+
if (t.closed && typeof t.closed.then === 'function') t.closed.then(() => this._onClosed(), () => this._onClosed());
|
|
132
|
+
|
|
133
|
+
this.connected = true;
|
|
134
|
+
if (this.onOpen) this.onOpen();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async _pumpDatagrams(readable) {
|
|
138
|
+
try {
|
|
139
|
+
const reader = readable.getReader();
|
|
140
|
+
for (;;) {
|
|
141
|
+
const { value, done } = await reader.read();
|
|
142
|
+
if (done) break;
|
|
143
|
+
if (value) this._onDatagramBytes(value);
|
|
144
|
+
}
|
|
145
|
+
} catch (e) { if (this.onError) this.onError(e); }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async _pumpReadable(readable, channel) {
|
|
149
|
+
try {
|
|
150
|
+
const reader = readable.getReader();
|
|
151
|
+
for (;;) {
|
|
152
|
+
const { value, done } = await reader.read();
|
|
153
|
+
if (done) break;
|
|
154
|
+
if (value) { const msgs = this._deframer.push(value); for (let i = 0; i < msgs.length; i++) if (this.onMessage) this.onMessage(msgs[i], channel); }
|
|
155
|
+
}
|
|
156
|
+
} catch (e) { if (this.onError) this.onError(e); }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Handle one inbound datagram frame (also the test entry point). */
|
|
160
|
+
_onDatagramBytes(frame) {
|
|
161
|
+
const parsed = decodeDatagram(frame);
|
|
162
|
+
if (!parsed) return;
|
|
163
|
+
if (this._sequenced) {
|
|
164
|
+
if (parsed.seq <= this._recvSeq) return; // stale / out-of-order
|
|
165
|
+
this._recvSeq = parsed.seq;
|
|
166
|
+
}
|
|
167
|
+
if (this.onMessage) this.onMessage(parsed.value, 'datagram');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Send over the unreliable datagram channel (sequenced). */
|
|
171
|
+
send(data) {
|
|
172
|
+
if (!this._dgWriter) return false;
|
|
173
|
+
this._sendSeq += 1;
|
|
174
|
+
try { this._dgWriter.write(encodeDatagram(this._sendSeq, data)); return true; }
|
|
175
|
+
catch (e) { return false; }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Send over the reliable ordered stream. */
|
|
179
|
+
sendReliable(data) {
|
|
180
|
+
if (!this._streamWriter) return false;
|
|
181
|
+
try { this._streamWriter.write(encodeStreamFrame(data)); return true; }
|
|
182
|
+
catch (e) { return false; }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_onClosed() {
|
|
186
|
+
if (this.connected) { this.connected = false; if (this.onClose) this.onClose(); }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
close() {
|
|
190
|
+
try { if (this._dgWriter) this._dgWriter.releaseLock && this._dgWriter.releaseLock(); } catch (_) {}
|
|
191
|
+
try { if (this._t) this._t.close(); } catch (_) {}
|
|
192
|
+
this._t = this._dgWriter = this._streamWriter = null;
|
|
193
|
+
this.connected = false;
|
|
194
|
+
}
|
|
195
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface UsionConfig {
|
|
|
16
16
|
theme?: 'light' | 'dark';
|
|
17
17
|
language?: string;
|
|
18
18
|
socketUrl?: string;
|
|
19
|
+
/** HTTP/3 WebTransport endpoint for direct-mode games (lowest latency). */
|
|
20
|
+
webTransportUrl?: string;
|
|
19
21
|
roomId?: string;
|
|
20
22
|
playerIds?: string[];
|
|
21
23
|
serviceId?: string;
|
|
@@ -241,6 +243,400 @@ export interface GameModule {
|
|
|
241
243
|
|
|
242
244
|
// Generic event listener
|
|
243
245
|
on(event: string, callback: (data: any) => void): void;
|
|
246
|
+
|
|
247
|
+
// ─── Netcode helpers (low-latency realtime) ──────────────────
|
|
248
|
+
// JSON state delta compression.
|
|
249
|
+
diff(prev: any, next: any): any | undefined;
|
|
250
|
+
patch(base: any, delta: any): any;
|
|
251
|
+
/** Round numeric fields to a fixed precision (snapshot compression). */
|
|
252
|
+
quantize<T = any>(value: T, precision?: number): T;
|
|
253
|
+
/** Compact binary codec (use on direct/mesh transports). */
|
|
254
|
+
encode(value: any): Uint8Array;
|
|
255
|
+
decode(buf: ArrayBuffer | Uint8Array): any;
|
|
256
|
+
|
|
257
|
+
/** Snapshot interpolation buffer (adaptive buffer + capped extrapolation). */
|
|
258
|
+
createInterpolation(opts?: InterpolationOptions): SnapshotInterpolation;
|
|
259
|
+
/** Client-side prediction + reconciliation + error smoothing. */
|
|
260
|
+
createPredictor<S = any, I = any>(opts: PredictorOptions<S, I>): Predictor<S, I>;
|
|
261
|
+
/** Fixed-rate outbound coalescer (defaults to sending via game.realtime). */
|
|
262
|
+
createSender(opts?: SenderOptions): Coalescer;
|
|
263
|
+
/** Sequence-guarded, delta-compressed snapshot sender. Pair with createSnapshotReceiver. */
|
|
264
|
+
createSnapshotSender(opts?: SnapshotSenderOptions): SnapshotSender;
|
|
265
|
+
/** Receiver for createSnapshotSender (drops stale/out-of-order frames). */
|
|
266
|
+
createSnapshotReceiver(opts?: SnapshotReceiverOptions): SnapshotReceiver;
|
|
267
|
+
/** One-line WebRTC P2P (2-peer) setup; signaling over realtime. */
|
|
268
|
+
createMesh(opts: MeshOptions): MeshConnection;
|
|
269
|
+
/** N-peer full mesh; signaling routed per-peer over realtime. */
|
|
270
|
+
createMeshNetwork(opts?: MeshNetworkOptions): MeshNetwork;
|
|
271
|
+
/** WebTransport (HTTP/3) connection — lowest-latency client-server datagrams. */
|
|
272
|
+
createWebTransport(opts?: WebTransportOptions): WebTransportConnection;
|
|
273
|
+
/** Declarative replication (host): mutate the object, it auto-syncs. */
|
|
274
|
+
replicate<T = any>(obj: T, opts?: ReplicateOptions): Replicated<T>;
|
|
275
|
+
/** Declarative replication (client): receive + (optionally) interpolate. */
|
|
276
|
+
replica<S = any>(opts?: ReplicaOptions): Replica<S>;
|
|
277
|
+
/** Server-side lag compensation (history + rewind) for fair hit-resolution. */
|
|
278
|
+
createLagCompensator(opts?: LagCompensatorOptions): LagCompensator;
|
|
279
|
+
/** Deterministic lockstep (inputs-only sim + free replays). */
|
|
280
|
+
createLockstep(opts: LockstepOptions): Lockstep;
|
|
281
|
+
/** Inject latency/jitter/loss locally to test under bad networks (null = off). */
|
|
282
|
+
simulateNetwork(opts: NetworkSimOptions | null): NetworkSim | void;
|
|
283
|
+
|
|
284
|
+
/** Measure round-trip time once (single outstanding probe); ms or null. */
|
|
285
|
+
ping(): Promise<number | null>;
|
|
286
|
+
/** Latest smoothed round-trip time in ms (null until first ping). */
|
|
287
|
+
getRtt(): number | null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ─── Netcode toolkit ──────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
export interface NetcodeEntity {
|
|
293
|
+
id: string | number;
|
|
294
|
+
[key: string]: any;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export interface Snapshot {
|
|
298
|
+
id?: string;
|
|
299
|
+
time?: number;
|
|
300
|
+
state: NetcodeEntity[] | { [group: string]: NetcodeEntity[] };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export interface InterpolationOptions {
|
|
304
|
+
/** Expected snapshot rate; sets the default buffer (default 20). */
|
|
305
|
+
serverFps?: number;
|
|
306
|
+
/** Fixed interpolation delay in ms (default ≈ 3 server frames). */
|
|
307
|
+
bufferMs?: number;
|
|
308
|
+
/** Grow the buffer with measured jitter (Source cl_interp_ratio style). */
|
|
309
|
+
adaptive?: boolean;
|
|
310
|
+
/** Adaptive buffer clamps. */
|
|
311
|
+
minBufferMs?: number;
|
|
312
|
+
maxBufferMs?: number;
|
|
313
|
+
/** Max forward extrapolation on buffer underrun, ms (0 = off; Source caps ~250). */
|
|
314
|
+
extrapolationMs?: number;
|
|
315
|
+
/** Render against snapshot.time (server clock domain) instead of arrival time. */
|
|
316
|
+
serverTime?: boolean;
|
|
317
|
+
/** Snapshot history depth (default 120). */
|
|
318
|
+
maxSize?: number;
|
|
319
|
+
/** Clock source override (default Date.now). */
|
|
320
|
+
now?: () => number;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export class SnapshotInterpolation {
|
|
324
|
+
constructor(opts?: InterpolationOptions);
|
|
325
|
+
getBufferMs(): number;
|
|
326
|
+
setBufferMs(ms: number): void;
|
|
327
|
+
getJitter(): number;
|
|
328
|
+
add(snapshot: Snapshot | NetcodeEntity[] | { [group: string]: NetcodeEntity[] }): void;
|
|
329
|
+
/** Interpolated entities for the render instant, or null if no data yet. */
|
|
330
|
+
calc(keys: string, group?: string): NetcodeEntity[] | null;
|
|
331
|
+
vault: Vault;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export class Vault {
|
|
335
|
+
constructor(maxSize?: number);
|
|
336
|
+
add(snapshot: { time: number; state: any }): void;
|
|
337
|
+
readonly size: number;
|
|
338
|
+
setMaxSize(n: number): void;
|
|
339
|
+
latest(): { time: number; state: any } | null;
|
|
340
|
+
clear(): void;
|
|
341
|
+
straddle(time: number): [any, any];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export interface PredictorSmoothOptions {
|
|
345
|
+
keys?: string | string[];
|
|
346
|
+
rate?: number;
|
|
347
|
+
snapTo?: number;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export interface PredictorOptions<S = any, I = any> {
|
|
351
|
+
apply: (state: S, input: I) => S;
|
|
352
|
+
initialState?: S;
|
|
353
|
+
/** Smoothly blend correction errors instead of snapping (Overwatch style). */
|
|
354
|
+
smooth?: PredictorSmoothOptions | string;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export class Predictor<S = any, I = any> {
|
|
358
|
+
constructor(opts: PredictorOptions<S, I>);
|
|
359
|
+
readonly state: S;
|
|
360
|
+
readonly pending: Array<{ seq: number; input: I }>;
|
|
361
|
+
readonly lastSeq: number;
|
|
362
|
+
predict(input: I): { seq: number; state: S };
|
|
363
|
+
reconcile(serverState: S, ackedSeq: number): S;
|
|
364
|
+
/** Render state: corrected + decaying error offset. Call once per frame. */
|
|
365
|
+
view(rate?: number): S;
|
|
366
|
+
reset(state?: S): void;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export interface CoalescerOptions {
|
|
370
|
+
hz?: number;
|
|
371
|
+
onFlush: (entries: Array<{ type: string; data: any }>) => void;
|
|
372
|
+
autoStart?: boolean;
|
|
373
|
+
setInterval?: (fn: () => void, ms: number) => any;
|
|
374
|
+
clearInterval?: (handle: any) => void;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export class Coalescer {
|
|
378
|
+
constructor(opts: CoalescerOptions);
|
|
379
|
+
readonly running: boolean;
|
|
380
|
+
/** Latest-wins: only the newest data for `type` is sent next flush. */
|
|
381
|
+
queue(type: string, data: any): void;
|
|
382
|
+
/** Buffered: every value for `type` is kept and sent next flush. */
|
|
383
|
+
append(type: string, data: any): void;
|
|
384
|
+
drain(): Array<{ type: string; data: any }>;
|
|
385
|
+
flush(): void;
|
|
386
|
+
start(): void;
|
|
387
|
+
stop(): void;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export interface SenderOptions {
|
|
391
|
+
hz?: number;
|
|
392
|
+
autoStart?: boolean;
|
|
393
|
+
send?: (type: string, data: any) => void;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export interface SnapshotSenderOptions extends SenderOptions {
|
|
397
|
+
/** realtime action_type to send under (default 'state'). */
|
|
398
|
+
channel?: string;
|
|
399
|
+
/** Use delta compression with periodic keyframes (default true). */
|
|
400
|
+
delta?: boolean;
|
|
401
|
+
/** Send a full keyframe every N deltas so lost deltas self-heal (default 30). */
|
|
402
|
+
keyframeEvery?: number;
|
|
403
|
+
/** Quantize numeric fields to this many decimals before diffing. */
|
|
404
|
+
precision?: number;
|
|
405
|
+
/** Binary-encode the wire payload: true (built-in) or a custom encoder. */
|
|
406
|
+
encode?: boolean | ((payload: any) => any);
|
|
407
|
+
/** Auto-read state each tick from this getter (basis for game.replicate). */
|
|
408
|
+
source?: () => any;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export interface ReplicateOptions extends SnapshotSenderOptions {}
|
|
412
|
+
|
|
413
|
+
export interface Replicated<T = any> {
|
|
414
|
+
state: T;
|
|
415
|
+
flush(): void;
|
|
416
|
+
start(): void;
|
|
417
|
+
stop(): void;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export interface ReplicaOptions {
|
|
421
|
+
channel?: string;
|
|
422
|
+
decode?: boolean | ((buf: any) => any);
|
|
423
|
+
/** Interpolation keys ('x y') or an InterpolationOptions object to smooth entities. */
|
|
424
|
+
interpolate?: string | InterpolationOptions;
|
|
425
|
+
keys?: string;
|
|
426
|
+
group?: string;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export interface Replica<S = any> {
|
|
430
|
+
readonly state: S;
|
|
431
|
+
view(): any;
|
|
432
|
+
onChange(cb: (state: S) => void): void;
|
|
433
|
+
stop(): void;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export interface NetworkSimOptions {
|
|
437
|
+
latencyMs?: number;
|
|
438
|
+
jitterMs?: number;
|
|
439
|
+
lossPct?: number;
|
|
440
|
+
dupPct?: number;
|
|
441
|
+
setTimeout?: (fn: () => void, ms: number) => any;
|
|
442
|
+
clearTimeout?: (handle: any) => void;
|
|
443
|
+
random?: () => number;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export class NetworkSim {
|
|
447
|
+
constructor(opts?: NetworkSimOptions);
|
|
448
|
+
latencyMs: number; jitterMs: number; lossPct: number; dupPct: number;
|
|
449
|
+
set(opts: NetworkSimOptions): NetworkSim;
|
|
450
|
+
wrap<F extends (...args: any[]) => any>(fn: F): F;
|
|
451
|
+
flush(): void;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export interface LockstepOptions {
|
|
455
|
+
playerId: string;
|
|
456
|
+
players: string[];
|
|
457
|
+
step: (frame: number, inputs: Record<string, any>) => void;
|
|
458
|
+
send?: (msg: { frame: number; playerId: string; input: any }) => void;
|
|
459
|
+
inputDelay?: number;
|
|
460
|
+
idleInput?: any;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export class Lockstep {
|
|
464
|
+
constructor(opts: LockstepOptions);
|
|
465
|
+
readonly frame: number;
|
|
466
|
+
readonly players: string[];
|
|
467
|
+
submit(input: any): number;
|
|
468
|
+
receive(msg: { frame: number; playerId: string; input: any }): void;
|
|
469
|
+
tick(): number;
|
|
470
|
+
isStalled(): boolean;
|
|
471
|
+
getReplay(): Array<{ frame: number; inputs: Record<string, any> }>;
|
|
472
|
+
static replay(log: Array<{ frame: number; inputs: Record<string, any> }>, step: (frame: number, inputs: Record<string, any>) => void): void;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export interface LagCompensatorOptions {
|
|
476
|
+
historyMs?: number;
|
|
477
|
+
maxSize?: number;
|
|
478
|
+
now?: () => number;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export class LagCompensator {
|
|
482
|
+
constructor(opts?: LagCompensatorOptions);
|
|
483
|
+
readonly size: number;
|
|
484
|
+
record(entities: Array<{ id: string | number; [k: string]: any }>, time?: number): void;
|
|
485
|
+
rewind(time: number, keys?: string[]): Record<string, any>;
|
|
486
|
+
rewindForClient(rttMs: number, interpBufferMs: number, opts?: { maxRewindMs?: number; keys?: string[] }): Record<string, any>;
|
|
487
|
+
clear(): void;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export interface SnapshotSender {
|
|
491
|
+
send(state: any): void;
|
|
492
|
+
flush(): void;
|
|
493
|
+
start(): void;
|
|
494
|
+
stop(): void;
|
|
495
|
+
reset(): void;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export interface SnapshotReceiverOptions {
|
|
499
|
+
/** Binary-decode incoming payloads: true (built-in) or a custom decoder. */
|
|
500
|
+
decode?: boolean | ((buf: any) => any);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export interface SnapshotReceiver {
|
|
504
|
+
/** Apply a message from the sender; returns the reconstructed state. */
|
|
505
|
+
receive<S = any>(msg: any): S;
|
|
506
|
+
readonly state: any;
|
|
507
|
+
readonly stats: { appliedSeq: number; baseSeq: number; dropped: number };
|
|
508
|
+
reset(): void;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export interface PingMeterOptions {
|
|
512
|
+
alpha?: number;
|
|
513
|
+
now?: () => number;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export class PingMeter {
|
|
517
|
+
constructor(opts?: PingMeterOptions);
|
|
518
|
+
readonly rtt: number | null;
|
|
519
|
+
readonly latency: number | null;
|
|
520
|
+
readonly jitter: number;
|
|
521
|
+
readonly last: number | null;
|
|
522
|
+
begin(): number;
|
|
523
|
+
end(id: number): number | null;
|
|
524
|
+
sample(rttMs: number): number | null;
|
|
525
|
+
reset(): void;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export interface IceServerConfig {
|
|
529
|
+
stun?: string;
|
|
530
|
+
turn?: string;
|
|
531
|
+
turnUsername?: string;
|
|
532
|
+
turnCredential?: string;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export interface MeshOptions {
|
|
536
|
+
role: 'host' | 'guest';
|
|
537
|
+
/** realtime action_type used for signaling (default 'signal'). */
|
|
538
|
+
signalChannel?: string;
|
|
539
|
+
iceServers?: RTCIceServer[];
|
|
540
|
+
/** Drop stale/out-of-order frames on the unreliable channel (default true). */
|
|
541
|
+
sequenced?: boolean;
|
|
542
|
+
/** Recover via ICE restart on connection failure (host; default true). */
|
|
543
|
+
autoReconnect?: boolean;
|
|
544
|
+
maxRestarts?: number;
|
|
545
|
+
RTCPeerConnection?: typeof RTCPeerConnection;
|
|
546
|
+
setTimeout?: (fn: () => void, ms: number) => any;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export class MeshConnection {
|
|
550
|
+
constructor(opts: MeshOptions & { sendSignal: (payload: any) => void });
|
|
551
|
+
/** Build an iceServers array with optional TURN relay. */
|
|
552
|
+
static iceServers(cfg?: IceServerConfig): RTCIceServer[];
|
|
553
|
+
readonly connected: boolean;
|
|
554
|
+
readonly role: 'host' | 'guest';
|
|
555
|
+
onOpen: (() => void) | null;
|
|
556
|
+
onMessage: ((data: any, channel: 'unreliable' | 'reliable') => void) | null;
|
|
557
|
+
onClose: (() => void) | null;
|
|
558
|
+
onError: ((err: any) => void) | null;
|
|
559
|
+
onStateChange: ((state: string) => void) | null;
|
|
560
|
+
start(): Promise<void>;
|
|
561
|
+
handleSignal(payload: any): Promise<void>;
|
|
562
|
+
send(data: any): boolean;
|
|
563
|
+
sendReliable(data: any): boolean;
|
|
564
|
+
close(): void;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export interface MeshNetworkOptions {
|
|
568
|
+
selfId?: string;
|
|
569
|
+
signalChannel?: string;
|
|
570
|
+
iceServers?: RTCIceServer[];
|
|
571
|
+
sequenced?: boolean;
|
|
572
|
+
autoReconnect?: boolean;
|
|
573
|
+
RTCPeerConnection?: typeof RTCPeerConnection;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export class MeshNetwork {
|
|
577
|
+
constructor(opts: MeshNetworkOptions & { selfId: string; sendSignal: (toPeerId: string, payload: any) => void });
|
|
578
|
+
readonly selfId: string;
|
|
579
|
+
readonly peerIds: string[];
|
|
580
|
+
readonly connectedCount: number;
|
|
581
|
+
peer(peerId: string): MeshConnection | null;
|
|
582
|
+
addPeer(peerId: string): Promise<MeshConnection | null>;
|
|
583
|
+
setRoster(peerIds: string[]): Promise<void>;
|
|
584
|
+
removePeer(peerId: string): void;
|
|
585
|
+
handleSignal(fromPeerId: string, payload: any): Promise<void>;
|
|
586
|
+
send(peerId: string, data: any): boolean;
|
|
587
|
+
sendReliable(peerId: string, data: any): boolean;
|
|
588
|
+
broadcast(data: any): void;
|
|
589
|
+
broadcastReliable(data: any): void;
|
|
590
|
+
onPeerOpen: ((peerId: string) => void) | null;
|
|
591
|
+
onPeerClose: ((peerId: string) => void) | null;
|
|
592
|
+
onMessage: ((peerId: string, data: any, channel: 'unreliable' | 'reliable') => void) | null;
|
|
593
|
+
onError: ((peerId: string, err: any) => void) | null;
|
|
594
|
+
close(): void;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export interface WebTransportOptions {
|
|
598
|
+
/** https:// HTTP/3 endpoint (defaults to Usion.config.webTransportUrl). */
|
|
599
|
+
url?: string;
|
|
600
|
+
/** For self-signed dev certificates. */
|
|
601
|
+
serverCertificateHashes?: Array<{ algorithm: string; value: BufferSource }>;
|
|
602
|
+
/** Drop stale/out-of-order datagrams (default true). */
|
|
603
|
+
sequenced?: boolean;
|
|
604
|
+
/** Injectable WebTransport constructor (tests). */
|
|
605
|
+
WebTransport?: any;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export class WebTransportConnection {
|
|
609
|
+
constructor(opts: WebTransportOptions & { url: string });
|
|
610
|
+
readonly connected: boolean;
|
|
611
|
+
onOpen: (() => void) | null;
|
|
612
|
+
onMessage: ((data: any, channel: 'datagram' | 'reliable') => void) | null;
|
|
613
|
+
onClose: (() => void) | null;
|
|
614
|
+
onError: ((err: any) => void) | null;
|
|
615
|
+
connect(): Promise<void>;
|
|
616
|
+
/** Send over the unreliable datagram channel (sequenced). */
|
|
617
|
+
send(data: any): boolean;
|
|
618
|
+
/** Send over the reliable ordered stream. */
|
|
619
|
+
sendReliable(data: any): boolean;
|
|
620
|
+
close(): void;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export interface NetcodeModule {
|
|
624
|
+
SnapshotInterpolation: typeof SnapshotInterpolation;
|
|
625
|
+
Vault: typeof Vault;
|
|
626
|
+
Predictor: typeof Predictor;
|
|
627
|
+
Coalescer: typeof Coalescer;
|
|
628
|
+
PingMeter: typeof PingMeter;
|
|
629
|
+
MeshConnection: typeof MeshConnection;
|
|
630
|
+
MeshNetwork: typeof MeshNetwork;
|
|
631
|
+
WebTransportConnection: typeof WebTransportConnection;
|
|
632
|
+
NetworkSim: typeof NetworkSim;
|
|
633
|
+
Lockstep: typeof Lockstep;
|
|
634
|
+
LagCompensator: typeof LagCompensator;
|
|
635
|
+
diff(prev: any, next: any): any | undefined;
|
|
636
|
+
patch(base: any, delta: any): any;
|
|
637
|
+
quantize<T = any>(value: T, precision?: number): T;
|
|
638
|
+
encode(value: any): Uint8Array;
|
|
639
|
+
decode(buf: ArrayBuffer | Uint8Array): any;
|
|
244
640
|
}
|
|
245
641
|
|
|
246
642
|
// ─── Results ────────────────────────────────────────────────────
|
|
@@ -346,6 +742,59 @@ export interface UsionSDK {
|
|
|
346
742
|
chat: ChatModule;
|
|
347
743
|
bot: BotModule;
|
|
348
744
|
game: GameModule;
|
|
745
|
+
lobby: LobbyModule;
|
|
746
|
+
leaderboard: LeaderboardModule;
|
|
747
|
+
matchmaking: MatchmakingModule;
|
|
748
|
+
netcode: NetcodeModule;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
export interface Match { roomId: string; players: string[]; serviceId?: string; }
|
|
752
|
+
|
|
753
|
+
export interface MatchmakingModule {
|
|
754
|
+
/** Join the quick-match queue; resolves when paired with online strangers. */
|
|
755
|
+
find(serviceId?: string, opts?: { size?: number }): Promise<Match>;
|
|
756
|
+
/** Leave the queue / stop waiting. */
|
|
757
|
+
cancel(): Promise<any>;
|
|
758
|
+
/** Called whenever a match is found. */
|
|
759
|
+
onMatch(cb: (match: Match) => void): void;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export interface LeaderboardEntry {
|
|
763
|
+
user_id: string;
|
|
764
|
+
name?: string | null;
|
|
765
|
+
avatar?: string | null;
|
|
766
|
+
score: number;
|
|
767
|
+
rank: number;
|
|
768
|
+
is_me: boolean;
|
|
769
|
+
metadata?: any;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export interface LeaderboardModule {
|
|
773
|
+
/** Submit a score (best is kept per the service's order config). Opt-in per game. */
|
|
774
|
+
submit(score: number, metadata?: any, opts?: { serviceId?: string }): Promise<{ success: boolean; score: number; best: number; rank: number; updated: boolean }>;
|
|
775
|
+
/** Leaderboard of people you've messaged (plus you). */
|
|
776
|
+
friends(opts?: { serviceId?: string; limit?: number }): Promise<LeaderboardEntry[]>;
|
|
777
|
+
/** Global top N. */
|
|
778
|
+
top(opts?: { serviceId?: string; limit?: number }): Promise<LeaderboardEntry[]>;
|
|
779
|
+
/** Your own score + global rank. */
|
|
780
|
+
me(opts?: { serviceId?: string }): Promise<{ score: number | null; rank: number | null; total: number; metadata?: any }>;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
export interface LobbyMember { id: string; name?: string; ready: boolean; }
|
|
784
|
+
export interface LobbyState { code: string | null; host: string | null; status: string | null; members: LobbyMember[]; }
|
|
785
|
+
|
|
786
|
+
export interface LobbyModule {
|
|
787
|
+
readonly state: LobbyState;
|
|
788
|
+
onUpdate(cb: (state: { code: string; host: string; status: string; members: LobbyMember[] }) => void): void;
|
|
789
|
+
onStarted(cb: (data: { room_id: string; by: string; player_ids?: string[] }) => void): void;
|
|
790
|
+
create(opts?: { maxPlayers?: number; public?: boolean }): Promise<{ code: string }>;
|
|
791
|
+
join(code: string): Promise<{ code: string }>;
|
|
792
|
+
leave(): Promise<void>;
|
|
793
|
+
setReady(ready?: boolean): Promise<any>;
|
|
794
|
+
allReady(): boolean;
|
|
795
|
+
isHost(): boolean;
|
|
796
|
+
start(roomId: string): Promise<any>;
|
|
797
|
+
queue(serviceId: string, opts?: { conversationId?: string }): Promise<any>;
|
|
349
798
|
}
|
|
350
799
|
|
|
351
800
|
declare const Usion: UsionSDK;
|