@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,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
+ };
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Usion SDK Netcode — Snapshot interpolation.
3
+ *
4
+ * Render every entity slightly in the past and interpolate between the two
5
+ * snapshots straddling "renderTime", so motion stays smooth and late /
6
+ * duplicate / out-of-order / dropped packets are absorbed for free. This is
7
+ * the single biggest perceived-lag fix for real-time games.
8
+ *
9
+ * Practices adopted (Valve Source "Interpolation" + geckos.io):
10
+ * - Interpolation delay (buffer) sized in server-frames, with an **adaptive**
11
+ * mode that grows the buffer with measured network jitter (cl_interp_ratio).
12
+ * - **Capped extrapolation** when the buffer underruns (packet loss): project
13
+ * forward from the last known velocity, bounded (Source caps at 0.25s) so
14
+ * prediction error stays small.
15
+ * - Optional **server-time domain**: if snapshots carry a server `time`, render
16
+ * against an estimated server clock (robust to bursty arrival). Default stays
17
+ * the client arrival-time domain — zero clock sync required.
18
+ *
19
+ * Key-spec syntax: 'x y angle(deg) r(rad)'.
20
+ */
21
+
22
+ function shortestAngle(a, b, twoPi) {
23
+ let d = (b - a) % twoPi;
24
+ const half = twoPi / 2;
25
+ if (d > half) d -= twoPi;
26
+ if (d < -half) d += twoPi;
27
+ return d;
28
+ }
29
+
30
+ function parseKeys(spec) {
31
+ return String(spec || '')
32
+ .trim()
33
+ .split(/\s+/)
34
+ .filter(Boolean)
35
+ .map((tok) => {
36
+ const m = tok.match(/^(.+?)\((deg|rad)\)$/);
37
+ return m ? { key: m[1], type: m[2] } : { key: tok, type: 'linear' };
38
+ });
39
+ }
40
+
41
+ function lerpField(a, b, t, type) {
42
+ if (type === 'deg') return a + shortestAngle(a, b, 360) * t;
43
+ if (type === 'rad') return a + shortestAngle(a, b, Math.PI * 2) * t;
44
+ return a + (b - a) * t;
45
+ }
46
+
47
+ /** Time-ordered ring buffer of snapshots. Newest last. */
48
+ export class Vault {
49
+ constructor(maxSize = 120) {
50
+ this._items = [];
51
+ this._maxSize = maxSize;
52
+ }
53
+ add(snapshot) {
54
+ this._items.push(snapshot);
55
+ if (this._items.length > this._maxSize) this._items.shift();
56
+ }
57
+ get size() { return this._items.length; }
58
+ setMaxSize(n) { this._maxSize = Math.max(2, n | 0); }
59
+ latest() { return this._items.length ? this._items[this._items.length - 1] : null; }
60
+ prevLatest() { return this._items.length >= 2 ? this._items[this._items.length - 2] : null; }
61
+ clear() { this._items = []; }
62
+
63
+ /** [older, newer] snapshots straddling `time`, clamping at the ends. */
64
+ straddle(time) {
65
+ const items = this._items;
66
+ if (items.length === 0) return [null, null];
67
+ if (items.length === 1) return [items[0], items[0]];
68
+ if (time <= items[0].time) return [items[0], items[0]];
69
+ const last = items[items.length - 1];
70
+ if (time >= last.time) return [last, last];
71
+ for (let i = items.length - 1; i > 0; i--) {
72
+ if (items[i - 1].time <= time && time <= items[i].time) return [items[i - 1], items[i]];
73
+ }
74
+ return [last, last];
75
+ }
76
+ }
77
+
78
+ function asEntityArray(state, group) {
79
+ if (group) return Array.isArray(state[group]) ? state[group] : [];
80
+ return Array.isArray(state) ? state : [];
81
+ }
82
+
83
+ export class SnapshotInterpolation {
84
+ /**
85
+ * @param {object} [opts]
86
+ * @param {number} [opts.serverFps=20] Expected snapshot rate (sets default buffer).
87
+ * @param {number} [opts.bufferMs] Fixed interpolation delay (default ≈ 3 frames).
88
+ * @param {boolean} [opts.adaptive=false] Grow the buffer with measured jitter.
89
+ * @param {number} [opts.minBufferMs] Lower clamp for adaptive buffer.
90
+ * @param {number} [opts.maxBufferMs] Upper clamp for adaptive buffer.
91
+ * @param {number} [opts.extrapolationMs=0] Max forward extrapolation on underrun (0 = off).
92
+ * @param {boolean} [opts.serverTime=false] Use snapshot.time (server clock) domain.
93
+ * @param {number} [opts.maxSize=120]
94
+ * @param {function} [opts.now]
95
+ */
96
+ constructor(opts = {}) {
97
+ const serverFps = opts.serverFps || 20;
98
+ this._frameMs = 1000 / serverFps;
99
+ this.vault = new Vault(opts.maxSize || 120);
100
+ this._fixedBuffer = opts.bufferMs != null ? opts.bufferMs : Math.ceil(this._frameMs * 3);
101
+ this._bufferMs = this._fixedBuffer;
102
+ this._adaptive = !!opts.adaptive;
103
+ this._minBuffer = opts.minBufferMs != null ? opts.minBufferMs : Math.ceil(this._frameMs * 2);
104
+ this._maxBuffer = opts.maxBufferMs != null ? opts.maxBufferMs : Math.ceil(this._frameMs * 8);
105
+ this._extrapolationMs = opts.extrapolationMs || 0;
106
+ this._useServerTime = !!opts.serverTime;
107
+ this._now = opts.now || (() => Date.now());
108
+
109
+ // adaptive/jitter + server-clock estimation state
110
+ this._lastArrival = null;
111
+ this._avgInterval = this._frameMs;
112
+ this._jitter = 0;
113
+ this._offset = null; // EWMA(arrival - serverTime)
114
+ }
115
+
116
+ getBufferMs() { return this._bufferMs; }
117
+ setBufferMs(ms) { this._fixedBuffer = Math.max(0, ms | 0); this._bufferMs = this._fixedBuffer; }
118
+ getJitter() { return this._jitter; }
119
+
120
+ /**
121
+ * Add a received snapshot. `state` is an entity array (each needs a stable
122
+ * `id`) or a map of named groups → entity arrays. Pass `{ state, time }` to
123
+ * supply a server timestamp for the server-time domain.
124
+ */
125
+ add(snapshot) {
126
+ if (!snapshot) return;
127
+ const arrival = this._now();
128
+ const state = snapshot.state !== undefined ? snapshot.state : snapshot;
129
+ const serverTime = snapshot.time;
130
+
131
+ // Jitter / interval estimate (EWMA), used by the adaptive buffer.
132
+ if (this._lastArrival != null) {
133
+ const interval = arrival - this._lastArrival;
134
+ this._avgInterval += 0.1 * (interval - this._avgInterval);
135
+ this._jitter += 0.1 * (Math.abs(interval - this._avgInterval) - this._jitter);
136
+ if (this._adaptive) {
137
+ const target = this._avgInterval + 2 * this._jitter;
138
+ this._bufferMs = Math.max(this._minBuffer, Math.min(this._maxBuffer, target));
139
+ }
140
+ }
141
+ this._lastArrival = arrival;
142
+
143
+ // Server-clock offset estimate.
144
+ if (this._useServerTime && typeof serverTime === 'number') {
145
+ const o = arrival - serverTime;
146
+ this._offset = this._offset == null ? o : this._offset + 0.05 * (o - this._offset);
147
+ }
148
+
149
+ // Stamp the time used for the interpolation domain.
150
+ const time = (this._useServerTime && typeof serverTime === 'number') ? serverTime : arrival;
151
+ this.vault.add({ time, state });
152
+ }
153
+
154
+ _renderTime() {
155
+ if (this._useServerTime && this._offset != null) return (this._now() - this._offset) - this._bufferMs;
156
+ return this._now() - this._bufferMs;
157
+ }
158
+
159
+ /**
160
+ * Interpolated entities for the current render instant.
161
+ * @param {string} keys e.g. 'x y' or 'x y angle(deg)'
162
+ * @param {string} [group]
163
+ * @returns {Array|null}
164
+ */
165
+ calc(keys, group) {
166
+ const renderTime = this._renderTime();
167
+ const latest = this.vault.latest();
168
+ if (!latest) return null;
169
+ const specs = parseKeys(keys);
170
+
171
+ // Buffer underrun: render time is ahead of the newest snapshot.
172
+ if (renderTime > latest.time) {
173
+ if (this._extrapolationMs > 0) return this._extrapolate(renderTime, specs, group);
174
+ return asEntityArray(latest.state, group).map((e) => Object.assign({}, e));
175
+ }
176
+
177
+ const [older, newer] = this.vault.straddle(renderTime);
178
+ if (!older || !newer) return null;
179
+
180
+ let t = 0;
181
+ if (newer.time !== older.time) {
182
+ t = (renderTime - older.time) / (newer.time - older.time);
183
+ t = t < 0 ? 0 : t > 1 ? 1 : t;
184
+ }
185
+ return this._blend(asEntityArray(older.state, group), asEntityArray(newer.state, group), t, specs);
186
+ }
187
+
188
+ _blend(oldEntities, newEntities, t, specs) {
189
+ const oldById = {};
190
+ for (let i = 0; i < oldEntities.length; i++) oldById[oldEntities[i].id] = oldEntities[i];
191
+ return newEntities.map((nentity) => {
192
+ const oentity = oldById[nentity.id];
193
+ const out = Object.assign({}, nentity);
194
+ if (oentity) {
195
+ for (let i = 0; i < specs.length; i++) {
196
+ const { key, type } = specs[i];
197
+ const a = oentity[key];
198
+ const b = nentity[key];
199
+ if (typeof a === 'number' && typeof b === 'number') out[key] = lerpField(a, b, t, type);
200
+ }
201
+ }
202
+ return out;
203
+ });
204
+ }
205
+
206
+ _extrapolate(renderTime, specs, group) {
207
+ const newer = this.vault.latest();
208
+ const older = this.vault.prevLatest();
209
+ const newEntities = asEntityArray(newer.state, group);
210
+ if (!older || newer.time <= older.time) return newEntities.map((e) => Object.assign({}, e));
211
+
212
+ const dt = Math.min(renderTime - newer.time, this._extrapolationMs); // cap
213
+ const span = newer.time - older.time;
214
+ const oldById = {};
215
+ const oldEntities = asEntityArray(older.state, group);
216
+ for (let i = 0; i < oldEntities.length; i++) oldById[oldEntities[i].id] = oldEntities[i];
217
+
218
+ return newEntities.map((nentity) => {
219
+ const oentity = oldById[nentity.id];
220
+ const out = Object.assign({}, nentity);
221
+ if (oentity) {
222
+ for (let i = 0; i < specs.length; i++) {
223
+ const { key } = specs[i]; // linear extrapolation (angles handled as linear here)
224
+ const a = oentity[key];
225
+ const b = nentity[key];
226
+ if (typeof a === 'number' && typeof b === 'number') {
227
+ const vel = (b - a) / span;
228
+ out[key] = b + vel * dt;
229
+ }
230
+ }
231
+ }
232
+ return out;
233
+ });
234
+ }
235
+ }