@net-mesh/browser 0.42.0 → 0.44.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +200 -0
  2. package/README.md +12 -0
  3. package/dist/index.bundle.js +2 -2
  4. package/dist/leader/session.d.ts +4 -2
  5. package/dist/leader/session.js +30 -6
  6. package/dist/lobby.d.ts +29 -2
  7. package/dist/lobby.js +80 -10
  8. package/dist/net_leaf.d.ts +19 -21
  9. package/dist/net_leaf.js +42 -44
  10. package/dist/net_leaf_bg.wasm +0 -0
  11. package/dist/net_leaf_bg.wasm.d.ts +10 -10
  12. package/dist/netcode/assignment.d.ts +169 -0
  13. package/dist/netcode/assignment.js +321 -0
  14. package/dist/netcode/client.d.ts +125 -3
  15. package/dist/netcode/client.js +291 -27
  16. package/dist/netcode/codec.d.ts +180 -0
  17. package/dist/netcode/codec.js +547 -0
  18. package/dist/netcode/host.d.ts +58 -1
  19. package/dist/netcode/host.js +254 -9
  20. package/dist/netcode/index.d.ts +14 -4
  21. package/dist/netcode/index.js +8 -3
  22. package/dist/netcode/interpolate.d.ts +24 -1
  23. package/dist/netcode/interpolate.js +52 -1
  24. package/dist/netcode/paths.d.ts +115 -0
  25. package/dist/netcode/paths.js +261 -0
  26. package/dist/netcode/shards.d.ts +138 -0
  27. package/dist/netcode/shards.js +270 -0
  28. package/dist/netcode/wire.d.ts +37 -3
  29. package/dist/netcode/wire.js +28 -1
  30. package/dist/node.d.ts +20 -0
  31. package/dist/node.js +76 -1
  32. package/dist/store/host.js +10 -4
  33. package/dist/store/join.js +38 -2
  34. package/dist/store/owner.d.ts +11 -0
  35. package/dist/store/owner.js +27 -7
  36. package/dist/store/types.d.ts +6 -0
  37. package/dist/stream.d.ts +29 -1
  38. package/dist/stream.js +66 -4
  39. package/dist/udp-probe.d.ts +21 -3
  40. package/dist/udp-probe.js +78 -7
  41. package/dist/world/link.d.ts +9 -0
  42. package/dist/world/link.js +4 -3
  43. package/dist/world/regions.d.ts +9 -2
  44. package/dist/world/regions.js +34 -4
  45. package/package.json +3 -2
@@ -0,0 +1,547 @@
1
+ /**
2
+ * Compact snapshots: a game-supplied entity schema, and the binary snapshot
3
+ * frame it makes possible (sharded-match plan, slice S0c).
4
+ *
5
+ * ```ts
6
+ * const shipCodec = entityCodec({
7
+ * x: quantized(0, 512), // 16 bits over the arena: ~8 mm
8
+ * z: quantized(0, 512),
9
+ * yaw: angle(), // 8 bits: ~1.4°
10
+ * hp: uint(8),
11
+ * });
12
+ * hostNetcode({ …, codec: shipCodec });
13
+ * joinNetcode({ …, codec: shipCodec });
14
+ * ```
15
+ *
16
+ * JSON stays the default and the fallback. A player announces its codec's
17
+ * fingerprint in its join and ping frames; the host sends binary snapshots
18
+ * only to a player whose fingerprint matches its own, and JSON to everyone
19
+ * else — so a mismatched or older peer degrades to JSON, never to garbage.
20
+ *
21
+ * **Ids.** On the binary wire an entity is named by a varint index, not its
22
+ * id string. The host assigns indexes in order of first appearance and
23
+ * **never reuses one** within its lifetime, so a late or reordered
24
+ * snapshot cannot name the wrong entity. A snapshot carries an entity's
25
+ * name the first time it shows that entity to a player; a player that sees
26
+ * an index it has no name for does not draw that entity and asks again
27
+ * (`u` frame) — the lossy carrier's repair, with no acknowledgements.
28
+ *
29
+ * Every binary snapshot also carries the host's **instance**, a random
30
+ * 32-bit number drawn when `hostNetcode` starts. A host re-created under
31
+ * the same label starts its indexes over; a player that sees a new instance
32
+ * drops every name (and every snapshot) it learnt from the old one, so the
33
+ * new host's indexes are never read through the old host's names.
34
+ *
35
+ * Names are also forgotten: an id no player has been shown for
36
+ * `NAME_FORGET_MS` (10 s) loses its index, and comes back, if it does,
37
+ * under a fresh one, named again. Transient ids (projectiles) do not grow
38
+ * the tables for the whole match.
39
+ *
40
+ * **Versions.** The fingerprint covers the binary wire's version as well as
41
+ * the schema. Frames carrying an instance are version 2; a peer from before
42
+ * it has another fingerprint for the same schema, so the pair falls back
43
+ * to JSON instead of refusing each other's frames.
44
+ *
45
+ * **Interpolation.** `entityCodec` also supplies an `interpolate` blend
46
+ * that knows its fields: an `angle()` turns the short way round across
47
+ * 0 / 2π. `joinNetcode` uses it when it has a codec and no `interpolate`
48
+ * of its own.
49
+ *
50
+ * **What the schema promises.** An entity is a flat object of the schema's
51
+ * fields and nothing else. Writing an entity with a field the schema does
52
+ * not have, or a value that is not a finite number (or boolean, for
53
+ * `bool()`), throws: the host then sends that player JSON for that tick
54
+ * (counted in `dropped`) rather than silently losing a field.
55
+ */
56
+ import { lerpAngle, lerpNumbers } from './interpolate.js';
57
+ const finite = (value, field) => {
58
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
59
+ throw new TypeError(`field '${field}': expected a finite number, got ${String(value)}`);
60
+ }
61
+ return value;
62
+ };
63
+ function checkBits(bits, allowed) {
64
+ if (!allowed.includes(bits))
65
+ throw new RangeError(`bits must be one of ${allowed.join(', ')}, got ${String(bits)}`);
66
+ }
67
+ function putUint(view, at, bits, value) {
68
+ if (bits === 8)
69
+ view.setUint8(at, value);
70
+ else if (bits === 16)
71
+ view.setUint16(at, value);
72
+ else
73
+ view.setUint32(at, value);
74
+ }
75
+ function getUint(view, at, bits) {
76
+ return bits === 8 ? view.getUint8(at) : bits === 16 ? view.getUint16(at) : view.getUint32(at);
77
+ }
78
+ /**
79
+ * A number in `[min, max]`, quantized to `bits` (8 or 16; default 16).
80
+ * Values outside the range are clamped — including one a hair past an edge
81
+ * after rounding, which must not wrap around.
82
+ */
83
+ export function quantized(min, max, bits = 16) {
84
+ if (!(Number.isFinite(min) && Number.isFinite(max) && max > min))
85
+ throw new RangeError('quantized(min, max): max must exceed min');
86
+ checkBits(bits, [8, 16]);
87
+ const steps = 2 ** bits - 1;
88
+ const span = max - min;
89
+ return {
90
+ size: bits / 8,
91
+ spec: `q${bits}(${min},${max})`,
92
+ quantum: span / steps / 2,
93
+ write(view, at, value) {
94
+ const v = finite(value, this.spec);
95
+ const q = Math.round(((v - min) / span) * steps);
96
+ putUint(view, at, bits, Math.min(steps, Math.max(0, q)));
97
+ },
98
+ read(view, at) {
99
+ return min + (getUint(view, at, bits) / steps) * span;
100
+ },
101
+ near(a, b) {
102
+ if (typeof a !== 'number' || typeof b !== 'number')
103
+ return a === b;
104
+ const clampTo = (v) => Math.min(max, Math.max(min, v));
105
+ return Math.abs(clampTo(a) - clampTo(b)) <= (2 * span) / steps + 1e-9;
106
+ },
107
+ };
108
+ }
109
+ /** An angle in radians, any value, stored as a fraction of a turn in `bits` (default 8). Reads in `[0, 2π)`. */
110
+ export function angle(bits = 8) {
111
+ checkBits(bits, [8, 16]);
112
+ const steps = 2 ** bits;
113
+ const turn = Math.PI * 2;
114
+ return {
115
+ size: bits / 8,
116
+ spec: `a${bits}`,
117
+ quantum: turn / steps / 2,
118
+ write(view, at, value) {
119
+ const v = finite(value, this.spec);
120
+ const fraction = (((v % turn) + turn) % turn) / turn;
121
+ putUint(view, at, bits, Math.round(fraction * steps) % steps);
122
+ },
123
+ read(view, at) {
124
+ return (getUint(view, at, bits) / steps) * turn;
125
+ },
126
+ near(a, b) {
127
+ if (typeof a !== 'number' || typeof b !== 'number')
128
+ return a === b;
129
+ const d = Math.abs((((a - b) % turn) + turn) % turn);
130
+ return Math.min(d, turn - d) <= (2 * turn) / steps + 1e-9;
131
+ },
132
+ blend: lerpAngle,
133
+ };
134
+ }
135
+ /** A whole number in `[0, 2^bits)`, exact (default 8 bits). Out of range throws. */
136
+ export function uint(bits = 8) {
137
+ checkBits(bits, [8, 16, 32]);
138
+ const max = 2 ** bits - 1;
139
+ return {
140
+ size: bits / 8,
141
+ spec: `u${bits}`,
142
+ quantum: 0,
143
+ write(view, at, value) {
144
+ const v = finite(value, this.spec);
145
+ if (!Number.isInteger(v) || v < 0 || v > max)
146
+ throw new RangeError(`field u${bits}: ${String(v)} is not a whole number in [0, ${max}]`);
147
+ putUint(view, at, bits, v);
148
+ },
149
+ read(view, at) {
150
+ return getUint(view, at, bits);
151
+ },
152
+ near: (a, b) => a === b,
153
+ };
154
+ }
155
+ /** A whole number in `[-2^(bits-1), 2^(bits-1))`, exact (default 16 bits). Out of range throws. */
156
+ export function int(bits = 16) {
157
+ checkBits(bits, [8, 16, 32]);
158
+ const low = -(2 ** (bits - 1));
159
+ const high = 2 ** (bits - 1) - 1;
160
+ return {
161
+ size: bits / 8,
162
+ spec: `i${bits}`,
163
+ quantum: 0,
164
+ write(view, at, value) {
165
+ const v = finite(value, this.spec);
166
+ if (!Number.isInteger(v) || v < low || v > high)
167
+ throw new RangeError(`field i${bits}: ${String(v)} is not a whole number in [${low}, ${high}]`);
168
+ if (bits === 8)
169
+ view.setInt8(at, v);
170
+ else if (bits === 16)
171
+ view.setInt16(at, v);
172
+ else
173
+ view.setInt32(at, v);
174
+ },
175
+ read(view, at) {
176
+ return bits === 8 ? view.getInt8(at) : bits === 16 ? view.getInt16(at) : view.getInt32(at);
177
+ },
178
+ near: (a, b) => a === b,
179
+ };
180
+ }
181
+ /** A 32-bit float: ~7 significant digits, no range to choose. A NaN or an infinity read off the wire refuses the frame. */
182
+ export function float32() {
183
+ return {
184
+ size: 4,
185
+ spec: 'f32',
186
+ quantum: Number.NaN, // relative, not absolute: see Math.fround
187
+ write(view, at, value) {
188
+ view.setFloat32(at, finite(value, this.spec));
189
+ },
190
+ read(view, at) {
191
+ const value = view.getFloat32(at);
192
+ // The writer refuses these, so only a hostile or broken host sends them.
193
+ if (!Number.isFinite(value))
194
+ throw new RangeError(`field f32: ${String(value)} is not finite`);
195
+ return value;
196
+ },
197
+ near(a, b) {
198
+ if (typeof a !== 'number' || typeof b !== 'number')
199
+ return a === b;
200
+ return Math.fround(a) === Math.fround(b) || Math.abs(a - b) <= 1e-6 * Math.max(Math.abs(a), Math.abs(b));
201
+ },
202
+ };
203
+ }
204
+ /** A boolean, one byte. */
205
+ export function bool() {
206
+ return {
207
+ size: 1,
208
+ spec: 'b',
209
+ quantum: 0,
210
+ write(view, at, value) {
211
+ if (typeof value !== 'boolean')
212
+ throw new TypeError(`field b: expected a boolean, got ${String(value)}`);
213
+ view.setUint8(at, value ? 1 : 0);
214
+ },
215
+ read(view, at) {
216
+ return view.getUint8(at) !== 0;
217
+ },
218
+ near: (a, b) => a === b,
219
+ };
220
+ }
221
+ /** FNV-1a of a string, as 8 hex digits. */
222
+ function fnv(text) {
223
+ let hash = 0x811c9dc5;
224
+ for (let i = 0; i < text.length; i += 1)
225
+ hash = Math.imul(hash ^ text.charCodeAt(i), 0x01000193);
226
+ return (hash >>> 0).toString(16).padStart(8, '0');
227
+ }
228
+ /**
229
+ * The binary wire's version, part of every fingerprint. 2: snapshots carry
230
+ * the host's instance. A peer at another version has another fingerprint
231
+ * for the same schema, so the pair falls back to JSON.
232
+ */
233
+ const BINARY_WIRE_VERSION = 2;
234
+ /**
235
+ * An entity codec from a schema: one field codec per property, written in
236
+ * the schema's key order. The fingerprint covers names, order and specs,
237
+ * and the binary wire's version.
238
+ */
239
+ export function entityCodec(fields) {
240
+ const entries = Object.entries(fields);
241
+ if (entries.length === 0)
242
+ throw new RangeError('an entity codec needs at least one field');
243
+ const names = new Set(entries.map(([name]) => name));
244
+ const offsets = [];
245
+ let size = 0;
246
+ for (const [, field] of entries) {
247
+ offsets.push(size);
248
+ size += field.size;
249
+ }
250
+ const fingerprint = fnv(`v${BINARY_WIRE_VERSION};${entries.map(([name, field]) => `${name}:${field.spec}`).join(';')}`);
251
+ const blended = entries.filter(([, field]) => field.blend !== undefined);
252
+ return {
253
+ size,
254
+ fingerprint,
255
+ write(entity, view, at) {
256
+ const record = entity;
257
+ for (const key in record) {
258
+ if (Object.prototype.hasOwnProperty.call(record, key) && !names.has(key)) {
259
+ throw new TypeError(`field '${key}' is not in the codec's schema`);
260
+ }
261
+ }
262
+ entries.forEach(([name, field], i) => {
263
+ try {
264
+ field.write(view, at + offsets[i], record[name]);
265
+ }
266
+ catch (error) {
267
+ throw new TypeError(`field '${name}': ${error.message}`);
268
+ }
269
+ });
270
+ },
271
+ read(view, at) {
272
+ const out = {};
273
+ entries.forEach(([name, field], i) => {
274
+ out[name] = field.read(view, at + offsets[i]);
275
+ });
276
+ return out;
277
+ },
278
+ near(a, b) {
279
+ const left = a;
280
+ const right = b;
281
+ return entries.every(([name, field]) => field.near(left[name], right[name]));
282
+ },
283
+ interpolate(from, to, alpha) {
284
+ const out = lerpNumbers(from, to, alpha);
285
+ if (blended.length === 0 || out === null || typeof out !== 'object')
286
+ return out;
287
+ const left = from;
288
+ const right = to;
289
+ const result = out;
290
+ for (const [name, field] of blended) {
291
+ const a = left[name];
292
+ const b = right[name];
293
+ if (typeof a === 'number' && typeof b === 'number')
294
+ result[name] = field.blend(a, b, alpha);
295
+ }
296
+ return out;
297
+ },
298
+ };
299
+ }
300
+ // ── The binary snapshot frame ──────────────────────────────────────────
301
+ /** First byte of a binary netcode frame. Never `{` (0x7b), so it cannot be JSON. */
302
+ export const BINARY_FRAME_MAGIC = 0xb1;
303
+ /** Kind byte: a snapshot. */
304
+ const KIND_SNAPSHOT = 1;
305
+ /**
306
+ * Kind byte: a snapshot stamped with an assignment epoch (a varint after
307
+ * the tick). A kind of its own, so a decoder that predates epochs refuses
308
+ * the frame rather than reading the epoch as the time.
309
+ */
310
+ const KIND_SNAPSHOT_EPOCH = 2;
311
+ /**
312
+ * Kind bytes: the same two, with the host's instance (a 32-bit number,
313
+ * four bytes) right after the kind byte. Wire version 2.
314
+ */
315
+ const KIND_SNAPSHOT_INSTANCE = 3;
316
+ const KIND_SNAPSHOT_EPOCH_INSTANCE = 4;
317
+ /** The label's 32-bit FNV-1a: a binary frame's stand-in for the JSON `n`. */
318
+ export function labelHash(label) {
319
+ let hash = 0x811c9dc5;
320
+ for (let i = 0; i < label.length; i += 1)
321
+ hash = Math.imul(hash ^ label.charCodeAt(i), 0x01000193);
322
+ return hash >>> 0;
323
+ }
324
+ const utf8 = new TextEncoder();
325
+ const fromUtf8 = new TextDecoder('utf-8', { fatal: true });
326
+ function varintSize(value) {
327
+ let size = 1;
328
+ while (value >= 0x80) {
329
+ value = Math.floor(value / 128);
330
+ size += 1;
331
+ }
332
+ return size;
333
+ }
334
+ function putVarint(bytes, at, value) {
335
+ while (value >= 0x80) {
336
+ bytes[at++] = (value % 128) | 0x80;
337
+ value = Math.floor(value / 128);
338
+ }
339
+ bytes[at++] = value;
340
+ return at;
341
+ }
342
+ /** `[value, next offset]`, or `null` past the end / over 2^53. */
343
+ function getVarint(bytes, at) {
344
+ let value = 0;
345
+ let scale = 1;
346
+ for (let i = 0; i < 8; i += 1) {
347
+ if (at >= bytes.length)
348
+ return null;
349
+ const byte = bytes[at++];
350
+ value += (byte & 0x7f) * scale;
351
+ if ((byte & 0x80) === 0)
352
+ return Number.isSafeInteger(value) ? [value, at] : null;
353
+ scale *= 128;
354
+ }
355
+ return null;
356
+ }
357
+ /** The binary size of one snapshot frame, without building it. */
358
+ export function binarySnapshotSize(frame, names, indexes, entitySize) {
359
+ let size = 1 + 4 + 1 + varintSize(frame.tick) + 8 + varintSize(frame.ack) + varintSize(frame.iv ?? 0);
360
+ if (frame.instance !== undefined)
361
+ size += 4;
362
+ if (frame.ep !== undefined)
363
+ size += varintSize(frame.ep);
364
+ size += varintSize(frame.of ?? 0) + varintSize(frame.c ?? 0);
365
+ let count = 0;
366
+ let nameCount = 0;
367
+ for (const [index, name] of names) {
368
+ nameCount += 1;
369
+ size += varintSize(index) + varintSize(name.length) + name.length;
370
+ }
371
+ size += varintSize(nameCount);
372
+ for (const index of indexes) {
373
+ count += 1;
374
+ size += varintSize(index) + entitySize;
375
+ }
376
+ return size + varintSize(count);
377
+ }
378
+ /**
379
+ * Build one binary snapshot frame. `names` are UTF-8 ids by index;
380
+ * `entities` are by index. Throws if the codec refuses an entity.
381
+ */
382
+ export function encodeBinarySnapshot(label, frame, names, entities, codec) {
383
+ const instance = frame.instance;
384
+ if (instance !== undefined && !(Number.isInteger(instance) && instance >= 0 && instance <= 0xffffffff)) {
385
+ throw new RangeError(`binary snapshot: instance ${String(instance)} is not a 32-bit unsigned integer`);
386
+ }
387
+ const size = binarySnapshotSize(frame, names, entities.keys(), codec.size);
388
+ const bytes = new Uint8Array(size);
389
+ const view = new DataView(bytes.buffer);
390
+ bytes[0] = BINARY_FRAME_MAGIC;
391
+ view.setUint32(1, labelHash(label));
392
+ let at = 6;
393
+ if (instance === undefined) {
394
+ bytes[5] = frame.ep === undefined ? KIND_SNAPSHOT : KIND_SNAPSHOT_EPOCH;
395
+ }
396
+ else {
397
+ bytes[5] = frame.ep === undefined ? KIND_SNAPSHOT_INSTANCE : KIND_SNAPSHOT_EPOCH_INSTANCE;
398
+ view.setUint32(6, instance);
399
+ at = 10;
400
+ }
401
+ at = putVarint(bytes, at, frame.tick);
402
+ if (frame.ep !== undefined)
403
+ at = putVarint(bytes, at, frame.ep);
404
+ view.setFloat64(at, frame.t);
405
+ at += 8;
406
+ at = putVarint(bytes, at, frame.ack);
407
+ at = putVarint(bytes, at, frame.iv ?? 0);
408
+ at = putVarint(bytes, at, frame.of ?? 0);
409
+ at = putVarint(bytes, at, frame.c ?? 0);
410
+ at = putVarint(bytes, at, names.size);
411
+ for (const [index, name] of names) {
412
+ at = putVarint(bytes, at, index);
413
+ at = putVarint(bytes, at, name.length);
414
+ bytes.set(name, at);
415
+ at += name.length;
416
+ }
417
+ at = putVarint(bytes, at, entities.size);
418
+ for (const [index, entity] of entities) {
419
+ at = putVarint(bytes, at, index);
420
+ codec.write(entity, view, at);
421
+ at += codec.size;
422
+ }
423
+ if (at !== size)
424
+ throw new Error(`binary snapshot: wrote ${at} of ${size} bytes`);
425
+ return bytes;
426
+ }
427
+ /** True if `payload` is a binary netcode frame (of any label). */
428
+ export function isBinaryFrame(payload) {
429
+ return payload !== undefined && payload.length > 0 && payload[0] === BINARY_FRAME_MAGIC;
430
+ }
431
+ /**
432
+ * Read a binary snapshot for `label`, or `null` for another label, another
433
+ * kind, or anything malformed (truncated, trailing bytes, bad chunk fields,
434
+ * a duplicate index, a name that is not UTF-8, an entity the codec will not
435
+ * read, such as a NaN `float32()`).
436
+ */
437
+ export function decodeBinarySnapshot(payload, label, codec) {
438
+ if (!isBinaryFrame(payload) || payload.length < 6)
439
+ return null;
440
+ const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
441
+ const kind = payload[5];
442
+ if (view.getUint32(1) !== labelHash(label))
443
+ return null;
444
+ if (kind !== KIND_SNAPSHOT && kind !== KIND_SNAPSHOT_EPOCH && kind !== KIND_SNAPSHOT_INSTANCE && kind !== KIND_SNAPSHOT_EPOCH_INSTANCE) {
445
+ return null;
446
+ }
447
+ let instance;
448
+ let start = 6;
449
+ if (kind === KIND_SNAPSHOT_INSTANCE || kind === KIND_SNAPSHOT_EPOCH_INSTANCE) {
450
+ if (payload.length < 10)
451
+ return null;
452
+ instance = view.getUint32(6);
453
+ start = 10;
454
+ }
455
+ const varint = (at) => getVarint(payload, at);
456
+ let read = varint(start);
457
+ if (read === null)
458
+ return null;
459
+ const [tick] = read;
460
+ let at = read[1];
461
+ let ep;
462
+ if (kind === KIND_SNAPSHOT_EPOCH || kind === KIND_SNAPSHOT_EPOCH_INSTANCE) {
463
+ read = varint(at);
464
+ if (read === null)
465
+ return null;
466
+ [ep, at] = read;
467
+ }
468
+ if (at + 8 > payload.length)
469
+ return null;
470
+ const t = view.getFloat64(at);
471
+ if (!Number.isFinite(t))
472
+ return null;
473
+ at += 8;
474
+ const fields = [];
475
+ for (let i = 0; i < 4; i += 1) {
476
+ read = varint(at);
477
+ if (read === null)
478
+ return null;
479
+ fields.push(read[0]);
480
+ at = read[1];
481
+ }
482
+ const [ack, iv, of, c] = fields;
483
+ if (of !== 0 && (of > 256 || c >= of))
484
+ return null;
485
+ if (of === 0 && c !== 0)
486
+ return null;
487
+ read = varint(at);
488
+ if (read === null)
489
+ return null;
490
+ const names = new Map();
491
+ let nameCount = read[0];
492
+ at = read[1];
493
+ for (; nameCount > 0; nameCount -= 1) {
494
+ const index = varint(at);
495
+ if (index === null)
496
+ return null;
497
+ const length = varint(index[1]);
498
+ if (length === null || length[1] + length[0] > payload.length)
499
+ return null;
500
+ let name;
501
+ try {
502
+ name = fromUtf8.decode(payload.subarray(length[1], length[1] + length[0]));
503
+ }
504
+ catch {
505
+ return null;
506
+ }
507
+ if (names.has(index[0]))
508
+ return null;
509
+ names.set(index[0], name);
510
+ at = length[1] + length[0];
511
+ }
512
+ read = varint(at);
513
+ if (read === null)
514
+ return null;
515
+ let count = read[0];
516
+ at = read[1];
517
+ const entities = new Map();
518
+ for (; count > 0; count -= 1) {
519
+ const index = varint(at);
520
+ if (index === null || index[1] + codec.size > payload.length || entities.has(index[0]))
521
+ return null;
522
+ try {
523
+ entities.set(index[0], codec.read(view, index[1]));
524
+ }
525
+ catch {
526
+ return null;
527
+ }
528
+ at = index[1] + codec.size;
529
+ }
530
+ if (at !== payload.length)
531
+ return null;
532
+ return {
533
+ tick,
534
+ t,
535
+ ack,
536
+ ...(iv > 0 ? { iv } : {}),
537
+ ...(of > 0 ? { c, of } : {}),
538
+ ...(ep === undefined ? {} : { ep }),
539
+ ...(instance === undefined ? {} : { instance }),
540
+ names,
541
+ entities,
542
+ };
543
+ }
544
+ /** UTF-8 of an id, for {@link encodeBinarySnapshot}'s names. */
545
+ export function nameBytes(id) {
546
+ return utf8.encode(id);
547
+ }
@@ -17,6 +17,7 @@
17
17
  * ```
18
18
  */
19
19
  import { type Frame, type NetcodeTransport, type Now } from './wire.js';
20
+ import { type EntityCodec } from './codec.js';
20
21
  /** One input as `step` receives it. */
21
22
  export interface TickInput<I> {
22
23
  readonly seq: number;
@@ -85,6 +86,30 @@ export interface HostNetcodeOptions<E, I> {
85
86
  readonly playerTimeoutMs?: number;
86
87
  /** `false` to drive ticks yourself with {@link HostNetcode.tick}. Default `true`. */
87
88
  readonly autoTick?: boolean;
89
+ /**
90
+ * Compact snapshots (`codec.ts`): players whose `joinNetcode` has a codec
91
+ * with the same fingerprint receive binary snapshots; everyone else JSON.
92
+ * An entity the codec refuses sends that player JSON for that tick
93
+ * (`dropped['codec-write-failed']`).
94
+ */
95
+ readonly codec?: EntityCodec<E>;
96
+ /**
97
+ * Sharded matches (`joinShards`): the assignment epoch this host is at,
98
+ * read every tick. Each snapshot carries it (`ep`), so a player draws
99
+ * this host's entities only at the epoch it has installed; an input
100
+ * frame stamped with another epoch is dropped (`dropped['stale-epoch']`).
101
+ * An input frame with no epoch is applied, as from any plain player.
102
+ */
103
+ epoch?(): number;
104
+ /**
105
+ * Sharded matches: whether this host still holds its lease (`leaseWatch`
106
+ * in `assignment.ts`), asked every tick. While `false` the host neither
107
+ * simulates nor sends — a host cut off from the lobby must stop before
108
+ * the lobby gives its entities to another — and inputs that arrive are
109
+ * dropped (`dropped['lease-lost']`), not saved for later. Clock pings are
110
+ * still answered.
111
+ */
112
+ live?(): boolean;
88
113
  readonly now?: Now;
89
114
  }
90
115
  /** The running host. */
@@ -101,6 +126,38 @@ export interface HostNetcode<E> {
101
126
  * never further back than `maxRewindMs` before now.
102
127
  */
103
128
  rewind(seen: number): Rewound<E>;
129
+ /**
130
+ * Stop taking `peer`'s inputs, and say how far they were applied: the
131
+ * half of a planned move (sharded matches, S5) that pairs with the
132
+ * entity's frozen state. Call it between ticks — the state you freeze in
133
+ * the same synchronous turn then contains exactly inputs `1..lastApplied`.
134
+ * Later inputs from `peer` are dropped unapplied and unacknowledged
135
+ * (`dropped['released']`), so the player keeps them for the new owner.
136
+ */
137
+ releasePlayer(peer: string): {
138
+ readonly lastApplied: number;
139
+ };
140
+ /**
141
+ * Take `peer`'s inputs from `lastApplied + 1` on: the other half of a
142
+ * planned move. Inputs at or below it — already in the state the entity
143
+ * arrived with — are never applied here.
144
+ *
145
+ * The boundary holds until the player arrives, however late: a player
146
+ * that times out (`playerTimeoutMs`) before its first frame, or that
147
+ * `authorize` refuses for now, keeps it for when it is admitted (the
148
+ * refusal is counted in `dropped['adopt-unauthorized']`).
149
+ *
150
+ * `{ resume: true }` is the crash-recovery form (S6), for an entity
151
+ * restored from an observation with no boundary to hand over: the first
152
+ * input frame from `peer` sets the boundary at its newest input and none
153
+ * of it is applied. The player's pending inputs are dropped (they were
154
+ * made against a state that was rolled back) and it carries on from there.
155
+ */
156
+ adoptPlayer(peer: string, from: {
157
+ readonly lastApplied: number;
158
+ } | {
159
+ readonly resume: true;
160
+ }): void;
104
161
  /** Frames dropped and why. */
105
162
  readonly dropped: Readonly<Record<string, number>>;
106
163
  /** The last error a send or stream open raised, for diagnosis; `null` if none. */
@@ -113,6 +170,6 @@ export interface HostNetcode<E> {
113
170
  * {@link MAX_SNAPSHOT_CHUNKS}; past that the chunks are sent over-size and
114
171
  * ride as fragments).
115
172
  */
116
- export declare function snapshotFrames<E>(label: string, tick: number, time: number, ack: number, entities: Readonly<Record<string, E>>, maxBytes: number, interestVersion?: number): Frame<E, never>[];
173
+ export declare function snapshotFrames<E>(label: string, tick: number, time: number, ack: number, entities: Readonly<Record<string, E>>, maxBytes: number, interestVersion?: number, epoch?: number): Frame<E, never>[];
117
174
  /** Start the authoritative netcode host. */
118
175
  export declare function hostNetcode<E, I>(options: HostNetcodeOptions<E, I>): HostNetcode<E>;