@carverjs/multiplayer 0.0.1

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 (43) hide show
  1. package/dist/NetworkManager-DrKM2tEx.d.mts +369 -0
  2. package/dist/NetworkManager-nvVAOr1O.d.ts +369 -0
  3. package/dist/chunk-3KT73N2S.mjs +655 -0
  4. package/dist/chunk-3KT73N2S.mjs.map +1 -0
  5. package/dist/chunk-EO3YNPRQ.mjs +817 -0
  6. package/dist/chunk-EO3YNPRQ.mjs.map +1 -0
  7. package/dist/chunk-UD6FDZMX.mjs +581 -0
  8. package/dist/chunk-UD6FDZMX.mjs.map +1 -0
  9. package/dist/firebase-CPu87KA0.d.ts +100 -0
  10. package/dist/firebase-PE6MxGdJ.d.mts +100 -0
  11. package/dist/index.d.mts +316 -0
  12. package/dist/index.d.ts +316 -0
  13. package/dist/index.js +3817 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/index.mjs +1743 -0
  16. package/dist/index.mjs.map +1 -0
  17. package/dist/strategy.d.mts +7 -0
  18. package/dist/strategy.d.ts +7 -0
  19. package/dist/strategy.js +619 -0
  20. package/dist/strategy.js.map +1 -0
  21. package/dist/strategy.mjs +11 -0
  22. package/dist/strategy.mjs.map +1 -0
  23. package/dist/sync.d.mts +212 -0
  24. package/dist/sync.d.ts +212 -0
  25. package/dist/sync.js +845 -0
  26. package/dist/sync.js.map +1 -0
  27. package/dist/sync.mjs +11 -0
  28. package/dist/sync.mjs.map +1 -0
  29. package/dist/transport.d.mts +159 -0
  30. package/dist/transport.d.ts +159 -0
  31. package/dist/transport.js +1274 -0
  32. package/dist/transport.js.map +1 -0
  33. package/dist/transport.mjs +19 -0
  34. package/dist/transport.mjs.map +1 -0
  35. package/dist/types-5LHBOW08.d.mts +74 -0
  36. package/dist/types-5LHBOW08.d.ts +74 -0
  37. package/dist/types.d.mts +2 -0
  38. package/dist/types.d.ts +2 -0
  39. package/dist/types.js +19 -0
  40. package/dist/types.js.map +1 -0
  41. package/dist/types.mjs +1 -0
  42. package/dist/types.mjs.map +1 -0
  43. package/package.json +73 -0
package/dist/sync.js ADDED
@@ -0,0 +1,845 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/sync/index.ts
21
+ var sync_exports = {};
22
+ __export(sync_exports, {
23
+ EventSync: () => EventSync,
24
+ PredictionSync: () => PredictionSync,
25
+ SnapshotSync: () => SnapshotSync
26
+ });
27
+ module.exports = __toCommonJS(sync_exports);
28
+
29
+ // src/sync/EventSync.ts
30
+ var EventSync = class {
31
+ constructor(transport, options) {
32
+ this._handlers = /* @__PURE__ */ new Map();
33
+ this._transport = transport;
34
+ this._hostValidation = options?.hostValidation ?? false;
35
+ this._channel = transport.createChannel("carver:events", {
36
+ reliable: true,
37
+ ordered: true
38
+ });
39
+ this._channel.onReceive((rawData, peerId) => {
40
+ try {
41
+ const packet = typeof rawData === "string" ? JSON.parse(rawData) : rawData;
42
+ if (this._hostValidation && this._transport.isHost && packet.sender !== this._transport.peerId) {
43
+ const targets = Array.from(this._transport.peers).filter((p) => p !== peerId);
44
+ if (targets.length > 0) {
45
+ this._channel.send(JSON.stringify(packet), targets);
46
+ }
47
+ }
48
+ if (this._hostValidation && !this._transport.isHost && peerId !== this._transport.hostId) {
49
+ return;
50
+ }
51
+ const handlers = this._handlers.get(packet.type);
52
+ if (handlers) {
53
+ for (const handler of handlers) {
54
+ handler(packet.payload, packet.sender);
55
+ }
56
+ }
57
+ } catch {
58
+ }
59
+ });
60
+ }
61
+ /**
62
+ * Send a typed event to a specific peer or all peers.
63
+ */
64
+ sendEvent(type, payload, target) {
65
+ const packet = {
66
+ type,
67
+ payload,
68
+ sender: this._transport.peerId,
69
+ target
70
+ };
71
+ const serialized = JSON.stringify(packet);
72
+ if (this._hostValidation && !this._transport.isHost) {
73
+ this._channel.send(serialized, this._transport.hostId);
74
+ } else if (target) {
75
+ this._channel.send(serialized, target);
76
+ } else {
77
+ this._channel.send(serialized);
78
+ }
79
+ }
80
+ /**
81
+ * Broadcast a typed event to all connected peers.
82
+ */
83
+ broadcast(type, payload) {
84
+ this.sendEvent(type, payload);
85
+ }
86
+ /**
87
+ * Register a handler for a specific event type.
88
+ * Returns an unsubscribe function.
89
+ */
90
+ onEvent(type, callback) {
91
+ let handlers = this._handlers.get(type);
92
+ if (!handlers) {
93
+ handlers = [];
94
+ this._handlers.set(type, handlers);
95
+ }
96
+ handlers.push(callback);
97
+ return () => {
98
+ const arr = this._handlers.get(type);
99
+ if (arr) {
100
+ const idx = arr.indexOf(callback);
101
+ if (idx >= 0) arr.splice(idx, 1);
102
+ if (arr.length === 0) this._handlers.delete(type);
103
+ }
104
+ };
105
+ }
106
+ /**
107
+ * Clean up the event channel.
108
+ */
109
+ destroy() {
110
+ this._channel.close();
111
+ this._handlers.clear();
112
+ }
113
+ };
114
+
115
+ // src/core/HostAuthority.ts
116
+ var HostAuthority = class {
117
+ constructor(transport, codec, snapshotBuffer, options) {
118
+ this._tick = 0;
119
+ this._broadcastAccumulator = 0;
120
+ // Per-client last ACK'd tick for delta compression
121
+ this._clientBaselines = /* @__PURE__ */ new Map();
122
+ // Per-client last keyframe tick for scheduling
123
+ this._clientLastKeyframeTick = /* @__PURE__ */ new Map();
124
+ // Interest management callback (optional)
125
+ this._interestFilter = null;
126
+ this._transport = transport;
127
+ this._codec = codec;
128
+ this._snapshotBuffer = snapshotBuffer;
129
+ this._broadcastRate = options?.broadcastRate ?? 20;
130
+ this._keyframeInterval = options?.keyframeInterval ?? 300;
131
+ this._snapshotChannel = transport.createChannel(
132
+ "carver:snapshots",
133
+ {
134
+ reliable: false,
135
+ ordered: false,
136
+ maxRetransmits: 0
137
+ }
138
+ );
139
+ this._ackChannel = transport.createChannel("carver:acks", {
140
+ reliable: true,
141
+ ordered: true
142
+ });
143
+ this._ackChannel.onReceive((data, peerId) => {
144
+ try {
145
+ const ackTick = typeof data === "string" ? parseInt(data, 10) : data;
146
+ if (ackTick === -1) {
147
+ this._clientBaselines.delete(peerId);
148
+ } else {
149
+ this._clientBaselines.set(peerId, ackTick);
150
+ }
151
+ } catch {
152
+ }
153
+ });
154
+ transport.onPeerJoin((peerId) => {
155
+ this._clientBaselines.delete(peerId);
156
+ });
157
+ transport.onPeerLeave((peerId) => {
158
+ this._clientBaselines.delete(peerId);
159
+ });
160
+ }
161
+ /** Set optional interest management filter */
162
+ setInterestFilter(filter) {
163
+ this._interestFilter = filter;
164
+ }
165
+ /**
166
+ * Called every fixed tick by the sync engine.
167
+ * Collects entity states and decides whether to broadcast.
168
+ */
169
+ tick(currentTick, entities, delta) {
170
+ this._tick = currentTick;
171
+ this._snapshotBuffer.store(currentTick, new Map(entities));
172
+ this._broadcastAccumulator += delta;
173
+ const broadcastInterval = 1 / this._broadcastRate;
174
+ if (this._broadcastAccumulator < broadcastInterval) return;
175
+ this._broadcastAccumulator -= broadcastInterval;
176
+ for (const peerId of this._transport.peers) {
177
+ this._broadcastToClient(peerId, currentTick, entities);
178
+ }
179
+ }
180
+ /** Force a keyframe broadcast to all clients (e.g., after host migration) */
181
+ forceKeyframe(currentTick, entities) {
182
+ this._clientBaselines.clear();
183
+ this._clientLastKeyframeTick.clear();
184
+ this._snapshotBuffer.store(currentTick, new Map(entities));
185
+ for (const peerId of this._transport.peers) {
186
+ this._broadcastToClient(peerId, currentTick, entities);
187
+ }
188
+ }
189
+ destroy() {
190
+ this._snapshotChannel.close();
191
+ this._ackChannel.close();
192
+ this._clientBaselines.clear();
193
+ this._clientLastKeyframeTick.clear();
194
+ }
195
+ _broadcastToClient(peerId, currentTick, entities) {
196
+ let clientEntities = entities;
197
+ if (this._interestFilter) {
198
+ clientEntities = /* @__PURE__ */ new Map();
199
+ for (const [id, entity] of entities) {
200
+ if (this._interestFilter(id, peerId)) {
201
+ clientEntities.set(id, entity);
202
+ }
203
+ }
204
+ }
205
+ const clientBaseTick = this._clientBaselines.get(peerId);
206
+ const clientLastKeyframe = this._clientLastKeyframeTick.get(peerId) ?? 0;
207
+ const needsKeyframe = clientBaseTick === void 0 || currentTick - clientLastKeyframe >= this._keyframeInterval;
208
+ let baseline;
209
+ if (!needsKeyframe && clientBaseTick !== void 0) {
210
+ baseline = this._snapshotBuffer.get(clientBaseTick);
211
+ }
212
+ if (needsKeyframe) {
213
+ this._clientLastKeyframeTick.set(peerId, currentTick);
214
+ }
215
+ const packet = this._codec.serializeDelta(
216
+ currentTick,
217
+ needsKeyframe ? -1 : clientBaseTick ?? -1,
218
+ clientEntities,
219
+ baseline
220
+ );
221
+ if (packet) {
222
+ this._snapshotChannel.send(packet, peerId);
223
+ }
224
+ }
225
+ };
226
+
227
+ // src/core/ClientReceiver.ts
228
+ var ClientReceiver = class {
229
+ constructor(transport, codec, options) {
230
+ // Snapshot buffer (ring buffer of last N snapshots)
231
+ this._buffer = [];
232
+ // Current interpolated state
233
+ this._interpolatedState = /* @__PURE__ */ new Map();
234
+ // Network quality tracking
235
+ this._lastSnapshotTime = 0;
236
+ this._networkQuality = "good";
237
+ this._packetLossCount = 0;
238
+ this._packetCount = 0;
239
+ // Entity state: full accumulated state from keyframes + deltas
240
+ this._fullState = /* @__PURE__ */ new Map();
241
+ this._transport = transport;
242
+ this._codec = codec;
243
+ this._bufferSize = options?.bufferSize ?? 3;
244
+ this._method = options?.method ?? "hermite";
245
+ this._extrapolateMs = options?.extrapolateMs ?? 250;
246
+ this._is2D = options?.is2D ?? false;
247
+ this._snapshotChannel = transport.createChannel(
248
+ "carver:snapshots",
249
+ {
250
+ reliable: false,
251
+ ordered: false,
252
+ maxRetransmits: 0
253
+ }
254
+ );
255
+ this._ackChannel = transport.createChannel("carver:acks", {
256
+ reliable: true,
257
+ ordered: true
258
+ });
259
+ this._snapshotChannel.onReceive((data) => {
260
+ this._handleSnapshot(data);
261
+ });
262
+ }
263
+ /** Get the current interpolated entity states */
264
+ get state() {
265
+ return this._interpolatedState;
266
+ }
267
+ get networkQuality() {
268
+ return this._networkQuality;
269
+ }
270
+ /**
271
+ * Called every render frame to interpolate between buffered snapshots.
272
+ * @param renderTime - current render time in ms
273
+ */
274
+ interpolate(renderTime) {
275
+ if (this._buffer.length < 2) {
276
+ return this._buffer.length > 0 ? this._buffer[this._buffer.length - 1].entities : this._interpolatedState;
277
+ }
278
+ const interpDelay = (this._bufferSize - 1) * (1e3 / 20);
279
+ const targetTime = renderTime - interpDelay;
280
+ let from = null;
281
+ let to = null;
282
+ for (let i = 0; i < this._buffer.length - 1; i++) {
283
+ if (this._buffer[i].receivedAt <= targetTime && this._buffer[i + 1].receivedAt > targetTime) {
284
+ from = this._buffer[i];
285
+ to = this._buffer[i + 1];
286
+ break;
287
+ }
288
+ }
289
+ if (!from || !to) {
290
+ const latest = this._buffer[this._buffer.length - 1];
291
+ const timeSinceLatest = renderTime - latest.receivedAt;
292
+ if (timeSinceLatest > this._extrapolateMs) {
293
+ this._updateNetworkQuality("poor");
294
+ return latest.entities;
295
+ }
296
+ if (this._buffer.length >= 2) {
297
+ from = this._buffer[this._buffer.length - 2];
298
+ to = latest;
299
+ this._updateNetworkQuality("degraded");
300
+ } else {
301
+ return latest.entities;
302
+ }
303
+ } else {
304
+ this._updateNetworkQuality("good");
305
+ }
306
+ const range = to.receivedAt - from.receivedAt;
307
+ const t = range > 0 ? Math.min(1, Math.max(0, (targetTime - from.receivedAt) / range)) : 1;
308
+ const result = /* @__PURE__ */ new Map();
309
+ const allIds = /* @__PURE__ */ new Set([...from.entities.keys(), ...to.entities.keys()]);
310
+ for (const id of allIds) {
311
+ const fromEntity = from.entities.get(id);
312
+ const toEntity = to.entities.get(id);
313
+ if (toEntity && toEntity.c?.__removed) continue;
314
+ if (fromEntity && toEntity) {
315
+ result.set(id, this._interpolateEntity(fromEntity, toEntity, t));
316
+ } else if (toEntity) {
317
+ result.set(id, toEntity);
318
+ }
319
+ }
320
+ this._interpolatedState = result;
321
+ return result;
322
+ }
323
+ /** Request a keyframe from the host */
324
+ requestKeyframe() {
325
+ this._ackChannel.send("-1");
326
+ }
327
+ destroy() {
328
+ this._snapshotChannel.close();
329
+ this._ackChannel.close();
330
+ this._buffer = [];
331
+ this._interpolatedState.clear();
332
+ this._fullState.clear();
333
+ }
334
+ _handleSnapshot(data) {
335
+ try {
336
+ const { tick, baseTick, entities } = this._codec.deserializePacket(data);
337
+ const now = performance.now();
338
+ if (baseTick === -1) {
339
+ this._fullState.clear();
340
+ for (const entity of entities) {
341
+ this._fullState.set(entity.id, entity);
342
+ }
343
+ } else {
344
+ for (const entity of entities) {
345
+ if (entity.c?.__removed) {
346
+ this._fullState.delete(entity.id);
347
+ } else {
348
+ this._fullState.set(entity.id, entity);
349
+ }
350
+ }
351
+ }
352
+ this._buffer.push({
353
+ tick,
354
+ entities: new Map(this._fullState),
355
+ receivedAt: now
356
+ });
357
+ while (this._buffer.length > this._bufferSize * 2) {
358
+ this._buffer.shift();
359
+ }
360
+ this._ackChannel.send(String(tick));
361
+ this._lastSnapshotTime = now;
362
+ this._packetCount++;
363
+ } catch {
364
+ this._packetLossCount++;
365
+ }
366
+ }
367
+ _interpolateEntity(from, to, t) {
368
+ if (this._is2D || !("z" in from)) {
369
+ return this._interpolateEntity2D(
370
+ from,
371
+ to,
372
+ t
373
+ );
374
+ }
375
+ return this._interpolateEntity3D(
376
+ from,
377
+ to,
378
+ t
379
+ );
380
+ }
381
+ _interpolateEntity2D(from, to, t) {
382
+ if (this._method === "hermite") {
383
+ return {
384
+ id: to.id,
385
+ x: hermite(from.x, from.vx, to.x, to.vx, t),
386
+ y: hermite(from.y, from.vy, to.y, to.vy, t),
387
+ a: lerpAngle(from.a, to.a, t),
388
+ vx: lerp(from.vx, to.vx, t),
389
+ vy: lerp(from.vy, to.vy, t),
390
+ va: lerp(from.va, to.va, t),
391
+ c: interpolateCustom(from.c, to.c, t)
392
+ };
393
+ }
394
+ return {
395
+ id: to.id,
396
+ x: lerp(from.x, to.x, t),
397
+ y: lerp(from.y, to.y, t),
398
+ a: lerpAngle(from.a, to.a, t),
399
+ vx: lerp(from.vx, to.vx, t),
400
+ vy: lerp(from.vy, to.vy, t),
401
+ va: lerp(from.va, to.va, t),
402
+ c: interpolateCustom(from.c, to.c, t)
403
+ };
404
+ }
405
+ _interpolateEntity3D(from, to, t) {
406
+ const [qx, qy, qz, qw] = slerp(
407
+ from.qx,
408
+ from.qy,
409
+ from.qz,
410
+ from.qw,
411
+ to.qx,
412
+ to.qy,
413
+ to.qz,
414
+ to.qw,
415
+ t
416
+ );
417
+ if (this._method === "hermite") {
418
+ return {
419
+ id: to.id,
420
+ x: hermite(from.x, from.vx, to.x, to.vx, t),
421
+ y: hermite(from.y, from.vy, to.y, to.vy, t),
422
+ z: hermite(from.z, from.vz, to.z, to.vz, t),
423
+ qx,
424
+ qy,
425
+ qz,
426
+ qw,
427
+ vx: lerp(from.vx, to.vx, t),
428
+ vy: lerp(from.vy, to.vy, t),
429
+ vz: lerp(from.vz, to.vz, t),
430
+ wx: lerp(from.wx, to.wx, t),
431
+ wy: lerp(from.wy, to.wy, t),
432
+ wz: lerp(from.wz, to.wz, t),
433
+ c: interpolateCustom(from.c, to.c, t)
434
+ };
435
+ }
436
+ return {
437
+ id: to.id,
438
+ x: lerp(from.x, to.x, t),
439
+ y: lerp(from.y, to.y, t),
440
+ z: lerp(from.z, to.z, t),
441
+ qx,
442
+ qy,
443
+ qz,
444
+ qw,
445
+ vx: lerp(from.vx, to.vx, t),
446
+ vy: lerp(from.vy, to.vy, t),
447
+ vz: lerp(from.vz, to.vz, t),
448
+ wx: lerp(from.wx, to.wx, t),
449
+ wy: lerp(from.wy, to.wy, t),
450
+ wz: lerp(from.wz, to.wz, t),
451
+ c: interpolateCustom(from.c, to.c, t)
452
+ };
453
+ }
454
+ _updateNetworkQuality(quality) {
455
+ this._networkQuality = quality;
456
+ }
457
+ };
458
+ function lerp(a, b, t) {
459
+ return a + (b - a) * t;
460
+ }
461
+ function lerpAngle(a, b, t) {
462
+ let diff = b - a;
463
+ while (diff > Math.PI) diff -= Math.PI * 2;
464
+ while (diff < -Math.PI) diff += Math.PI * 2;
465
+ return a + diff * t;
466
+ }
467
+ function hermite(p0, v0, p1, v1, t) {
468
+ const t2 = t * t;
469
+ const t3 = t2 * t;
470
+ return (2 * t3 - 3 * t2 + 1) * p0 + (t3 - 2 * t2 + t) * v0 + (-2 * t3 + 3 * t2) * p1 + (t3 - t2) * v1;
471
+ }
472
+ function slerp(ax, ay, az, aw, bx, by, bz, bw, t) {
473
+ let dot = ax * bx + ay * by + az * bz + aw * bw;
474
+ if (dot < 0) {
475
+ bx = -bx;
476
+ by = -by;
477
+ bz = -bz;
478
+ bw = -bw;
479
+ dot = -dot;
480
+ }
481
+ if (dot > 0.9995) {
482
+ return [lerp(ax, bx, t), lerp(ay, by, t), lerp(az, bz, t), lerp(aw, bw, t)];
483
+ }
484
+ const theta = Math.acos(Math.min(1, Math.max(-1, dot)));
485
+ const sinTheta = Math.sin(theta);
486
+ const wa = Math.sin((1 - t) * theta) / sinTheta;
487
+ const wb = Math.sin(t * theta) / sinTheta;
488
+ return [
489
+ ax * wa + bx * wb,
490
+ ay * wa + by * wb,
491
+ az * wa + bz * wb,
492
+ aw * wa + bw * wb
493
+ ];
494
+ }
495
+ function interpolateCustom(from, to, t) {
496
+ if (!from && !to) return void 0;
497
+ if (!from) return to;
498
+ if (!to) return from;
499
+ const result = {};
500
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(from), ...Object.keys(to)]);
501
+ for (const key of allKeys) {
502
+ const fromVal = from[key];
503
+ const toVal = to[key];
504
+ if (typeof fromVal === "number" && typeof toVal === "number") {
505
+ result[key] = lerp(fromVal, toVal, t);
506
+ } else {
507
+ result[key] = t >= 0.5 ? toVal : fromVal;
508
+ }
509
+ }
510
+ return result;
511
+ }
512
+
513
+ // src/sync/SnapshotSync.ts
514
+ var SnapshotSync = class {
515
+ constructor(transport, codec, snapshotBuffer, options) {
516
+ this._hostAuthority = null;
517
+ this._clientReceiver = null;
518
+ this._transport = transport;
519
+ this._codec = codec;
520
+ this._snapshotBuffer = snapshotBuffer;
521
+ if (transport.isHost) {
522
+ this._hostAuthority = new HostAuthority(
523
+ transport,
524
+ codec,
525
+ snapshotBuffer,
526
+ {
527
+ broadcastRate: options?.broadcastRate,
528
+ keyframeInterval: options?.keyframeInterval
529
+ }
530
+ );
531
+ } else {
532
+ this._clientReceiver = new ClientReceiver(transport, codec, {
533
+ bufferSize: options?.bufferSize,
534
+ method: options?.interpolationMethod,
535
+ extrapolateMs: options?.extrapolateMs,
536
+ is2D: options?.is2D
537
+ });
538
+ }
539
+ }
540
+ get isHost() {
541
+ return this._hostAuthority !== null;
542
+ }
543
+ get hostAuthority() {
544
+ return this._hostAuthority;
545
+ }
546
+ get clientReceiver() {
547
+ return this._clientReceiver;
548
+ }
549
+ /** Host: called every fixed tick to potentially broadcast state */
550
+ hostTick(tick, entities, delta) {
551
+ this._hostAuthority?.tick(tick, entities, delta);
552
+ }
553
+ /** Client: called every render frame to interpolate */
554
+ clientInterpolate(renderTime) {
555
+ return this._clientReceiver?.interpolate(renderTime) ?? /* @__PURE__ */ new Map();
556
+ }
557
+ /** Set interest filter on host authority */
558
+ setInterestFilter(filter) {
559
+ this._hostAuthority?.setInterestFilter(filter);
560
+ }
561
+ /** Handle host migration: switch from client to host mode */
562
+ promoteToHost(options) {
563
+ this._clientReceiver?.destroy();
564
+ this._clientReceiver = null;
565
+ this._hostAuthority = new HostAuthority(
566
+ this._transport,
567
+ this._codec,
568
+ this._snapshotBuffer,
569
+ {
570
+ broadcastRate: options?.broadcastRate,
571
+ keyframeInterval: options?.keyframeInterval
572
+ }
573
+ );
574
+ }
575
+ /** Handle host migration: switch from host to client mode */
576
+ demoteToClient(options) {
577
+ this._hostAuthority?.destroy();
578
+ this._hostAuthority = null;
579
+ this._clientReceiver = new ClientReceiver(
580
+ this._transport,
581
+ this._codec,
582
+ {
583
+ bufferSize: options?.bufferSize,
584
+ method: options?.interpolationMethod,
585
+ extrapolateMs: options?.extrapolateMs,
586
+ is2D: options?.is2D
587
+ }
588
+ );
589
+ }
590
+ destroy() {
591
+ this._hostAuthority?.destroy();
592
+ this._clientReceiver?.destroy();
593
+ this._hostAuthority = null;
594
+ this._clientReceiver = null;
595
+ }
596
+ };
597
+
598
+ // src/sync/PredictionSync.ts
599
+ var import_msgpackr = require("msgpackr");
600
+ var DEFAULT_OPTIONS = {
601
+ maxRewindTicks: 15,
602
+ errorSmoothingDecay: 0.85,
603
+ maxErrorPerFrame: 5,
604
+ snapThreshold: 15,
605
+ lagCompensation: false
606
+ };
607
+ var PredictionSync = class {
608
+ constructor(transport, codec, tickKeeper, options) {
609
+ // Input buffer: ring buffer of recent inputs keyed by tick
610
+ this._inputBuffer = [];
611
+ // Per-client last processed input tick (host-side tracking)
612
+ this._clientLastProcessedTick = /* @__PURE__ */ new Map();
613
+ // Predicted state (client-side)
614
+ this._predictedState = /* @__PURE__ */ new Map();
615
+ // Error correction vectors per entity
616
+ this._errorCorrections = /* @__PURE__ */ new Map();
617
+ // Server state (last received authoritative snapshot)
618
+ this._serverState = /* @__PURE__ */ new Map();
619
+ this._serverTick = 0;
620
+ // Physics step callback (provided by developer)
621
+ this._onPhysicsStep = null;
622
+ // Own input for current tick
623
+ this._currentInput = null;
624
+ this._transport = transport;
625
+ this._codec = codec;
626
+ this._tickKeeper = tickKeeper;
627
+ this._options = { ...DEFAULT_OPTIONS, ...options };
628
+ this._isHost = transport.isHost;
629
+ this._inputChannel = transport.createChannel("carver:inputs", {
630
+ reliable: true,
631
+ ordered: true
632
+ });
633
+ this._stateChannel = transport.createChannel("carver:pred-state", {
634
+ reliable: false,
635
+ ordered: false,
636
+ maxRetransmits: 0
637
+ });
638
+ this._ackChannel = transport.createChannel("carver:pred-acks", {
639
+ reliable: true,
640
+ ordered: true
641
+ });
642
+ if (this._isHost) {
643
+ this._setupHostListeners();
644
+ } else {
645
+ this._setupClientListeners();
646
+ }
647
+ }
648
+ /** Set the physics step callback (required for rollback re-simulation) */
649
+ setPhysicsStep(cb) {
650
+ this._onPhysicsStep = cb;
651
+ }
652
+ /** Set the current input for this tick (client-side) */
653
+ setInput(input) {
654
+ this._currentInput = input;
655
+ }
656
+ /**
657
+ * Called every fixed tick on the client.
658
+ * Applies input locally (prediction), buffers it, and sends to host.
659
+ */
660
+ clientTick(tick) {
661
+ if (this._isHost) return;
662
+ if (this._currentInput !== null) {
663
+ this._inputBuffer.push({ tick, input: this._currentInput });
664
+ const packet = {
665
+ t: tick,
666
+ i: this._currentInput,
667
+ p: this._transport.peerId
668
+ };
669
+ this._inputChannel.send(JSON.stringify(packet));
670
+ if (this._onPhysicsStep) {
671
+ const inputs = /* @__PURE__ */ new Map();
672
+ inputs.set(this._transport.peerId, this._currentInput);
673
+ this._onPhysicsStep(inputs, tick, false);
674
+ }
675
+ this._currentInput = null;
676
+ }
677
+ const minTick = tick - this._options.maxRewindTicks * 2;
678
+ while (this._inputBuffer.length > 0 && this._inputBuffer[0].tick < minTick) {
679
+ this._inputBuffer.shift();
680
+ }
681
+ }
682
+ /**
683
+ * Called every fixed tick on the host.
684
+ * Processes received inputs and broadcasts authoritative state.
685
+ */
686
+ hostTick(tick, entities, _delta) {
687
+ if (!this._isHost) return;
688
+ const stateArray = Array.from(entities.values());
689
+ const data = this._codec.serialize(stateArray);
690
+ for (const peerId of this._transport.peers) {
691
+ const lastTick = this._clientLastProcessedTick.get(peerId) ?? -1;
692
+ const packet = {
693
+ t: tick,
694
+ s: data,
695
+ li: lastTick
696
+ };
697
+ this._stateChannel.send((0, import_msgpackr.pack)(packet), peerId);
698
+ }
699
+ }
700
+ /**
701
+ * Called every render frame on the client to apply visual error smoothing.
702
+ * Returns the corrected entity states.
703
+ */
704
+ applyErrorSmoothing(entities) {
705
+ const result = /* @__PURE__ */ new Map();
706
+ const decay = this._options.errorSmoothingDecay;
707
+ for (const [id, entity] of entities) {
708
+ const correction = this._errorCorrections.get(id);
709
+ if (!correction) {
710
+ result.set(id, entity);
711
+ continue;
712
+ }
713
+ const corrected = { ...entity };
714
+ corrected.x += correction.x;
715
+ corrected.y += correction.y;
716
+ if ("z" in corrected) {
717
+ corrected.z += correction.z;
718
+ }
719
+ correction.x *= decay;
720
+ correction.y *= decay;
721
+ correction.z *= decay;
722
+ const mag = Math.abs(correction.x) + Math.abs(correction.y) + Math.abs(correction.z);
723
+ if (mag < 1e-3) {
724
+ this._errorCorrections.delete(id);
725
+ }
726
+ result.set(id, corrected);
727
+ }
728
+ return result;
729
+ }
730
+ get predictedState() {
731
+ return this._predictedState;
732
+ }
733
+ get serverTick() {
734
+ return this._serverTick;
735
+ }
736
+ destroy() {
737
+ this._inputChannel.close();
738
+ this._stateChannel.close();
739
+ this._ackChannel.close();
740
+ this._inputBuffer = [];
741
+ this._predictedState.clear();
742
+ this._errorCorrections.clear();
743
+ this._serverState.clear();
744
+ }
745
+ // ── Private: Host-side ──
746
+ _setupHostListeners() {
747
+ this._inputChannel.onReceive((rawData, peerId) => {
748
+ try {
749
+ const packet = JSON.parse(rawData);
750
+ if (this._onPhysicsStep) {
751
+ const inputs = /* @__PURE__ */ new Map();
752
+ inputs.set(peerId, packet.i);
753
+ this._onPhysicsStep(inputs, packet.t, false);
754
+ }
755
+ const prevTick = this._clientLastProcessedTick.get(peerId) ?? -1;
756
+ this._clientLastProcessedTick.set(peerId, Math.max(prevTick, packet.t));
757
+ } catch (err) {
758
+ if (typeof console !== "undefined") console.debug("[CarverJS] Malformed input packet:", err);
759
+ }
760
+ });
761
+ }
762
+ // ── Private: Client-side ──
763
+ _setupClientListeners() {
764
+ this._stateChannel.onReceive((data) => {
765
+ try {
766
+ const packet = (0, import_msgpackr.unpack)(data);
767
+ const entities = this._codec.deserialize(packet.s);
768
+ const serverTick = packet.t;
769
+ const lastInputTick = packet.li;
770
+ this._serverTick = serverTick;
771
+ this._tickKeeper.setServerTick(serverTick);
772
+ this._serverState.clear();
773
+ for (const entity of entities) {
774
+ this._serverState.set(entity.id, entity);
775
+ }
776
+ this._reconcile(lastInputTick);
777
+ } catch (err) {
778
+ if (typeof console !== "undefined") console.debug("[CarverJS] Malformed state packet:", err);
779
+ }
780
+ });
781
+ }
782
+ _reconcile(lastInputTick) {
783
+ this._inputBuffer = this._inputBuffer.filter((entry) => entry.tick > lastInputTick);
784
+ let needsRollback = false;
785
+ let maxError = 0;
786
+ for (const [id, serverEntity] of this._serverState) {
787
+ const predicted = this._predictedState.get(id);
788
+ if (!predicted) continue;
789
+ const error = this._computeError(predicted, serverEntity);
790
+ maxError = Math.max(maxError, error);
791
+ if (error > this._options.maxErrorPerFrame) {
792
+ needsRollback = true;
793
+ }
794
+ }
795
+ if (maxError > this._options.snapThreshold) {
796
+ this._predictedState = new Map(this._serverState);
797
+ this._errorCorrections.clear();
798
+ return;
799
+ }
800
+ if (needsRollback) {
801
+ const oldPositions = /* @__PURE__ */ new Map();
802
+ for (const [id, entity] of this._predictedState) {
803
+ oldPositions.set(id, {
804
+ x: entity.x,
805
+ y: entity.y,
806
+ z: "z" in entity ? entity.z : 0
807
+ });
808
+ }
809
+ this._predictedState = new Map(this._serverState);
810
+ if (this._onPhysicsStep) {
811
+ for (const entry of this._inputBuffer) {
812
+ const inputs = /* @__PURE__ */ new Map();
813
+ inputs.set(this._transport.peerId, entry.input);
814
+ this._onPhysicsStep(inputs, entry.tick, true);
815
+ }
816
+ }
817
+ for (const [id, newEntity] of this._predictedState) {
818
+ const oldPos = oldPositions.get(id);
819
+ if (oldPos) {
820
+ this._errorCorrections.set(id, {
821
+ x: oldPos.x - newEntity.x,
822
+ y: oldPos.y - newEntity.y,
823
+ z: oldPos.z - ("z" in newEntity ? newEntity.z : 0)
824
+ });
825
+ }
826
+ }
827
+ }
828
+ }
829
+ _computeError(predicted, server) {
830
+ const dx = predicted.x - server.x;
831
+ const dy = predicted.y - server.y;
832
+ let dz = 0;
833
+ if ("z" in predicted && "z" in server) {
834
+ dz = predicted.z - server.z;
835
+ }
836
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
837
+ }
838
+ };
839
+ // Annotate the CommonJS export names for ESM import in node:
840
+ 0 && (module.exports = {
841
+ EventSync,
842
+ PredictionSync,
843
+ SnapshotSync
844
+ });
845
+ //# sourceMappingURL=sync.js.map