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