@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.
- package/README.md +154 -0
- package/dist/InputBuffer-J6XT_Tt0.d.mts +61 -0
- package/dist/InputBuffer-V7XfHbc6.d.ts +61 -0
- package/dist/{NetworkManager-nvVAOr1O.d.ts → NetworkManager-D-DxFgdM.d.mts} +66 -14
- package/dist/{NetworkManager-DrKM2tEx.d.mts → NetworkManager-DH9uGVMg.d.ts} +66 -14
- package/dist/{chunk-UD6FDZMX.mjs → chunk-GOTAQDBJ.mjs} +47 -4
- package/dist/chunk-GOTAQDBJ.mjs.map +1 -0
- package/dist/{chunk-3KT73N2S.mjs → chunk-LPNEP2VH.mjs} +0 -0
- package/dist/chunk-LPNEP2VH.mjs.map +1 -0
- package/dist/{chunk-EO3YNPRQ.mjs → chunk-Q25TJEY4.mjs} +494 -204
- package/dist/chunk-Q25TJEY4.mjs.map +1 -0
- package/dist/{firebase-CPu87KA0.d.ts → firebase-B5MgLlHk.d.ts} +6 -1
- package/dist/{firebase-PE6MxGdJ.d.mts → firebase-GrbVrNgs.d.mts} +6 -1
- package/dist/index.d.mts +27 -6
- package/dist/index.d.ts +27 -6
- package/dist/index.js +821 -258
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +172 -37
- package/dist/index.mjs.map +1 -1
- package/dist/strategy.d.mts +2 -2
- package/dist/strategy.d.ts +2 -2
- package/dist/strategy.js +46 -3
- package/dist/strategy.js.map +1 -1
- package/dist/strategy.mjs +1 -1
- package/dist/sync.d.mts +134 -50
- package/dist/sync.d.ts +134 -50
- package/dist/sync.js +499 -205
- package/dist/sync.js.map +1 -1
- package/dist/sync.mjs +15 -3
- package/dist/transport.d.mts +0 -0
- package/dist/transport.d.ts +0 -0
- package/dist/transport.js +0 -0
- package/dist/transport.js.map +1 -1
- package/dist/transport.mjs +2 -2
- package/dist/{types-5LHBOW08.d.mts → types-hNfCIBzj.d.mts} +7 -0
- package/dist/{types-5LHBOW08.d.ts → types-hNfCIBzj.d.ts} +7 -0
- package/dist/types.d.mts +2 -2
- package/dist/types.d.ts +2 -2
- package/dist/types.js.map +1 -1
- package/package.json +26 -5
- package/dist/chunk-3KT73N2S.mjs.map +0 -1
- package/dist/chunk-EO3YNPRQ.mjs.map +0 -1
- 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:
|
|
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
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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,
|
|
581
|
-
//
|
|
582
|
-
this.
|
|
583
|
-
//
|
|
584
|
-
this.
|
|
585
|
-
//
|
|
586
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
611
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
630
|
-
*
|
|
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
|
-
|
|
633
|
-
if (this.
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
*
|
|
656
|
-
*
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
*
|
|
674
|
-
*
|
|
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
|
-
|
|
1019
|
+
getRenderErrorOffsets() {
|
|
677
1020
|
const result = /* @__PURE__ */ new Map();
|
|
678
|
-
const decay = this._options.
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
|
|
692
|
-
|
|
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
|
-
|
|
703
|
-
|
|
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.
|
|
711
|
-
this.
|
|
712
|
-
this.
|
|
713
|
-
this.
|
|
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-
|
|
1107
|
+
//# sourceMappingURL=chunk-Q25TJEY4.mjs.map
|