@carverjs/multiplayer 0.0.1 → 0.0.3

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/README.md +154 -0
  2. package/dist/InputBuffer-J6XT_Tt0.d.mts +61 -0
  3. package/dist/InputBuffer-V7XfHbc6.d.ts +61 -0
  4. package/dist/{NetworkManager-nvVAOr1O.d.ts → NetworkManager-D-DxFgdM.d.mts} +66 -14
  5. package/dist/{NetworkManager-DrKM2tEx.d.mts → NetworkManager-DH9uGVMg.d.ts} +66 -14
  6. package/dist/{chunk-UD6FDZMX.mjs → chunk-GOTAQDBJ.mjs} +47 -4
  7. package/dist/chunk-GOTAQDBJ.mjs.map +1 -0
  8. package/dist/{chunk-3KT73N2S.mjs → chunk-LPNEP2VH.mjs} +0 -0
  9. package/dist/chunk-LPNEP2VH.mjs.map +1 -0
  10. package/dist/{chunk-EO3YNPRQ.mjs → chunk-Q25TJEY4.mjs} +494 -204
  11. package/dist/chunk-Q25TJEY4.mjs.map +1 -0
  12. package/dist/{firebase-CPu87KA0.d.ts → firebase-B5MgLlHk.d.ts} +6 -1
  13. package/dist/{firebase-PE6MxGdJ.d.mts → firebase-GrbVrNgs.d.mts} +6 -1
  14. package/dist/index.d.mts +27 -6
  15. package/dist/index.d.ts +27 -6
  16. package/dist/index.js +821 -258
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.mjs +172 -37
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/strategy.d.mts +2 -2
  21. package/dist/strategy.d.ts +2 -2
  22. package/dist/strategy.js +46 -3
  23. package/dist/strategy.js.map +1 -1
  24. package/dist/strategy.mjs +1 -1
  25. package/dist/sync.d.mts +134 -50
  26. package/dist/sync.d.ts +134 -50
  27. package/dist/sync.js +499 -205
  28. package/dist/sync.js.map +1 -1
  29. package/dist/sync.mjs +15 -3
  30. package/dist/transport.d.mts +0 -0
  31. package/dist/transport.d.ts +0 -0
  32. package/dist/transport.js +0 -0
  33. package/dist/transport.js.map +1 -1
  34. package/dist/transport.mjs +2 -2
  35. package/dist/{types-5LHBOW08.d.mts → types-hNfCIBzj.d.mts} +7 -0
  36. package/dist/{types-5LHBOW08.d.ts → types-hNfCIBzj.d.ts} +7 -0
  37. package/dist/types.d.mts +2 -2
  38. package/dist/types.d.ts +2 -2
  39. package/dist/types.js.map +1 -1
  40. package/package.json +26 -5
  41. package/dist/chunk-3KT73N2S.mjs.map +0 -1
  42. package/dist/chunk-EO3YNPRQ.mjs.map +0 -1
  43. package/dist/chunk-UD6FDZMX.mjs.map +0 -1
package/dist/sync.js CHANGED
@@ -22,7 +22,13 @@ var sync_exports = {};
22
22
  __export(sync_exports, {
23
23
  EventSync: () => EventSync,
24
24
  PredictionSync: () => PredictionSync,
25
- SnapshotSync: () => SnapshotSync
25
+ SnapshotSync: () => SnapshotSync,
26
+ applyRollback: () => applyRollback,
27
+ quatAngle: () => quatAngle,
28
+ quatInvert: () => quatInvert,
29
+ quatMultiply: () => quatMultiply,
30
+ quatNormalize: () => quatNormalize,
31
+ quatScaleAngle: () => quatScaleAngle
26
32
  });
27
33
  module.exports = __toCommonJS(sync_exports);
28
34
 
@@ -165,8 +171,9 @@ var HostAuthority = class {
165
171
  /**
166
172
  * Called every fixed tick by the sync engine.
167
173
  * Collects entity states and decides whether to broadcast.
174
+ * `hostInput` (prediction mode) is embedded in the snapshot packet as `hi`.
168
175
  */
169
- tick(currentTick, entities, delta) {
176
+ tick(currentTick, entities, delta, hostInput) {
170
177
  this._tick = currentTick;
171
178
  this._snapshotBuffer.store(currentTick, new Map(entities));
172
179
  this._broadcastAccumulator += delta;
@@ -174,16 +181,16 @@ var HostAuthority = class {
174
181
  if (this._broadcastAccumulator < broadcastInterval) return;
175
182
  this._broadcastAccumulator -= broadcastInterval;
176
183
  for (const peerId of this._transport.peers) {
177
- this._broadcastToClient(peerId, currentTick, entities);
184
+ this._broadcastToClient(peerId, currentTick, entities, hostInput);
178
185
  }
179
186
  }
180
187
  /** Force a keyframe broadcast to all clients (e.g., after host migration) */
181
- forceKeyframe(currentTick, entities) {
188
+ forceKeyframe(currentTick, entities, hostInput) {
182
189
  this._clientBaselines.clear();
183
190
  this._clientLastKeyframeTick.clear();
184
191
  this._snapshotBuffer.store(currentTick, new Map(entities));
185
192
  for (const peerId of this._transport.peers) {
186
- this._broadcastToClient(peerId, currentTick, entities);
193
+ this._broadcastToClient(peerId, currentTick, entities, hostInput);
187
194
  }
188
195
  }
189
196
  destroy() {
@@ -192,7 +199,7 @@ var HostAuthority = class {
192
199
  this._clientBaselines.clear();
193
200
  this._clientLastKeyframeTick.clear();
194
201
  }
195
- _broadcastToClient(peerId, currentTick, entities) {
202
+ _broadcastToClient(peerId, currentTick, entities, hostInput) {
196
203
  let clientEntities = entities;
197
204
  if (this._interestFilter) {
198
205
  clientEntities = /* @__PURE__ */ new Map();
@@ -216,7 +223,8 @@ var HostAuthority = class {
216
223
  currentTick,
217
224
  needsKeyframe ? -1 : clientBaseTick ?? -1,
218
225
  clientEntities,
219
- baseline
226
+ baseline,
227
+ hostInput
220
228
  );
221
229
  if (packet) {
222
230
  this._snapshotChannel.send(packet, peerId);
@@ -238,6 +246,8 @@ var ClientReceiver = class {
238
246
  this._packetCount = 0;
239
247
  // Entity state: full accumulated state from keyframes + deltas
240
248
  this._fullState = /* @__PURE__ */ new Map();
249
+ // Listeners fired after each snapshot is merged into the full world state
250
+ this._snapshotListeners = [];
241
251
  this._transport = transport;
242
252
  this._codec = codec;
243
253
  this._bufferSize = options?.bufferSize ?? 3;
@@ -324,16 +334,21 @@ var ClientReceiver = class {
324
334
  requestKeyframe() {
325
335
  this._ackChannel.send("-1");
326
336
  }
337
+ /** Register a listener fired after each snapshot is merged into the full world state */
338
+ onSnapshot(cb) {
339
+ this._snapshotListeners.push(cb);
340
+ }
327
341
  destroy() {
328
342
  this._snapshotChannel.close();
329
343
  this._ackChannel.close();
330
344
  this._buffer = [];
331
345
  this._interpolatedState.clear();
332
346
  this._fullState.clear();
347
+ this._snapshotListeners = [];
333
348
  }
334
349
  _handleSnapshot(data) {
335
350
  try {
336
- const { tick, baseTick, entities } = this._codec.deserializePacket(data);
351
+ const { tick, baseTick, entities, hostInput } = this._codec.deserializePacket(data);
337
352
  const now = performance.now();
338
353
  if (baseTick === -1) {
339
354
  this._fullState.clear();
@@ -349,15 +364,19 @@ var ClientReceiver = class {
349
364
  }
350
365
  }
351
366
  }
367
+ const fullStateClone = new Map(this._fullState);
352
368
  this._buffer.push({
353
369
  tick,
354
- entities: new Map(this._fullState),
370
+ entities: fullStateClone,
355
371
  receivedAt: now
356
372
  });
357
373
  while (this._buffer.length > this._bufferSize * 2) {
358
374
  this._buffer.shift();
359
375
  }
360
376
  this._ackChannel.send(String(tick));
377
+ for (const cb of this._snapshotListeners) {
378
+ cb(tick, fullStateClone, hostInput);
379
+ }
361
380
  this._lastSnapshotTime = now;
362
381
  this._packetCount++;
363
382
  } catch {
@@ -515,6 +534,8 @@ var SnapshotSync = class {
515
534
  constructor(transport, codec, snapshotBuffer, options) {
516
535
  this._hostAuthority = null;
517
536
  this._clientReceiver = null;
537
+ // Snapshot listeners survive host migration (forwarder re-attached on demote)
538
+ this._snapshotListeners = [];
518
539
  this._transport = transport;
519
540
  this._codec = codec;
520
541
  this._snapshotBuffer = snapshotBuffer;
@@ -535,6 +556,7 @@ var SnapshotSync = class {
535
556
  extrapolateMs: options?.extrapolateMs,
536
557
  is2D: options?.is2D
537
558
  });
559
+ this._attachSnapshotForwarder(this._clientReceiver);
538
560
  }
539
561
  }
540
562
  get isHost() {
@@ -547,8 +569,12 @@ var SnapshotSync = class {
547
569
  return this._clientReceiver;
548
570
  }
549
571
  /** Host: called every fixed tick to potentially broadcast state */
550
- hostTick(tick, entities, delta) {
551
- this._hostAuthority?.tick(tick, entities, delta);
572
+ hostTick(tick, entities, delta, hostInput) {
573
+ this._hostAuthority?.tick(tick, entities, delta, hostInput);
574
+ }
575
+ /** Register a listener fired after each merged snapshot (client side). */
576
+ onSnapshot(cb) {
577
+ this._snapshotListeners.push(cb);
552
578
  }
553
579
  /** Client: called every render frame to interpolate */
554
580
  clientInterpolate(renderTime) {
@@ -586,260 +612,528 @@ var SnapshotSync = class {
586
612
  is2D: options?.is2D
587
613
  }
588
614
  );
615
+ this._attachSnapshotForwarder(this._clientReceiver);
589
616
  }
590
617
  destroy() {
591
618
  this._hostAuthority?.destroy();
592
619
  this._clientReceiver?.destroy();
593
620
  this._hostAuthority = null;
594
621
  this._clientReceiver = null;
622
+ this._snapshotListeners = [];
623
+ }
624
+ // ── Private ──
625
+ /** Forward receiver snapshots to registered listeners (survives host migration). */
626
+ _attachSnapshotForwarder(receiver) {
627
+ receiver.onSnapshot((t, e, hi) => {
628
+ for (const l of this._snapshotListeners) l(t, e, hi);
629
+ });
630
+ }
631
+ };
632
+
633
+ // src/core/InputBuffer.ts
634
+ var InputBuffer = class {
635
+ constructor(neutralInput, historySize = 120) {
636
+ /** Local player tick-keyed inputs (ring buffer). */
637
+ this._local = /* @__PURE__ */ new Map();
638
+ /** Last-received input per remote peer. */
639
+ this._remotes = /* @__PURE__ */ new Map();
640
+ /** Per-peer tick-keyed inputs for accurate rollback re-simulation. */
641
+ this._peerTicks = /* @__PURE__ */ new Map();
642
+ this._neutral = { ...neutralInput };
643
+ this._historySize = historySize;
644
+ }
645
+ // ── Local input ──
646
+ /** Record a snapshot of the local input at the given tick. Evicts the entry exactly historySize back. */
647
+ storeTick(tick, input) {
648
+ this._local.set(tick, { ...input });
649
+ this._local.delete(tick - this._historySize);
650
+ }
651
+ /** Return the local input at `tick`, or a neutral copy if out of range. */
652
+ getTick(tick) {
653
+ return this._local.get(tick) ?? { ...this._neutral };
654
+ }
655
+ /** True if we have a stored local input for this tick (used to avoid spurious justPressed after snap/rejoin). */
656
+ hasTick(tick) {
657
+ return this._local.has(tick);
658
+ }
659
+ /** Neutral payload with every boolean field forced false (use for justPressed when prev tick is unknown). */
660
+ getJustPressedZero() {
661
+ const out = {};
662
+ for (const key in this._neutral) {
663
+ const v = this._neutral[key];
664
+ out[key] = typeof v === "boolean" ? false : v;
665
+ }
666
+ return out;
667
+ }
668
+ // ── Remote (peer) inputs ──
669
+ /**
670
+ * Record a remote peer's input. If `tick` is given (the sender's local tick),
671
+ * also store it in the per-peer ring buffer for rollback.
672
+ */
673
+ setRemote(peerId, input, tick) {
674
+ this._remotes.set(peerId, input);
675
+ if (tick !== void 0) {
676
+ let peerMap = this._peerTicks.get(peerId);
677
+ if (!peerMap) {
678
+ peerMap = /* @__PURE__ */ new Map();
679
+ this._peerTicks.set(peerId, peerMap);
680
+ }
681
+ peerMap.set(tick, input);
682
+ peerMap.delete(tick - this._historySize);
683
+ }
684
+ }
685
+ /** Last-known input for a peer, or a neutral copy if never received. */
686
+ getRemote(peerId) {
687
+ return this._remotes.get(peerId) ?? { ...this._neutral };
688
+ }
689
+ /** Snapshot of all remote peers' last-known inputs (shallow copy of the map). */
690
+ allRemotes() {
691
+ return new Map(this._remotes);
692
+ }
693
+ /**
694
+ * Return a peer's exact input at the given tick (for rollback accuracy),
695
+ * falling back to their last-known input when history does not reach that far.
696
+ */
697
+ getRemoteAtTick(peerId, tick) {
698
+ return this._peerTicks.get(peerId)?.get(tick) ?? this.getRemote(peerId);
699
+ }
700
+ /** Override the last-known input for a peer (does NOT touch tick history). */
701
+ overrideRemote(peerId, input) {
702
+ this._remotes.set(peerId, input);
703
+ }
704
+ /** Number of currently tracked remote peers. */
705
+ get peerCount() {
706
+ return this._remotes.size;
707
+ }
708
+ /** Iterate tracked peer IDs. */
709
+ peerIds() {
710
+ return this._remotes.keys();
711
+ }
712
+ /**
713
+ * Keep only these peer IDs; remove any other remotes (e.g. after leave/rejoin).
714
+ * Call when the room's peer list changes so stale peers stop receiving input.
715
+ */
716
+ setPeerIds(peerIds) {
717
+ const set = peerIds instanceof Set ? peerIds : new Set(peerIds);
718
+ for (const id of this._remotes.keys()) {
719
+ if (!set.has(id)) {
720
+ this._remotes.delete(id);
721
+ this._peerTicks.delete(id);
722
+ }
723
+ }
724
+ }
725
+ // ── Lifecycle ──
726
+ /** Clear local history, remote last-known inputs, and per-peer tick history. */
727
+ clear() {
728
+ this._local.clear();
729
+ this._remotes.clear();
730
+ this._peerTicks.clear();
595
731
  }
596
732
  };
597
733
 
734
+ // src/core/InputUtils.ts
735
+ function computeJustPressed(curr, prev) {
736
+ const out = {};
737
+ for (const key in curr) {
738
+ const c = curr[key];
739
+ out[key] = typeof c === "boolean" ? c === true && prev[key] !== true : c;
740
+ }
741
+ return out;
742
+ }
743
+
744
+ // src/sync/Rollback.ts
745
+ var IDENTITY_QUAT = { x: 0, y: 0, z: 0, w: 1 };
746
+ function quatMultiply(a, b) {
747
+ return {
748
+ x: a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
749
+ y: a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
750
+ z: a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w,
751
+ w: a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z
752
+ };
753
+ }
754
+ function quatInvert(q) {
755
+ return { x: -q.x, y: -q.y, z: -q.z, w: q.w };
756
+ }
757
+ function quatNormalize(q) {
758
+ const mag = Math.hypot(q.x, q.y, q.z, q.w);
759
+ if (mag < 1e-12) return { ...IDENTITY_QUAT };
760
+ return { x: q.x / mag, y: q.y / mag, z: q.z / mag, w: q.w / mag };
761
+ }
762
+ function quatAngle(q) {
763
+ return 2 * Math.acos(Math.min(1, Math.abs(q.w)));
764
+ }
765
+ function quatScaleAngle(q, factor) {
766
+ let n = quatNormalize(q);
767
+ if (n.w < 0) n = { x: -n.x, y: -n.y, z: -n.z, w: -n.w };
768
+ const angle = 2 * Math.acos(Math.min(1, n.w));
769
+ if (angle < 1e-6) return { ...IDENTITY_QUAT };
770
+ const s = Math.sin(angle / 2);
771
+ const ax = n.x / s;
772
+ const ay = n.y / s;
773
+ const az = n.z / s;
774
+ const na = angle * factor;
775
+ const ns = Math.sin(na / 2);
776
+ return { x: ax * ns, y: ay * ns, z: az * ns, w: Math.cos(na / 2) };
777
+ }
778
+ function applyRollback(params) {
779
+ const {
780
+ serverTick,
781
+ serverState,
782
+ localTick,
783
+ localPeerId,
784
+ inputs,
785
+ currentErrors,
786
+ driver,
787
+ callback,
788
+ dt,
789
+ driftTargetTicks,
790
+ maxRewindTicks,
791
+ snapThreshold
792
+ } = params;
793
+ const preState = driver.captureState();
794
+ const preMap = /* @__PURE__ */ new Map();
795
+ for (const [id, s] of preState) {
796
+ const e = currentErrors.get(id);
797
+ const ex = e?.x ?? 0;
798
+ const ey = e?.y ?? 0;
799
+ if ("z" in s) {
800
+ const errQ = e ? { x: e.qx, y: e.qy, z: e.qz, w: e.qw } : { ...IDENTITY_QUAT };
801
+ preMap.set(id, {
802
+ x: s.x + ex,
803
+ y: s.y + ey,
804
+ z: s.z + (e?.z ?? 0),
805
+ a: 0,
806
+ q: quatMultiply(errQ, { x: s.qx, y: s.qy, z: s.qz, w: s.qw }),
807
+ is3D: true
808
+ });
809
+ } else {
810
+ preMap.set(id, {
811
+ x: s.x + ex,
812
+ y: s.y + ey,
813
+ z: 0,
814
+ a: s.a + (e?.a ?? 0),
815
+ q: { ...IDENTITY_QUAT },
816
+ is3D: false
817
+ });
818
+ }
819
+ }
820
+ driver.applyState(serverState.values());
821
+ const targetTick = serverTick + driftTargetTicks;
822
+ const tickDiff = localTick - targetTick;
823
+ let newLocalTick = localTick;
824
+ let snapped = false;
825
+ if (Math.abs(tickDiff) > maxRewindTicks) {
826
+ newLocalTick = targetTick;
827
+ snapped = true;
828
+ } else {
829
+ for (let i = serverTick + 1; i <= localTick; i++) {
830
+ if (callback) {
831
+ const tickInputs = /* @__PURE__ */ new Map();
832
+ const justPressed = /* @__PURE__ */ new Map();
833
+ for (const peerId of inputs.peerIds()) {
834
+ if (peerId === localPeerId) continue;
835
+ const curr = inputs.getRemoteAtTick(peerId, i);
836
+ tickInputs.set(peerId, curr);
837
+ justPressed.set(
838
+ peerId,
839
+ computeJustPressed(curr, inputs.getRemoteAtTick(peerId, i - 1))
840
+ );
841
+ }
842
+ const localCurr = inputs.getTick(i);
843
+ tickInputs.set(localPeerId, localCurr);
844
+ justPressed.set(
845
+ localPeerId,
846
+ computeJustPressed(localCurr, inputs.getTick(i - 1))
847
+ );
848
+ callback(tickInputs, justPressed, i, true, dt);
849
+ }
850
+ driver.stepWorld?.();
851
+ }
852
+ }
853
+ const postState = driver.captureState();
854
+ const errors = /* @__PURE__ */ new Map();
855
+ for (const [id, post] of postState) {
856
+ const pre = preMap.get(id);
857
+ if (!pre) continue;
858
+ const errX = pre.x - post.x;
859
+ const errY = pre.y - post.y;
860
+ let errZ = 0;
861
+ let errA = 0;
862
+ let q = { ...IDENTITY_QUAT };
863
+ if ("z" in post) {
864
+ errZ = pre.z - post.z;
865
+ let dq = quatNormalize(
866
+ quatMultiply(
867
+ pre.q,
868
+ quatInvert({ x: post.qx, y: post.qy, z: post.qz, w: post.qw })
869
+ )
870
+ );
871
+ if (dq.w < 0) dq = { x: -dq.x, y: -dq.y, z: -dq.z, w: -dq.w };
872
+ q = dq;
873
+ } else {
874
+ const ad = pre.a - post.a;
875
+ errA = ad - Math.PI * 2 * Math.floor((ad + Math.PI) / (Math.PI * 2));
876
+ }
877
+ if (Math.abs(errX) > snapThreshold || Math.abs(errY) > snapThreshold || Math.abs(errZ) > snapThreshold) {
878
+ errors.set(id, { x: 0, y: 0, z: 0, a: 0, qx: 0, qy: 0, qz: 0, qw: 1 });
879
+ } else {
880
+ errors.set(id, {
881
+ x: errX,
882
+ y: errY,
883
+ z: errZ,
884
+ a: errA,
885
+ qx: q.x,
886
+ qy: q.y,
887
+ qz: q.z,
888
+ qw: q.w
889
+ });
890
+ }
891
+ }
892
+ return { newLocalTick, snapped, errors };
893
+ }
894
+
598
895
  // src/sync/PredictionSync.ts
599
- var import_msgpackr = require("msgpackr");
600
896
  var DEFAULT_OPTIONS = {
601
897
  maxRewindTicks: 15,
602
- errorSmoothingDecay: 0.85,
603
- maxErrorPerFrame: 5,
604
- snapThreshold: 15,
605
- lagCompensation: false
898
+ snapThreshold: 150,
899
+ errorDecay: 0.85,
900
+ maxErrorPerFrame: 0,
901
+ neutralInput: {},
902
+ inputHistorySize: 120,
903
+ driftTargetTicks: 4
606
904
  };
607
905
  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();
906
+ constructor(transport, tickKeeper, snapshots, options) {
907
+ // Local input; persists across ticks until replaced (hold-input semantics)
908
+ this._currentInput = null;
909
+ // Newest pending server snapshot awaiting rollback (only the newest survives)
910
+ this._pending = null;
911
+ // Per-entity accumulated visual error offsets
912
+ this._errors = /* @__PURE__ */ new Map();
619
913
  this._serverTick = 0;
620
- // Physics step callback (provided by developer)
914
+ this._lastAppliedServerTick = 0;
915
+ this._worldDriver = null;
621
916
  this._onPhysicsStep = null;
622
- // Own input for current tick
623
- this._currentInput = null;
624
917
  this._transport = transport;
625
- this._codec = codec;
626
918
  this._tickKeeper = tickKeeper;
627
919
  this._options = { ...DEFAULT_OPTIONS, ...options };
628
- this._isHost = transport.isHost;
920
+ this._inputs = new InputBuffer(
921
+ this._options.neutralInput,
922
+ this._options.inputHistorySize
923
+ );
629
924
  this._inputChannel = transport.createChannel("carver:inputs", {
630
925
  reliable: true,
631
926
  ordered: true
632
927
  });
633
- this._stateChannel = transport.createChannel("carver:pred-state", {
634
- reliable: false,
635
- ordered: false,
636
- maxRetransmits: 0
928
+ this._inputChannel.onReceive(
929
+ (data, peerId) => {
930
+ try {
931
+ const packet = typeof data === "string" ? JSON.parse(data) : data;
932
+ if (typeof packet.t === "number" && packet.i !== null && typeof packet.i === "object") {
933
+ this._inputs.setRemote(peerId, packet.i, packet.t);
934
+ }
935
+ } catch (err) {
936
+ if (typeof console !== "undefined")
937
+ console.debug("[CarverJS] Malformed input packet:", err);
938
+ }
939
+ }
940
+ );
941
+ snapshots.onSnapshot((tick, entities, hostInput) => {
942
+ if (this._transport.isHost) return;
943
+ if (tick <= this._lastAppliedServerTick) return;
944
+ this._serverTick = tick;
945
+ this._tickKeeper.setServerTick(tick);
946
+ if (!this._pending || tick > this._pending.t) {
947
+ this._pending = { t: tick, entities, hostInput };
948
+ }
637
949
  });
638
- this._ackChannel = transport.createChannel("carver:pred-acks", {
639
- reliable: true,
640
- ordered: true
950
+ transport.onPeerLeave(() => {
951
+ this._inputs.setPeerIds(this._transport.peers);
641
952
  });
642
- if (this._isHost) {
643
- this._setupHostListeners();
644
- } else {
645
- this._setupClientListeners();
646
- }
647
953
  }
648
- /** Set the physics step callback (required for rollback re-simulation) */
954
+ // ── Wiring ──
955
+ /** Set the game simulation callback (forward sim + rollback resim). */
649
956
  setPhysicsStep(cb) {
650
957
  this._onPhysicsStep = cb;
651
958
  }
652
- /** Set the current input for this tick (client-side) */
959
+ /** Set the world driver used for forward stepping and rollback. */
960
+ setWorldDriver(driver) {
961
+ this._worldDriver = driver;
962
+ }
963
+ // ── Input ──
964
+ /** Set the local player's input. PERSISTS across ticks until replaced. */
653
965
  setInput(input) {
654
966
  this._currentInput = input;
655
967
  }
968
+ /** Local input stored at the given tick (neutral fallback). Used by the host to embed `hi`. */
969
+ getLocalInput(tick) {
970
+ return this._inputs.getTick(tick);
971
+ }
972
+ // ── Frame lifecycle ──
656
973
  /**
657
- * Called every fixed tick on the client.
658
- * Applies input locally (prediction), buffers it, and sends to host.
974
+ * Apply the newest pending server snapshot (full-world rollback).
975
+ * Call once per render frame BEFORE tickKeeper.update().
976
+ * No-op on host or when nothing is pending.
659
977
  */
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;
978
+ beginFrame() {
979
+ if (this._transport.isHost || !this._pending) return;
980
+ const pending = this._pending;
981
+ this._pending = null;
982
+ this._lastAppliedServerTick = pending.t;
983
+ if (pending.hostInput !== void 0) {
984
+ this._inputs.setRemote(
985
+ this._transport.hostId,
986
+ pending.hostInput,
987
+ pending.t
988
+ );
676
989
  }
677
- const minTick = tick - this._options.maxRewindTicks * 2;
678
- while (this._inputBuffer.length > 0 && this._inputBuffer[0].tick < minTick) {
679
- this._inputBuffer.shift();
990
+ if (!this._worldDriver) return;
991
+ const result = applyRollback({
992
+ serverTick: pending.t,
993
+ serverState: pending.entities,
994
+ localTick: this._tickKeeper.tick,
995
+ localPeerId: this._transport.peerId,
996
+ inputs: this._inputs,
997
+ currentErrors: this._errors,
998
+ driver: this._worldDriver,
999
+ callback: this._onPhysicsStep,
1000
+ dt: this._tickKeeper.tickDelta,
1001
+ driftTargetTicks: this._options.driftTargetTicks,
1002
+ maxRewindTicks: this._options.maxRewindTicks,
1003
+ snapThreshold: this._options.snapThreshold
1004
+ });
1005
+ this._errors = result.errors;
1006
+ if (result.newLocalTick !== this._tickKeeper.tick) {
1007
+ this._tickKeeper.snapTick(result.newLocalTick);
680
1008
  }
681
1009
  }
682
1010
  /**
683
- * Called every fixed tick on the host.
684
- * Processes received inputs and broadcasts authoritative state.
1011
+ * Run one forward fixed tick (host AND client): store + broadcast input,
1012
+ * build per-tick input maps, invoke the callback, then step the world.
685
1013
  */
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);
1014
+ tick(tick) {
1015
+ const localInput = this._currentInput ?? { ...this._options.neutralInput };
1016
+ this._inputs.storeTick(tick, localInput);
1017
+ this._inputChannel.send({
1018
+ t: tick,
1019
+ i: localInput,
1020
+ p: this._transport.peerId
1021
+ });
1022
+ const prevTick = tick - 1;
1023
+ const tickInputs = this._inputs.allRemotes();
1024
+ tickInputs.delete(this._transport.peerId);
1025
+ const justPressed = /* @__PURE__ */ new Map();
1026
+ for (const [peerId, inp] of tickInputs) {
1027
+ justPressed.set(
1028
+ peerId,
1029
+ computeJustPressed(inp, this._inputs.getRemoteAtTick(peerId, prevTick))
1030
+ );
698
1031
  }
1032
+ tickInputs.set(this._transport.peerId, localInput);
1033
+ justPressed.set(
1034
+ this._transport.peerId,
1035
+ this._inputs.hasTick(prevTick) ? computeJustPressed(localInput, this._inputs.getTick(prevTick)) : this._inputs.getJustPressedZero()
1036
+ // suppress spurious edges after snap/rejoin
1037
+ );
1038
+ if (this._onPhysicsStep) {
1039
+ this._onPhysicsStep(
1040
+ tickInputs,
1041
+ justPressed,
1042
+ tick,
1043
+ false,
1044
+ this._tickKeeper.tickDelta
1045
+ );
1046
+ }
1047
+ this._worldDriver?.stepWorld?.();
699
1048
  }
700
1049
  /**
701
- * Called every render frame on the client to apply visual error smoothing.
702
- * Returns the corrected entity states.
1050
+ * Decay stored error offsets and return the portion to ADD to rendered
1051
+ * transforms this frame. Call exactly once per render frame.
703
1052
  */
704
- applyErrorSmoothing(entities) {
1053
+ getRenderErrorOffsets() {
705
1054
  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;
1055
+ const decay = this._options.errorDecay;
1056
+ const maxErr = this._options.maxErrorPerFrame;
1057
+ for (const [id, e] of this._errors) {
1058
+ e.x *= decay;
1059
+ e.y *= decay;
1060
+ e.z *= decay;
1061
+ e.a *= decay;
1062
+ let q = quatScaleAngle({ x: e.qx, y: e.qy, z: e.qz, w: e.qw }, decay);
1063
+ if (Math.abs(e.x) < 0.1) e.x = 0;
1064
+ if (Math.abs(e.y) < 0.1) e.y = 0;
1065
+ if (Math.abs(e.z) < 0.1) e.z = 0;
1066
+ if (Math.abs(e.a) < 1e-3) e.a = 0;
1067
+ if (quatAngle(q) < 1e-3) q = { x: 0, y: 0, z: 0, w: 1 };
1068
+ e.qx = q.x;
1069
+ e.qy = q.y;
1070
+ e.qz = q.z;
1071
+ e.qw = q.w;
1072
+ let ax = e.x;
1073
+ let ay = e.y;
1074
+ let az = e.z;
1075
+ if (maxErr > 0) {
1076
+ const mag = Math.hypot(e.x, e.y, e.z);
1077
+ if (mag > maxErr) {
1078
+ const s = maxErr / mag;
1079
+ ax = e.x * s;
1080
+ ay = e.y * s;
1081
+ az = e.z * s;
1082
+ e.x -= ax;
1083
+ e.y -= ay;
1084
+ e.z -= az;
1085
+ } else {
1086
+ e.x = 0;
1087
+ e.y = 0;
1088
+ e.z = 0;
1089
+ }
712
1090
  }
713
- const corrected = { ...entity };
714
- corrected.x += correction.x;
715
- corrected.y += correction.y;
716
- if ("z" in corrected) {
717
- corrected.z += correction.z;
1091
+ const quatIsIdentity = e.qx === 0 && e.qy === 0 && e.qz === 0 && e.qw === 1;
1092
+ if (ax !== 0 || ay !== 0 || az !== 0 || e.a !== 0 || !quatIsIdentity) {
1093
+ result.set(id, {
1094
+ x: ax,
1095
+ y: ay,
1096
+ z: az,
1097
+ a: e.a,
1098
+ qx: e.qx,
1099
+ qy: e.qy,
1100
+ qz: e.qz,
1101
+ qw: e.qw
1102
+ });
718
1103
  }
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);
1104
+ if (e.x === 0 && e.y === 0 && e.z === 0 && e.a === 0 && quatIsIdentity) {
1105
+ this._errors.delete(id);
725
1106
  }
726
- result.set(id, corrected);
727
1107
  }
728
1108
  return result;
729
1109
  }
730
- get predictedState() {
731
- return this._predictedState;
732
- }
1110
+ // ── State ──
1111
+ /** Tick of the newest RECEIVED snapshot. */
733
1112
  get serverTick() {
734
1113
  return this._serverTick;
735
1114
  }
1115
+ /** Tick of the newest APPLIED (rolled-back) snapshot, 0 initially. */
1116
+ get lastAppliedServerTick() {
1117
+ return this._lastAppliedServerTick;
1118
+ }
736
1119
  destroy() {
737
1120
  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);
1121
+ this._inputs.clear();
1122
+ this._errors.clear();
1123
+ this._pending = null;
1124
+ this._currentInput = null;
837
1125
  }
838
1126
  };
839
1127
  // Annotate the CommonJS export names for ESM import in node:
840
1128
  0 && (module.exports = {
841
1129
  EventSync,
842
1130
  PredictionSync,
843
- SnapshotSync
1131
+ SnapshotSync,
1132
+ applyRollback,
1133
+ quatAngle,
1134
+ quatInvert,
1135
+ quatMultiply,
1136
+ quatNormalize,
1137
+ quatScaleAngle
844
1138
  });
845
1139
  //# sourceMappingURL=sync.js.map