@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,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
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usion SDK Netcode — compact binary codec (zero-dependency).
|
|
3
|
+
*
|
|
4
|
+
* A tiny MessagePack-style serializer for JSON-like snapshot payloads. Pair it
|
|
5
|
+
* with the snapshot sender/receiver (`encode`/`decode` options) to send game
|
|
6
|
+
* state as binary over the WebRTC data channel or a binary-capable WebSocket —
|
|
7
|
+
* meaningfully smaller than JSON, especially after quantization (small ints
|
|
8
|
+
* pack into 1–2 bytes). WebRTC/Socket.IO transmit ArrayBuffers natively.
|
|
9
|
+
*
|
|
10
|
+
* NOT for the iframe/WebView proxy path (that bridge relays JSON) — use binary
|
|
11
|
+
* on direct/mesh transports.
|
|
12
|
+
*
|
|
13
|
+
* Wire tags: 0xc0 null · 0xc2 false · 0xc3 true · 0xc4 int(zigzag varint) ·
|
|
14
|
+
* 0xc5 float64 · 0xc6 string · 0xc7 array · 0xc8 object
|
|
15
|
+
*/
|
|
16
|
+
const NULL = 0xc0, FALSE = 0xc2, TRUE = 0xc3, INT = 0xc4, F64 = 0xc5, STR = 0xc6, ARR = 0xc7, OBJ = 0xc8;
|
|
17
|
+
|
|
18
|
+
const _enc = typeof TextEncoder !== 'undefined' ? new TextEncoder() : null;
|
|
19
|
+
const _dec = typeof TextDecoder !== 'undefined' ? new TextDecoder() : null;
|
|
20
|
+
|
|
21
|
+
function pushVarint(out, n) {
|
|
22
|
+
// unsigned LEB128
|
|
23
|
+
n = n >>> 0;
|
|
24
|
+
while (n > 0x7f) { out.push((n & 0x7f) | 0x80); n >>>= 7; }
|
|
25
|
+
out.push(n);
|
|
26
|
+
}
|
|
27
|
+
function zigzag(n) { return ((n << 1) ^ (n >> 31)) >>> 0; }
|
|
28
|
+
function unzigzag(u) { return (u >>> 1) ^ -(u & 1); }
|
|
29
|
+
|
|
30
|
+
function encodeValue(out, v) {
|
|
31
|
+
if (v === null || v === undefined) { out.push(NULL); return; }
|
|
32
|
+
const t = typeof v;
|
|
33
|
+
if (t === 'boolean') { out.push(v ? TRUE : FALSE); return; }
|
|
34
|
+
if (t === 'number') {
|
|
35
|
+
if (Number.isInteger(v) && v >= -2147483648 && v <= 2147483647) {
|
|
36
|
+
out.push(INT); pushVarint(out, zigzag(v));
|
|
37
|
+
} else {
|
|
38
|
+
out.push(F64);
|
|
39
|
+
const b = new Uint8Array(8); new DataView(b.buffer).setFloat64(0, v, true);
|
|
40
|
+
for (let i = 0; i < 8; i++) out.push(b[i]);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (t === 'string') {
|
|
45
|
+
out.push(STR);
|
|
46
|
+
const bytes = _enc ? _enc.encode(v) : Buffer.from(v, 'utf8');
|
|
47
|
+
pushVarint(out, bytes.length);
|
|
48
|
+
for (let i = 0; i < bytes.length; i++) out.push(bytes[i]);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(v)) {
|
|
52
|
+
out.push(ARR); pushVarint(out, v.length);
|
|
53
|
+
for (let i = 0; i < v.length; i++) encodeValue(out, v[i]);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (t === 'object') {
|
|
57
|
+
const keys = Object.keys(v);
|
|
58
|
+
out.push(OBJ); pushVarint(out, keys.length);
|
|
59
|
+
for (let i = 0; i < keys.length; i++) {
|
|
60
|
+
const k = keys[i];
|
|
61
|
+
const kb = _enc ? _enc.encode(k) : Buffer.from(k, 'utf8');
|
|
62
|
+
pushVarint(out, kb.length);
|
|
63
|
+
for (let j = 0; j < kb.length; j++) out.push(kb[j]);
|
|
64
|
+
encodeValue(out, v[k]);
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
out.push(NULL);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Encode a JSON-like value to a Uint8Array. */
|
|
72
|
+
export function encode(value) {
|
|
73
|
+
const out = [];
|
|
74
|
+
encodeValue(out, value);
|
|
75
|
+
return Uint8Array.from(out);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function reader(bytes) {
|
|
79
|
+
let off = 0;
|
|
80
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
81
|
+
function varint() {
|
|
82
|
+
let shift = 0, result = 0, b;
|
|
83
|
+
do { b = bytes[off++]; result |= (b & 0x7f) << shift; shift += 7; } while (b & 0x80);
|
|
84
|
+
return result >>> 0;
|
|
85
|
+
}
|
|
86
|
+
function str() {
|
|
87
|
+
const len = varint();
|
|
88
|
+
const slice = bytes.subarray(off, off + len);
|
|
89
|
+
off += len;
|
|
90
|
+
return _dec ? _dec.decode(slice) : Buffer.from(slice).toString('utf8');
|
|
91
|
+
}
|
|
92
|
+
function value() {
|
|
93
|
+
const tag = bytes[off++];
|
|
94
|
+
switch (tag) {
|
|
95
|
+
case NULL: return null;
|
|
96
|
+
case TRUE: return true;
|
|
97
|
+
case FALSE: return false;
|
|
98
|
+
case INT: return unzigzag(varint());
|
|
99
|
+
case F64: { const v = dv.getFloat64(off, true); off += 8; return v; }
|
|
100
|
+
case STR: return str();
|
|
101
|
+
case ARR: { const n = varint(); const a = new Array(n); for (let i = 0; i < n; i++) a[i] = value(); return a; }
|
|
102
|
+
case OBJ: { const n = varint(); const o = {}; for (let i = 0; i < n; i++) { const k = str(); o[k] = value(); } return o; }
|
|
103
|
+
default: return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Decode a Uint8Array / ArrayBuffer produced by `encode`. */
|
|
110
|
+
export function decode(buf) {
|
|
111
|
+
const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
112
|
+
return reader(bytes)();
|
|
113
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usion SDK Netcode — Delta compression for JSON game state.
|
|
3
|
+
*
|
|
4
|
+
* Real-time games typically re-send their entire world every tick. Most of
|
|
5
|
+
* that payload is unchanged. `diff` produces a minimal patch describing only
|
|
6
|
+
* what changed since the previous state; `patch` reconstructs the new state
|
|
7
|
+
* from a base + patch. Both are plain JSON, so the patch rides on the existing
|
|
8
|
+
* realtime/action channels unchanged.
|
|
9
|
+
*
|
|
10
|
+
* Wire format:
|
|
11
|
+
* - Objects → `{ k: <patch> }` for changed keys, `_d: [keys]` for deletions.
|
|
12
|
+
* - **Entity arrays** (every element a plain object with a stable `id`) →
|
|
13
|
+
* keyed diff `{ _ka:{id:patch}, _add:[entities], _del:[ids], _ord:[ids] }`.
|
|
14
|
+
* This is O(changes), not O(index shift): inserting/removing an element no
|
|
15
|
+
* longer rewrites every following index. (Gaffer-on-Games "state sync".)
|
|
16
|
+
* - Other arrays → index diff `{ _a:{i:patch}, _n:length }`.
|
|
17
|
+
* - Primitives / type changes → `{ _s: value }`.
|
|
18
|
+
* - No change → `undefined`.
|
|
19
|
+
*
|
|
20
|
+
* `quantize` rounds numeric fields to a fixed resolution before diffing, so
|
|
21
|
+
* sub-resolution jitter doesn't produce spurious deltas and payloads shrink
|
|
22
|
+
* (the single biggest, simplest snapshot-compression win — Gaffer on Games).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const SET = '_s'; // wholesale set
|
|
26
|
+
const DEL = '_d'; // deleted object keys
|
|
27
|
+
const ARR = '_a'; // index array patches
|
|
28
|
+
const LEN = '_n'; // index array length
|
|
29
|
+
const KA = '_ka'; // keyed-array: per-id patches
|
|
30
|
+
const ADD = '_add'; // keyed-array: added entities (full)
|
|
31
|
+
const KDEL = '_del';// keyed-array: removed ids
|
|
32
|
+
const ORD = '_ord'; // keyed-array: explicit id order (only when it changes)
|
|
33
|
+
|
|
34
|
+
function isPlainObject(v) {
|
|
35
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isEntityArray(v) {
|
|
39
|
+
if (!Array.isArray(v) || v.length === 0) return false;
|
|
40
|
+
for (let i = 0; i < v.length; i++) {
|
|
41
|
+
const e = v[i];
|
|
42
|
+
if (!isPlainObject(e) || (typeof e.id !== 'string' && typeof e.id !== 'number')) return false;
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sameOrder(a, b) {
|
|
48
|
+
if (a.length !== b.length) return false;
|
|
49
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Compute a minimal patch turning `prev` into `next`.
|
|
55
|
+
* @returns the patch, or `undefined` when nothing changed.
|
|
56
|
+
*/
|
|
57
|
+
export function diff(prev, next) {
|
|
58
|
+
if (prev === next) return undefined;
|
|
59
|
+
|
|
60
|
+
const prevArr = Array.isArray(prev);
|
|
61
|
+
const nextArr = Array.isArray(next);
|
|
62
|
+
|
|
63
|
+
// Type changed (or one side primitive) — send the whole value.
|
|
64
|
+
if (prevArr !== nextArr || isPlainObject(prev) !== isPlainObject(next)) {
|
|
65
|
+
return { [SET]: next };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (nextArr) {
|
|
69
|
+
// Keyed (entity) arrays: diff by id so index shifts are free.
|
|
70
|
+
if (isEntityArray(prev) && isEntityArray(next)) return diffKeyedArray(prev, next);
|
|
71
|
+
return diffIndexArray(prev, next);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (isPlainObject(next)) return diffObject(prev, next);
|
|
75
|
+
|
|
76
|
+
// Two differing primitives.
|
|
77
|
+
return { [SET]: next };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function diffObject(prev, next) {
|
|
81
|
+
const out = {};
|
|
82
|
+
let changed = false;
|
|
83
|
+
for (const k in next) {
|
|
84
|
+
if (!Object.prototype.hasOwnProperty.call(next, k)) continue;
|
|
85
|
+
const d = diff(prev[k], next[k]);
|
|
86
|
+
if (d !== undefined) { out[k] = d; changed = true; }
|
|
87
|
+
}
|
|
88
|
+
const deleted = [];
|
|
89
|
+
for (const k in prev) {
|
|
90
|
+
if (!Object.prototype.hasOwnProperty.call(prev, k)) continue;
|
|
91
|
+
if (!Object.prototype.hasOwnProperty.call(next, k)) deleted.push(k);
|
|
92
|
+
}
|
|
93
|
+
if (deleted.length) { out[DEL] = deleted; changed = true; }
|
|
94
|
+
return changed ? out : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function diffIndexArray(prev, next) {
|
|
98
|
+
const out = { [ARR]: {} };
|
|
99
|
+
let changed = false;
|
|
100
|
+
for (let i = 0; i < next.length; i++) {
|
|
101
|
+
const d = i < prev.length ? diff(prev[i], next[i]) : { [SET]: next[i] };
|
|
102
|
+
if (d !== undefined) { out[ARR][i] = d; changed = true; }
|
|
103
|
+
}
|
|
104
|
+
if (next.length !== prev.length) { out[LEN] = next.length; changed = true; }
|
|
105
|
+
return changed ? out : undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function diffKeyedArray(prev, next) {
|
|
109
|
+
const prevById = {};
|
|
110
|
+
const prevIds = [];
|
|
111
|
+
for (let i = 0; i < prev.length; i++) { prevById[prev[i].id] = prev[i]; prevIds.push(prev[i].id); }
|
|
112
|
+
const nextIds = [];
|
|
113
|
+
const out = {};
|
|
114
|
+
let changed = false;
|
|
115
|
+
|
|
116
|
+
for (let i = 0; i < next.length; i++) {
|
|
117
|
+
const e = next[i];
|
|
118
|
+
nextIds.push(e.id);
|
|
119
|
+
if (Object.prototype.hasOwnProperty.call(prevById, e.id)) {
|
|
120
|
+
const d = diff(prevById[e.id], e);
|
|
121
|
+
if (d !== undefined) {
|
|
122
|
+
if (!out[KA]) out[KA] = {};
|
|
123
|
+
out[KA][e.id] = d;
|
|
124
|
+
changed = true;
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
if (!out[ADD]) out[ADD] = [];
|
|
128
|
+
out[ADD].push(e);
|
|
129
|
+
changed = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const nextIdSet = {};
|
|
134
|
+
for (let i = 0; i < nextIds.length; i++) nextIdSet[nextIds[i]] = true;
|
|
135
|
+
for (let i = 0; i < prevIds.length; i++) {
|
|
136
|
+
if (!nextIdSet[prevIds[i]]) {
|
|
137
|
+
if (!out[KDEL]) out[KDEL] = [];
|
|
138
|
+
out[KDEL].push(prevIds[i]);
|
|
139
|
+
changed = true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!sameOrder(prevIds, nextIds)) { out[ORD] = nextIds; changed = true; }
|
|
144
|
+
return changed ? out : undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Apply a patch produced by `diff` to `base`, returning the new value.
|
|
149
|
+
* Does not mutate `base`. A nullish patch returns `base` unchanged.
|
|
150
|
+
*/
|
|
151
|
+
export function patch(base, p) {
|
|
152
|
+
if (p === undefined || p === null) return base;
|
|
153
|
+
if (Object.prototype.hasOwnProperty.call(p, SET)) return p[SET];
|
|
154
|
+
|
|
155
|
+
// Keyed (entity) array.
|
|
156
|
+
if (p[KA] || p[ADD] || p[KDEL] || p[ORD]) return patchKeyedArray(base, p);
|
|
157
|
+
|
|
158
|
+
// Index array.
|
|
159
|
+
if (Object.prototype.hasOwnProperty.call(p, ARR) || Object.prototype.hasOwnProperty.call(p, LEN)) {
|
|
160
|
+
const src = Array.isArray(base) ? base : [];
|
|
161
|
+
const len = Object.prototype.hasOwnProperty.call(p, LEN) ? p[LEN] : src.length;
|
|
162
|
+
const out = src.slice(0, len);
|
|
163
|
+
const patches = p[ARR] || {};
|
|
164
|
+
for (const i in patches) {
|
|
165
|
+
if (!Object.prototype.hasOwnProperty.call(patches, i)) continue;
|
|
166
|
+
out[i] = patch(out[i], patches[i]);
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Object patch.
|
|
172
|
+
const out = isPlainObject(base) ? Object.assign({}, base) : {};
|
|
173
|
+
const deleted = p[DEL];
|
|
174
|
+
for (const k in p) {
|
|
175
|
+
if (k === DEL) continue;
|
|
176
|
+
if (!Object.prototype.hasOwnProperty.call(p, k)) continue;
|
|
177
|
+
out[k] = patch(out[k], p[k]);
|
|
178
|
+
}
|
|
179
|
+
if (Array.isArray(deleted)) for (let i = 0; i < deleted.length; i++) delete out[deleted[i]];
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function patchKeyedArray(base, p) {
|
|
184
|
+
const baseArr = Array.isArray(base) ? base : [];
|
|
185
|
+
const byId = {};
|
|
186
|
+
const baseOrder = [];
|
|
187
|
+
for (let i = 0; i < baseArr.length; i++) {
|
|
188
|
+
const e = baseArr[i];
|
|
189
|
+
if (e && (typeof e.id === 'string' || typeof e.id === 'number')) { byId[e.id] = e; baseOrder.push(e.id); }
|
|
190
|
+
}
|
|
191
|
+
// Apply per-id field patches.
|
|
192
|
+
if (p[KA]) for (const id in p[KA]) {
|
|
193
|
+
if (Object.prototype.hasOwnProperty.call(p[KA], id) && byId[id] !== undefined) byId[id] = patch(byId[id], p[KA][id]);
|
|
194
|
+
}
|
|
195
|
+
// Additions.
|
|
196
|
+
if (p[ADD]) for (let i = 0; i < p[ADD].length; i++) { const e = p[ADD][i]; byId[e.id] = e; }
|
|
197
|
+
// Removals.
|
|
198
|
+
const removed = {};
|
|
199
|
+
if (p[KDEL]) for (let i = 0; i < p[KDEL].length; i++) removed[p[KDEL][i]] = true;
|
|
200
|
+
|
|
201
|
+
if (p[ORD]) {
|
|
202
|
+
const out = [];
|
|
203
|
+
for (let i = 0; i < p[ORD].length; i++) {
|
|
204
|
+
const id = p[ORD][i];
|
|
205
|
+
if (byId[id] !== undefined) out.push(byId[id]);
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
// No order change: keep base order minus removals, patched in place.
|
|
210
|
+
const out = [];
|
|
211
|
+
for (let i = 0; i < baseOrder.length; i++) {
|
|
212
|
+
const id = baseOrder[i];
|
|
213
|
+
if (!removed[id]) out.push(byId[id]);
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Round every numeric field to `precision` decimal places (default 2),
|
|
220
|
+
* returning a new value. Apply before diffing so sub-resolution jitter and
|
|
221
|
+
* float noise don't generate spurious deltas, and payloads stay small.
|
|
222
|
+
*/
|
|
223
|
+
export function quantize(value, precision = 2) {
|
|
224
|
+
const f = Math.pow(10, precision);
|
|
225
|
+
function q(v) {
|
|
226
|
+
if (typeof v === 'number') return Number.isFinite(v) ? Math.round(v * f) / f : v;
|
|
227
|
+
if (Array.isArray(v)) return v.map(q);
|
|
228
|
+
if (isPlainObject(v)) {
|
|
229
|
+
const o = {};
|
|
230
|
+
for (const k in v) if (Object.prototype.hasOwnProperty.call(v, k)) o[k] = q(v[k]);
|
|
231
|
+
return o;
|
|
232
|
+
}
|
|
233
|
+
return v;
|
|
234
|
+
}
|
|
235
|
+
return q(value);
|
|
236
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usion SDK Netcode — public namespace.
|
|
3
|
+
*
|
|
4
|
+
* A small, zero-dependency, transport-agnostic toolkit for smooth low-latency
|
|
5
|
+
* multiplayer. Works across all Usion connection modes (platform / direct /
|
|
6
|
+
* proxy) and both platforms, computing on top of the existing realtime/action
|
|
7
|
+
* plumbing — no host or backend changes required to use it.
|
|
8
|
+
*
|
|
9
|
+
* SnapshotInterpolation — smooth rendering; adaptive buffer + capped extrapolation
|
|
10
|
+
* Predictor — client prediction + reconciliation + error smoothing
|
|
11
|
+
* Coalescer — fixed-Hz outbound send batching
|
|
12
|
+
* PingMeter — RTT / jitter telemetry
|
|
13
|
+
* MeshConnection — WebRTC P2P (sequenced, TURN-ready, auto-reconnect)
|
|
14
|
+
* MeshNetwork — N-peer full mesh
|
|
15
|
+
* diff / patch / quantize — JSON delta compression (id-keyed arrays)
|
|
16
|
+
* encode / decode — compact binary codec
|
|
17
|
+
*/
|
|
18
|
+
import { SnapshotInterpolation, Vault } from './interpolation.js';
|
|
19
|
+
import { Predictor } from './prediction.js';
|
|
20
|
+
import { Coalescer } from './sender.js';
|
|
21
|
+
import { PingMeter } from './ping.js';
|
|
22
|
+
import { MeshConnection } from './mesh.js';
|
|
23
|
+
import { MeshNetwork } from './mesh-network.js';
|
|
24
|
+
import { WebTransportConnection } from './webtransport.js';
|
|
25
|
+
import { NetworkSim } from './netsim.js';
|
|
26
|
+
import { Lockstep } from './lockstep.js';
|
|
27
|
+
import { LagCompensator } from './lagcomp.js';
|
|
28
|
+
import { diff, patch, quantize } from './delta.js';
|
|
29
|
+
import { encode, decode } from './binary.js';
|
|
30
|
+
|
|
31
|
+
export const netcode = {
|
|
32
|
+
SnapshotInterpolation, Vault, Predictor, Coalescer, PingMeter,
|
|
33
|
+
MeshConnection, MeshNetwork, WebTransportConnection, NetworkSim, Lockstep, LagCompensator,
|
|
34
|
+
diff, patch, quantize, encode, decode,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
SnapshotInterpolation, Vault, Predictor, Coalescer, PingMeter,
|
|
39
|
+
MeshConnection, MeshNetwork, WebTransportConnection, NetworkSim, Lockstep, LagCompensator,
|
|
40
|
+
diff, patch, quantize, encode, decode,
|
|
41
|
+
};
|