@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
@@ -137,8 +137,9 @@ var HostAuthority = class {
137
137
  /**
138
138
  * Called every fixed tick by the sync engine.
139
139
  * Collects entity states and decides whether to broadcast.
140
+ * `hostInput` (prediction mode) is embedded in the snapshot packet as `hi`.
140
141
  */
141
- tick(currentTick, entities, delta) {
142
+ tick(currentTick, entities, delta, hostInput) {
142
143
  this._tick = currentTick;
143
144
  this._snapshotBuffer.store(currentTick, new Map(entities));
144
145
  this._broadcastAccumulator += delta;
@@ -146,16 +147,16 @@ var HostAuthority = class {
146
147
  if (this._broadcastAccumulator < broadcastInterval) return;
147
148
  this._broadcastAccumulator -= broadcastInterval;
148
149
  for (const peerId of this._transport.peers) {
149
- this._broadcastToClient(peerId, currentTick, entities);
150
+ this._broadcastToClient(peerId, currentTick, entities, hostInput);
150
151
  }
151
152
  }
152
153
  /** Force a keyframe broadcast to all clients (e.g., after host migration) */
153
- forceKeyframe(currentTick, entities) {
154
+ forceKeyframe(currentTick, entities, hostInput) {
154
155
  this._clientBaselines.clear();
155
156
  this._clientLastKeyframeTick.clear();
156
157
  this._snapshotBuffer.store(currentTick, new Map(entities));
157
158
  for (const peerId of this._transport.peers) {
158
- this._broadcastToClient(peerId, currentTick, entities);
159
+ this._broadcastToClient(peerId, currentTick, entities, hostInput);
159
160
  }
160
161
  }
161
162
  destroy() {
@@ -164,7 +165,7 @@ var HostAuthority = class {
164
165
  this._clientBaselines.clear();
165
166
  this._clientLastKeyframeTick.clear();
166
167
  }
167
- _broadcastToClient(peerId, currentTick, entities) {
168
+ _broadcastToClient(peerId, currentTick, entities, hostInput) {
168
169
  let clientEntities = entities;
169
170
  if (this._interestFilter) {
170
171
  clientEntities = /* @__PURE__ */ new Map();
@@ -188,7 +189,8 @@ var HostAuthority = class {
188
189
  currentTick,
189
190
  needsKeyframe ? -1 : clientBaseTick ?? -1,
190
191
  clientEntities,
191
- baseline
192
+ baseline,
193
+ hostInput
192
194
  );
193
195
  if (packet) {
194
196
  this._snapshotChannel.send(packet, peerId);
@@ -210,6 +212,8 @@ var ClientReceiver = class {
210
212
  this._packetCount = 0;
211
213
  // Entity state: full accumulated state from keyframes + deltas
212
214
  this._fullState = /* @__PURE__ */ new Map();
215
+ // Listeners fired after each snapshot is merged into the full world state
216
+ this._snapshotListeners = [];
213
217
  this._transport = transport;
214
218
  this._codec = codec;
215
219
  this._bufferSize = options?.bufferSize ?? 3;
@@ -296,16 +300,21 @@ var ClientReceiver = class {
296
300
  requestKeyframe() {
297
301
  this._ackChannel.send("-1");
298
302
  }
303
+ /** Register a listener fired after each snapshot is merged into the full world state */
304
+ onSnapshot(cb) {
305
+ this._snapshotListeners.push(cb);
306
+ }
299
307
  destroy() {
300
308
  this._snapshotChannel.close();
301
309
  this._ackChannel.close();
302
310
  this._buffer = [];
303
311
  this._interpolatedState.clear();
304
312
  this._fullState.clear();
313
+ this._snapshotListeners = [];
305
314
  }
306
315
  _handleSnapshot(data) {
307
316
  try {
308
- const { tick, baseTick, entities } = this._codec.deserializePacket(data);
317
+ const { tick, baseTick, entities, hostInput } = this._codec.deserializePacket(data);
309
318
  const now = performance.now();
310
319
  if (baseTick === -1) {
311
320
  this._fullState.clear();
@@ -321,15 +330,19 @@ var ClientReceiver = class {
321
330
  }
322
331
  }
323
332
  }
333
+ const fullStateClone = new Map(this._fullState);
324
334
  this._buffer.push({
325
335
  tick,
326
- entities: new Map(this._fullState),
336
+ entities: fullStateClone,
327
337
  receivedAt: now
328
338
  });
329
339
  while (this._buffer.length > this._bufferSize * 2) {
330
340
  this._buffer.shift();
331
341
  }
332
342
  this._ackChannel.send(String(tick));
343
+ for (const cb of this._snapshotListeners) {
344
+ cb(tick, fullStateClone, hostInput);
345
+ }
333
346
  this._lastSnapshotTime = now;
334
347
  this._packetCount++;
335
348
  } catch {
@@ -487,6 +500,8 @@ var SnapshotSync = class {
487
500
  constructor(transport, codec, snapshotBuffer, options) {
488
501
  this._hostAuthority = null;
489
502
  this._clientReceiver = null;
503
+ // Snapshot listeners survive host migration (forwarder re-attached on demote)
504
+ this._snapshotListeners = [];
490
505
  this._transport = transport;
491
506
  this._codec = codec;
492
507
  this._snapshotBuffer = snapshotBuffer;
@@ -507,6 +522,7 @@ var SnapshotSync = class {
507
522
  extrapolateMs: options?.extrapolateMs,
508
523
  is2D: options?.is2D
509
524
  });
525
+ this._attachSnapshotForwarder(this._clientReceiver);
510
526
  }
511
527
  }
512
528
  get isHost() {
@@ -519,8 +535,12 @@ var SnapshotSync = class {
519
535
  return this._clientReceiver;
520
536
  }
521
537
  /** Host: called every fixed tick to potentially broadcast state */
522
- hostTick(tick, entities, delta) {
523
- this._hostAuthority?.tick(tick, entities, delta);
538
+ hostTick(tick, entities, delta, hostInput) {
539
+ this._hostAuthority?.tick(tick, entities, delta, hostInput);
540
+ }
541
+ /** Register a listener fired after each merged snapshot (client side). */
542
+ onSnapshot(cb) {
543
+ this._snapshotListeners.push(cb);
524
544
  }
525
545
  /** Client: called every render frame to interpolate */
526
546
  clientInterpolate(renderTime) {
@@ -558,260 +578,530 @@ var SnapshotSync = class {
558
578
  is2D: options?.is2D
559
579
  }
560
580
  );
581
+ this._attachSnapshotForwarder(this._clientReceiver);
561
582
  }
562
583
  destroy() {
563
584
  this._hostAuthority?.destroy();
564
585
  this._clientReceiver?.destroy();
565
586
  this._hostAuthority = null;
566
587
  this._clientReceiver = null;
588
+ this._snapshotListeners = [];
589
+ }
590
+ // ── Private ──
591
+ /** Forward receiver snapshots to registered listeners (survives host migration). */
592
+ _attachSnapshotForwarder(receiver) {
593
+ receiver.onSnapshot((t, e, hi) => {
594
+ for (const l of this._snapshotListeners) l(t, e, hi);
595
+ });
596
+ }
597
+ };
598
+
599
+ // src/core/InputBuffer.ts
600
+ var InputBuffer = class {
601
+ constructor(neutralInput, historySize = 120) {
602
+ /** Local player tick-keyed inputs (ring buffer). */
603
+ this._local = /* @__PURE__ */ new Map();
604
+ /** Last-received input per remote peer. */
605
+ this._remotes = /* @__PURE__ */ new Map();
606
+ /** Per-peer tick-keyed inputs for accurate rollback re-simulation. */
607
+ this._peerTicks = /* @__PURE__ */ new Map();
608
+ this._neutral = { ...neutralInput };
609
+ this._historySize = historySize;
610
+ }
611
+ // ── Local input ──
612
+ /** Record a snapshot of the local input at the given tick. Evicts the entry exactly historySize back. */
613
+ storeTick(tick, input) {
614
+ this._local.set(tick, { ...input });
615
+ this._local.delete(tick - this._historySize);
616
+ }
617
+ /** Return the local input at `tick`, or a neutral copy if out of range. */
618
+ getTick(tick) {
619
+ return this._local.get(tick) ?? { ...this._neutral };
620
+ }
621
+ /** True if we have a stored local input for this tick (used to avoid spurious justPressed after snap/rejoin). */
622
+ hasTick(tick) {
623
+ return this._local.has(tick);
624
+ }
625
+ /** Neutral payload with every boolean field forced false (use for justPressed when prev tick is unknown). */
626
+ getJustPressedZero() {
627
+ const out = {};
628
+ for (const key in this._neutral) {
629
+ const v = this._neutral[key];
630
+ out[key] = typeof v === "boolean" ? false : v;
631
+ }
632
+ return out;
633
+ }
634
+ // ── Remote (peer) inputs ──
635
+ /**
636
+ * Record a remote peer's input. If `tick` is given (the sender's local tick),
637
+ * also store it in the per-peer ring buffer for rollback.
638
+ */
639
+ setRemote(peerId, input, tick) {
640
+ this._remotes.set(peerId, input);
641
+ if (tick !== void 0) {
642
+ let peerMap = this._peerTicks.get(peerId);
643
+ if (!peerMap) {
644
+ peerMap = /* @__PURE__ */ new Map();
645
+ this._peerTicks.set(peerId, peerMap);
646
+ }
647
+ peerMap.set(tick, input);
648
+ peerMap.delete(tick - this._historySize);
649
+ }
650
+ }
651
+ /** Last-known input for a peer, or a neutral copy if never received. */
652
+ getRemote(peerId) {
653
+ return this._remotes.get(peerId) ?? { ...this._neutral };
654
+ }
655
+ /** Snapshot of all remote peers' last-known inputs (shallow copy of the map). */
656
+ allRemotes() {
657
+ return new Map(this._remotes);
658
+ }
659
+ /**
660
+ * Return a peer's exact input at the given tick (for rollback accuracy),
661
+ * falling back to their last-known input when history does not reach that far.
662
+ */
663
+ getRemoteAtTick(peerId, tick) {
664
+ return this._peerTicks.get(peerId)?.get(tick) ?? this.getRemote(peerId);
665
+ }
666
+ /** Override the last-known input for a peer (does NOT touch tick history). */
667
+ overrideRemote(peerId, input) {
668
+ this._remotes.set(peerId, input);
669
+ }
670
+ /** Number of currently tracked remote peers. */
671
+ get peerCount() {
672
+ return this._remotes.size;
673
+ }
674
+ /** Iterate tracked peer IDs. */
675
+ peerIds() {
676
+ return this._remotes.keys();
677
+ }
678
+ /**
679
+ * Keep only these peer IDs; remove any other remotes (e.g. after leave/rejoin).
680
+ * Call when the room's peer list changes so stale peers stop receiving input.
681
+ */
682
+ setPeerIds(peerIds) {
683
+ const set = peerIds instanceof Set ? peerIds : new Set(peerIds);
684
+ for (const id of this._remotes.keys()) {
685
+ if (!set.has(id)) {
686
+ this._remotes.delete(id);
687
+ this._peerTicks.delete(id);
688
+ }
689
+ }
690
+ }
691
+ // ── Lifecycle ──
692
+ /** Clear local history, remote last-known inputs, and per-peer tick history. */
693
+ clear() {
694
+ this._local.clear();
695
+ this._remotes.clear();
696
+ this._peerTicks.clear();
567
697
  }
568
698
  };
569
699
 
700
+ // src/core/InputUtils.ts
701
+ function computeJustPressed(curr, prev) {
702
+ const out = {};
703
+ for (const key in curr) {
704
+ const c = curr[key];
705
+ out[key] = typeof c === "boolean" ? c === true && prev[key] !== true : c;
706
+ }
707
+ return out;
708
+ }
709
+
710
+ // src/sync/Rollback.ts
711
+ var IDENTITY_QUAT = { x: 0, y: 0, z: 0, w: 1 };
712
+ function quatMultiply(a, b) {
713
+ return {
714
+ x: a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
715
+ y: a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
716
+ z: a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w,
717
+ w: a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z
718
+ };
719
+ }
720
+ function quatInvert(q) {
721
+ return { x: -q.x, y: -q.y, z: -q.z, w: q.w };
722
+ }
723
+ function quatNormalize(q) {
724
+ const mag = Math.hypot(q.x, q.y, q.z, q.w);
725
+ if (mag < 1e-12) return { ...IDENTITY_QUAT };
726
+ return { x: q.x / mag, y: q.y / mag, z: q.z / mag, w: q.w / mag };
727
+ }
728
+ function quatAngle(q) {
729
+ return 2 * Math.acos(Math.min(1, Math.abs(q.w)));
730
+ }
731
+ function quatScaleAngle(q, factor) {
732
+ let n = quatNormalize(q);
733
+ if (n.w < 0) n = { x: -n.x, y: -n.y, z: -n.z, w: -n.w };
734
+ const angle = 2 * Math.acos(Math.min(1, n.w));
735
+ if (angle < 1e-6) return { ...IDENTITY_QUAT };
736
+ const s = Math.sin(angle / 2);
737
+ const ax = n.x / s;
738
+ const ay = n.y / s;
739
+ const az = n.z / s;
740
+ const na = angle * factor;
741
+ const ns = Math.sin(na / 2);
742
+ return { x: ax * ns, y: ay * ns, z: az * ns, w: Math.cos(na / 2) };
743
+ }
744
+ function applyRollback(params) {
745
+ const {
746
+ serverTick,
747
+ serverState,
748
+ localTick,
749
+ localPeerId,
750
+ inputs,
751
+ currentErrors,
752
+ driver,
753
+ callback,
754
+ dt,
755
+ driftTargetTicks,
756
+ maxRewindTicks,
757
+ snapThreshold
758
+ } = params;
759
+ const preState = driver.captureState();
760
+ const preMap = /* @__PURE__ */ new Map();
761
+ for (const [id, s] of preState) {
762
+ const e = currentErrors.get(id);
763
+ const ex = e?.x ?? 0;
764
+ const ey = e?.y ?? 0;
765
+ if ("z" in s) {
766
+ const errQ = e ? { x: e.qx, y: e.qy, z: e.qz, w: e.qw } : { ...IDENTITY_QUAT };
767
+ preMap.set(id, {
768
+ x: s.x + ex,
769
+ y: s.y + ey,
770
+ z: s.z + (e?.z ?? 0),
771
+ a: 0,
772
+ q: quatMultiply(errQ, { x: s.qx, y: s.qy, z: s.qz, w: s.qw }),
773
+ is3D: true
774
+ });
775
+ } else {
776
+ preMap.set(id, {
777
+ x: s.x + ex,
778
+ y: s.y + ey,
779
+ z: 0,
780
+ a: s.a + (e?.a ?? 0),
781
+ q: { ...IDENTITY_QUAT },
782
+ is3D: false
783
+ });
784
+ }
785
+ }
786
+ driver.applyState(serverState.values());
787
+ const targetTick = serverTick + driftTargetTicks;
788
+ const tickDiff = localTick - targetTick;
789
+ let newLocalTick = localTick;
790
+ let snapped = false;
791
+ if (Math.abs(tickDiff) > maxRewindTicks) {
792
+ newLocalTick = targetTick;
793
+ snapped = true;
794
+ } else {
795
+ for (let i = serverTick + 1; i <= localTick; i++) {
796
+ if (callback) {
797
+ const tickInputs = /* @__PURE__ */ new Map();
798
+ const justPressed = /* @__PURE__ */ new Map();
799
+ for (const peerId of inputs.peerIds()) {
800
+ if (peerId === localPeerId) continue;
801
+ const curr = inputs.getRemoteAtTick(peerId, i);
802
+ tickInputs.set(peerId, curr);
803
+ justPressed.set(
804
+ peerId,
805
+ computeJustPressed(curr, inputs.getRemoteAtTick(peerId, i - 1))
806
+ );
807
+ }
808
+ const localCurr = inputs.getTick(i);
809
+ tickInputs.set(localPeerId, localCurr);
810
+ justPressed.set(
811
+ localPeerId,
812
+ computeJustPressed(localCurr, inputs.getTick(i - 1))
813
+ );
814
+ callback(tickInputs, justPressed, i, true, dt);
815
+ }
816
+ driver.stepWorld?.();
817
+ }
818
+ }
819
+ const postState = driver.captureState();
820
+ const errors = /* @__PURE__ */ new Map();
821
+ for (const [id, post] of postState) {
822
+ const pre = preMap.get(id);
823
+ if (!pre) continue;
824
+ const errX = pre.x - post.x;
825
+ const errY = pre.y - post.y;
826
+ let errZ = 0;
827
+ let errA = 0;
828
+ let q = { ...IDENTITY_QUAT };
829
+ if ("z" in post) {
830
+ errZ = pre.z - post.z;
831
+ let dq = quatNormalize(
832
+ quatMultiply(
833
+ pre.q,
834
+ quatInvert({ x: post.qx, y: post.qy, z: post.qz, w: post.qw })
835
+ )
836
+ );
837
+ if (dq.w < 0) dq = { x: -dq.x, y: -dq.y, z: -dq.z, w: -dq.w };
838
+ q = dq;
839
+ } else {
840
+ const ad = pre.a - post.a;
841
+ errA = ad - Math.PI * 2 * Math.floor((ad + Math.PI) / (Math.PI * 2));
842
+ }
843
+ if (Math.abs(errX) > snapThreshold || Math.abs(errY) > snapThreshold || Math.abs(errZ) > snapThreshold) {
844
+ errors.set(id, { x: 0, y: 0, z: 0, a: 0, qx: 0, qy: 0, qz: 0, qw: 1 });
845
+ } else {
846
+ errors.set(id, {
847
+ x: errX,
848
+ y: errY,
849
+ z: errZ,
850
+ a: errA,
851
+ qx: q.x,
852
+ qy: q.y,
853
+ qz: q.z,
854
+ qw: q.w
855
+ });
856
+ }
857
+ }
858
+ return { newLocalTick, snapped, errors };
859
+ }
860
+
570
861
  // src/sync/PredictionSync.ts
571
- import { pack, unpack } from "msgpackr";
572
862
  var DEFAULT_OPTIONS = {
573
863
  maxRewindTicks: 15,
574
- errorSmoothingDecay: 0.85,
575
- maxErrorPerFrame: 5,
576
- snapThreshold: 15,
577
- lagCompensation: false
864
+ snapThreshold: 150,
865
+ errorDecay: 0.85,
866
+ maxErrorPerFrame: 0,
867
+ neutralInput: {},
868
+ inputHistorySize: 120,
869
+ driftTargetTicks: 4
578
870
  };
579
871
  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();
872
+ constructor(transport, tickKeeper, snapshots, options) {
873
+ // Local input; persists across ticks until replaced (hold-input semantics)
874
+ this._currentInput = null;
875
+ // Newest pending server snapshot awaiting rollback (only the newest survives)
876
+ this._pending = null;
877
+ // Per-entity accumulated visual error offsets
878
+ this._errors = /* @__PURE__ */ new Map();
591
879
  this._serverTick = 0;
592
- // Physics step callback (provided by developer)
880
+ this._lastAppliedServerTick = 0;
881
+ this._worldDriver = null;
593
882
  this._onPhysicsStep = null;
594
- // Own input for current tick
595
- this._currentInput = null;
596
883
  this._transport = transport;
597
- this._codec = codec;
598
884
  this._tickKeeper = tickKeeper;
599
885
  this._options = { ...DEFAULT_OPTIONS, ...options };
600
- this._isHost = transport.isHost;
886
+ this._inputs = new InputBuffer(
887
+ this._options.neutralInput,
888
+ this._options.inputHistorySize
889
+ );
601
890
  this._inputChannel = transport.createChannel("carver:inputs", {
602
891
  reliable: true,
603
892
  ordered: true
604
893
  });
605
- this._stateChannel = transport.createChannel("carver:pred-state", {
606
- reliable: false,
607
- ordered: false,
608
- maxRetransmits: 0
894
+ this._inputChannel.onReceive(
895
+ (data, peerId) => {
896
+ try {
897
+ const packet = typeof data === "string" ? JSON.parse(data) : data;
898
+ if (typeof packet.t === "number" && packet.i !== null && typeof packet.i === "object") {
899
+ this._inputs.setRemote(peerId, packet.i, packet.t);
900
+ }
901
+ } catch (err) {
902
+ if (typeof console !== "undefined")
903
+ console.debug("[CarverJS] Malformed input packet:", err);
904
+ }
905
+ }
906
+ );
907
+ snapshots.onSnapshot((tick, entities, hostInput) => {
908
+ if (this._transport.isHost) return;
909
+ if (tick <= this._lastAppliedServerTick) return;
910
+ this._serverTick = tick;
911
+ this._tickKeeper.setServerTick(tick);
912
+ if (!this._pending || tick > this._pending.t) {
913
+ this._pending = { t: tick, entities, hostInput };
914
+ }
609
915
  });
610
- this._ackChannel = transport.createChannel("carver:pred-acks", {
611
- reliable: true,
612
- ordered: true
916
+ transport.onPeerLeave(() => {
917
+ this._inputs.setPeerIds(this._transport.peers);
613
918
  });
614
- if (this._isHost) {
615
- this._setupHostListeners();
616
- } else {
617
- this._setupClientListeners();
618
- }
619
919
  }
620
- /** Set the physics step callback (required for rollback re-simulation) */
920
+ // ── Wiring ──
921
+ /** Set the game simulation callback (forward sim + rollback resim). */
621
922
  setPhysicsStep(cb) {
622
923
  this._onPhysicsStep = cb;
623
924
  }
624
- /** Set the current input for this tick (client-side) */
925
+ /** Set the world driver used for forward stepping and rollback. */
926
+ setWorldDriver(driver) {
927
+ this._worldDriver = driver;
928
+ }
929
+ // ── Input ──
930
+ /** Set the local player's input. PERSISTS across ticks until replaced. */
625
931
  setInput(input) {
626
932
  this._currentInput = input;
627
933
  }
934
+ /** Local input stored at the given tick (neutral fallback). Used by the host to embed `hi`. */
935
+ getLocalInput(tick) {
936
+ return this._inputs.getTick(tick);
937
+ }
938
+ // ── Frame lifecycle ──
628
939
  /**
629
- * Called every fixed tick on the client.
630
- * Applies input locally (prediction), buffers it, and sends to host.
940
+ * Apply the newest pending server snapshot (full-world rollback).
941
+ * Call once per render frame BEFORE tickKeeper.update().
942
+ * No-op on host or when nothing is pending.
631
943
  */
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;
944
+ beginFrame() {
945
+ if (this._transport.isHost || !this._pending) return;
946
+ const pending = this._pending;
947
+ this._pending = null;
948
+ this._lastAppliedServerTick = pending.t;
949
+ if (pending.hostInput !== void 0) {
950
+ this._inputs.setRemote(
951
+ this._transport.hostId,
952
+ pending.hostInput,
953
+ pending.t
954
+ );
648
955
  }
649
- const minTick = tick - this._options.maxRewindTicks * 2;
650
- while (this._inputBuffer.length > 0 && this._inputBuffer[0].tick < minTick) {
651
- this._inputBuffer.shift();
956
+ if (!this._worldDriver) return;
957
+ const result = applyRollback({
958
+ serverTick: pending.t,
959
+ serverState: pending.entities,
960
+ localTick: this._tickKeeper.tick,
961
+ localPeerId: this._transport.peerId,
962
+ inputs: this._inputs,
963
+ currentErrors: this._errors,
964
+ driver: this._worldDriver,
965
+ callback: this._onPhysicsStep,
966
+ dt: this._tickKeeper.tickDelta,
967
+ driftTargetTicks: this._options.driftTargetTicks,
968
+ maxRewindTicks: this._options.maxRewindTicks,
969
+ snapThreshold: this._options.snapThreshold
970
+ });
971
+ this._errors = result.errors;
972
+ if (result.newLocalTick !== this._tickKeeper.tick) {
973
+ this._tickKeeper.snapTick(result.newLocalTick);
652
974
  }
653
975
  }
654
976
  /**
655
- * Called every fixed tick on the host.
656
- * Processes received inputs and broadcasts authoritative state.
977
+ * Run one forward fixed tick (host AND client): store + broadcast input,
978
+ * build per-tick input maps, invoke the callback, then step the world.
657
979
  */
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);
980
+ tick(tick) {
981
+ const localInput = this._currentInput ?? { ...this._options.neutralInput };
982
+ this._inputs.storeTick(tick, localInput);
983
+ this._inputChannel.send({
984
+ t: tick,
985
+ i: localInput,
986
+ p: this._transport.peerId
987
+ });
988
+ const prevTick = tick - 1;
989
+ const tickInputs = this._inputs.allRemotes();
990
+ tickInputs.delete(this._transport.peerId);
991
+ const justPressed = /* @__PURE__ */ new Map();
992
+ for (const [peerId, inp] of tickInputs) {
993
+ justPressed.set(
994
+ peerId,
995
+ computeJustPressed(inp, this._inputs.getRemoteAtTick(peerId, prevTick))
996
+ );
670
997
  }
998
+ tickInputs.set(this._transport.peerId, localInput);
999
+ justPressed.set(
1000
+ this._transport.peerId,
1001
+ this._inputs.hasTick(prevTick) ? computeJustPressed(localInput, this._inputs.getTick(prevTick)) : this._inputs.getJustPressedZero()
1002
+ // suppress spurious edges after snap/rejoin
1003
+ );
1004
+ if (this._onPhysicsStep) {
1005
+ this._onPhysicsStep(
1006
+ tickInputs,
1007
+ justPressed,
1008
+ tick,
1009
+ false,
1010
+ this._tickKeeper.tickDelta
1011
+ );
1012
+ }
1013
+ this._worldDriver?.stepWorld?.();
671
1014
  }
672
1015
  /**
673
- * Called every render frame on the client to apply visual error smoothing.
674
- * Returns the corrected entity states.
1016
+ * Decay stored error offsets and return the portion to ADD to rendered
1017
+ * transforms this frame. Call exactly once per render frame.
675
1018
  */
676
- applyErrorSmoothing(entities) {
1019
+ getRenderErrorOffsets() {
677
1020
  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;
1021
+ const decay = this._options.errorDecay;
1022
+ const maxErr = this._options.maxErrorPerFrame;
1023
+ for (const [id, e] of this._errors) {
1024
+ e.x *= decay;
1025
+ e.y *= decay;
1026
+ e.z *= decay;
1027
+ e.a *= decay;
1028
+ let q = quatScaleAngle({ x: e.qx, y: e.qy, z: e.qz, w: e.qw }, decay);
1029
+ if (Math.abs(e.x) < 0.1) e.x = 0;
1030
+ if (Math.abs(e.y) < 0.1) e.y = 0;
1031
+ if (Math.abs(e.z) < 0.1) e.z = 0;
1032
+ if (Math.abs(e.a) < 1e-3) e.a = 0;
1033
+ if (quatAngle(q) < 1e-3) q = { x: 0, y: 0, z: 0, w: 1 };
1034
+ e.qx = q.x;
1035
+ e.qy = q.y;
1036
+ e.qz = q.z;
1037
+ e.qw = q.w;
1038
+ let ax = e.x;
1039
+ let ay = e.y;
1040
+ let az = e.z;
1041
+ if (maxErr > 0) {
1042
+ const mag = Math.hypot(e.x, e.y, e.z);
1043
+ if (mag > maxErr) {
1044
+ const s = maxErr / mag;
1045
+ ax = e.x * s;
1046
+ ay = e.y * s;
1047
+ az = e.z * s;
1048
+ e.x -= ax;
1049
+ e.y -= ay;
1050
+ e.z -= az;
1051
+ } else {
1052
+ e.x = 0;
1053
+ e.y = 0;
1054
+ e.z = 0;
1055
+ }
684
1056
  }
685
- const corrected = { ...entity };
686
- corrected.x += correction.x;
687
- corrected.y += correction.y;
688
- if ("z" in corrected) {
689
- corrected.z += correction.z;
1057
+ const quatIsIdentity = e.qx === 0 && e.qy === 0 && e.qz === 0 && e.qw === 1;
1058
+ if (ax !== 0 || ay !== 0 || az !== 0 || e.a !== 0 || !quatIsIdentity) {
1059
+ result.set(id, {
1060
+ x: ax,
1061
+ y: ay,
1062
+ z: az,
1063
+ a: e.a,
1064
+ qx: e.qx,
1065
+ qy: e.qy,
1066
+ qz: e.qz,
1067
+ qw: e.qw
1068
+ });
690
1069
  }
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);
1070
+ if (e.x === 0 && e.y === 0 && e.z === 0 && e.a === 0 && quatIsIdentity) {
1071
+ this._errors.delete(id);
697
1072
  }
698
- result.set(id, corrected);
699
1073
  }
700
1074
  return result;
701
1075
  }
702
- get predictedState() {
703
- return this._predictedState;
704
- }
1076
+ // ── State ──
1077
+ /** Tick of the newest RECEIVED snapshot. */
705
1078
  get serverTick() {
706
1079
  return this._serverTick;
707
1080
  }
1081
+ /** Tick of the newest APPLIED (rolled-back) snapshot, 0 initially. */
1082
+ get lastAppliedServerTick() {
1083
+ return this._lastAppliedServerTick;
1084
+ }
708
1085
  destroy() {
709
1086
  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);
1087
+ this._inputs.clear();
1088
+ this._errors.clear();
1089
+ this._pending = null;
1090
+ this._currentInput = null;
809
1091
  }
810
1092
  };
811
1093
 
812
1094
  export {
813
1095
  EventSync,
814
1096
  SnapshotSync,
1097
+ InputBuffer,
1098
+ computeJustPressed,
1099
+ quatMultiply,
1100
+ quatInvert,
1101
+ quatNormalize,
1102
+ quatAngle,
1103
+ quatScaleAngle,
1104
+ applyRollback,
815
1105
  PredictionSync
816
1106
  };
817
- //# sourceMappingURL=chunk-EO3YNPRQ.mjs.map
1107
+ //# sourceMappingURL=chunk-Q25TJEY4.mjs.map