murow 0.1.1 → 0.1.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 +1 -1
- package/dist/cjs/core/clock/clock.js +1 -0
- package/dist/cjs/core/clock/index.js +1 -0
- package/dist/cjs/core/hitbox/hitbox-library.js +1 -0
- package/dist/cjs/core/hitbox/hitbox.js +1 -0
- package/dist/cjs/core/hitbox/index.js +1 -0
- package/dist/cjs/core/hitbox/test.js +1 -0
- package/dist/cjs/core/index.js +1 -1
- package/dist/cjs/core/prediction/prediction.js +1 -1
- package/dist/cjs/core/ray/ray-3d.js +1 -1
- package/dist/cjs/core/raycast/hit-buffer.js +1 -0
- package/dist/cjs/core/raycast/index.js +1 -0
- package/dist/cjs/core/raycast/raycaster.js +1 -0
- package/dist/cjs/core/slot-map/index.js +1 -0
- package/dist/cjs/core/slot-map/slot-map.js +1 -0
- package/dist/cjs/core/state-machine/index.js +1 -0
- package/dist/cjs/core/state-machine/state-machine.js +1 -0
- package/dist/cjs/core/timeline/index.js +1 -0
- package/dist/cjs/core/timeline/timeline.js +1 -0
- package/dist/cjs/game/loop/loop.js +1 -1
- package/dist/cjs/game/loop/ticker-schedule.js +1 -0
- package/dist/cjs/renderer/index.js +1 -1
- package/dist/cjs/renderer/prefab-bucket/concrete.js +1 -1
- package/dist/cjs/renderer/prefab-bucket/index.js +1 -1
- package/dist/cjs/renderer/prefab-bucket/parsers.js +1 -1
- package/dist/cjs/renderer/prefab-bucket/specs.js +1 -1
- package/dist/cjs/renderer/raycast/index.js +1 -0
- package/dist/cjs/renderer/raycast/raycast.js +1 -0
- package/dist/esm/core/clock/clock.js +1 -0
- package/dist/esm/core/clock/index.js +1 -0
- package/dist/esm/core/hitbox/hitbox-library.js +1 -0
- package/dist/esm/core/hitbox/hitbox.js +1 -0
- package/dist/esm/core/hitbox/index.js +1 -0
- package/dist/esm/core/hitbox/test.js +1 -0
- package/dist/esm/core/index.js +1 -1
- package/dist/esm/core/prediction/prediction.js +1 -1
- package/dist/esm/core/ray/ray-3d.js +1 -1
- package/dist/esm/core/raycast/hit-buffer.js +1 -0
- package/dist/esm/core/raycast/index.js +1 -0
- package/dist/esm/core/raycast/raycaster.js +1 -0
- package/dist/esm/core/slot-map/index.js +1 -0
- package/dist/esm/core/slot-map/slot-map.js +1 -0
- package/dist/esm/core/state-machine/index.js +1 -0
- package/dist/esm/core/state-machine/state-machine.js +1 -0
- package/dist/esm/core/timeline/index.js +1 -0
- package/dist/esm/core/timeline/timeline.js +1 -0
- package/dist/esm/game/loop/loop.js +1 -1
- package/dist/esm/game/loop/ticker-schedule.js +1 -0
- package/dist/esm/renderer/index.js +1 -1
- package/dist/esm/renderer/prefab-bucket/concrete.js +1 -1
- package/dist/esm/renderer/prefab-bucket/index.js +1 -1
- package/dist/esm/renderer/prefab-bucket/parsers.js +1 -1
- package/dist/esm/renderer/raycast/index.js +1 -0
- package/dist/esm/renderer/raycast/raycast.js +1 -0
- package/dist/netcode/cjs/index.js +144 -140
- package/dist/netcode/esm/index.js +144 -140
- package/dist/netcode/types/client/game-client.d.ts +17 -3
- package/dist/netcode/types/client/strategies/snapshot-interpolation.d.ts +33 -0
- package/dist/netcode/types/codec/delta-codec.d.ts +1 -1
- package/dist/netcode/types/components/sync-spec.d.ts +6 -0
- package/dist/types/core/clock/clock.d.ts +37 -0
- package/dist/types/core/clock/index.d.ts +1 -0
- package/dist/types/core/hitbox/hitbox-library.d.ts +29 -0
- package/dist/types/core/hitbox/hitbox.d.ts +50 -0
- package/dist/types/core/hitbox/index.d.ts +3 -0
- package/dist/types/core/hitbox/test.d.ts +44 -0
- package/dist/types/core/index.d.ts +6 -0
- package/dist/types/core/prediction/prediction.d.ts +35 -58
- package/dist/types/core/ray/ray-3d.d.ts +21 -1
- package/dist/types/core/raycast/hit-buffer.d.ts +43 -0
- package/dist/types/core/raycast/index.d.ts +2 -0
- package/dist/types/core/raycast/raycaster.d.ts +54 -0
- package/dist/types/core/slot-map/index.d.ts +1 -0
- package/dist/types/core/slot-map/slot-map.d.ts +109 -0
- package/dist/types/core/state-machine/index.d.ts +1 -0
- package/dist/types/core/state-machine/state-machine.d.ts +114 -0
- package/dist/types/core/timeline/index.d.ts +1 -0
- package/dist/types/core/timeline/timeline.d.ts +34 -0
- package/dist/types/game/loop/loop.d.ts +30 -0
- package/dist/types/game/loop/ticker-schedule.d.ts +52 -0
- package/dist/types/renderer/index.d.ts +1 -0
- package/dist/types/renderer/prefab-bucket/concrete.d.ts +16 -6
- package/dist/types/renderer/prefab-bucket/index.d.ts +11 -7
- package/dist/types/renderer/prefab-bucket/specs.d.ts +10 -0
- package/dist/types/renderer/raycast/index.d.ts +1 -0
- package/dist/types/renderer/raycast/raycast.d.ts +24 -0
- package/dist/types/renderer/types.d.ts +1 -0
- package/dist/webgpu/cjs/index.js +1777 -587
- package/dist/webgpu/esm/index.js +1769 -573
- package/dist/webgpu/types/2d/raycast.d.ts +45 -0
- package/dist/webgpu/types/2d/renderer.d.ts +11 -0
- package/dist/webgpu/types/2d/sprite-accessor.d.ts +3 -1
- package/dist/webgpu/types/3d/hitbox.d.ts +32 -0
- package/dist/webgpu/types/3d/lights.d.ts +113 -0
- package/dist/webgpu/types/3d/lights.test.d.ts +1 -0
- package/dist/webgpu/types/3d/raycast.d.ts +44 -0
- package/dist/webgpu/types/3d/renderer.d.ts +50 -1
- package/dist/webgpu/types/3d/shader.d.ts +88 -5
- package/dist/webgpu/types/core/types.d.ts +55 -0
- package/dist/webgpu/types/geometry/geometry-builder.d.ts +1 -4
- package/dist/webgpu/types/index.d.ts +1 -0
- package/dist/webgpu/types/shaders/utils.d.ts +24 -0
- package/package.json +1 -1
- package/dist/netcode/types/client/interpolation-buffer.d.ts +0 -37
- /package/dist/netcode/types/client/{interpolation-buffer.test.d.ts → strategies/snapshot-interpolation.test.d.ts} +0 -0
|
@@ -68,7 +68,7 @@ import { defineRPC as defineRPC2 } from "murow/protocol";
|
|
|
68
68
|
|
|
69
69
|
// src/codec/delta-codec.ts
|
|
70
70
|
var HEADER_BYTES = 4 + 4 + 2 + 2;
|
|
71
|
-
function encodeDelta(world, tick, entities, components, numMaskWords, despawned = [], clientAckTick = 0) {
|
|
71
|
+
function encodeDelta(world, tick, entities, components, numMaskWords, despawned = [], clientAckTick = 0, includeComponent = () => true) {
|
|
72
72
|
const perEntityMasks = new Array(entities.length);
|
|
73
73
|
const perEntityBitmaskBytes = numMaskWords * 4;
|
|
74
74
|
let bodyBytes = 0;
|
|
@@ -80,6 +80,8 @@ function encodeDelta(world, tick, entities, components, numMaskWords, despawned
|
|
|
80
80
|
const c = components[ci];
|
|
81
81
|
if (!world.has(eid, c))
|
|
82
82
|
continue;
|
|
83
|
+
if (!includeComponent(eid, c))
|
|
84
|
+
continue;
|
|
83
85
|
const wordIndex = ci >>> 5;
|
|
84
86
|
const bitIndex = ci & 31;
|
|
85
87
|
mask[wordIndex] |= 1 << bitIndex;
|
|
@@ -231,6 +233,18 @@ function decodeDelta(world, buf, components, numMaskWords, ensureEntity, shouldA
|
|
|
231
233
|
};
|
|
232
234
|
}
|
|
233
235
|
|
|
236
|
+
// src/components/sync-spec.ts
|
|
237
|
+
function networked(spec) {
|
|
238
|
+
return spec;
|
|
239
|
+
}
|
|
240
|
+
function rateIncludes(rate, dirty, tick) {
|
|
241
|
+
if (rate === "every-tick")
|
|
242
|
+
return true;
|
|
243
|
+
if (rate === "on-change")
|
|
244
|
+
return dirty;
|
|
245
|
+
return dirty || rate.every > 0 && tick % rate.every === 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
234
248
|
// src/ctx.ts
|
|
235
249
|
function makeFieldsAccessor(world, entity) {
|
|
236
250
|
return function fields(component) {
|
|
@@ -461,6 +475,7 @@ var GameServer = class extends Network {
|
|
|
461
475
|
}
|
|
462
476
|
if (current.length === 0 && despawned.length === 0 && !peer.needsBaseline)
|
|
463
477
|
continue;
|
|
478
|
+
const include = peer.needsBaseline ? void 0 : (eid, c) => rateIncludes(c.__sync.rate, this.world.isDirty(eid, c), this.tickCounter);
|
|
464
479
|
const buf = encodeDelta(
|
|
465
480
|
this.world,
|
|
466
481
|
this.tickCounter,
|
|
@@ -468,7 +483,8 @@ var GameServer = class extends Network {
|
|
|
468
483
|
this.syncedComponents,
|
|
469
484
|
this.syncedNumMaskWords,
|
|
470
485
|
despawned,
|
|
471
|
-
peer.lastAckedClientTick
|
|
486
|
+
peer.lastAckedClientTick,
|
|
487
|
+
include
|
|
472
488
|
);
|
|
473
489
|
const framed = new Uint8Array(buf.length + 1);
|
|
474
490
|
framed[0] = MSG_SNAPSHOT;
|
|
@@ -810,67 +826,67 @@ var LagCompensation = class {
|
|
|
810
826
|
// src/client/game-client.ts
|
|
811
827
|
import { u16 as u162 } from "murow/core/binary-codec";
|
|
812
828
|
import { SimpleRNG as SimpleRNG2 } from "murow/core/simple-rng";
|
|
829
|
+
import { Reconciler } from "murow/core/prediction";
|
|
813
830
|
import { defineRPC as defineRPC3 } from "murow/protocol";
|
|
814
831
|
|
|
815
|
-
// src/client/interpolation
|
|
832
|
+
// src/client/strategies/snapshot-interpolation.ts
|
|
816
833
|
import { lerp } from "murow/core/lerp";
|
|
834
|
+
import { Timeline } from "murow/core/timeline";
|
|
835
|
+
import { SlewClock } from "murow/core/clock";
|
|
817
836
|
function modeFor(c) {
|
|
818
837
|
const sync = c.__sync;
|
|
819
838
|
return sync?.interp ?? "lerp";
|
|
820
839
|
}
|
|
821
|
-
var
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
this.
|
|
826
|
-
this.smoothedTickRateMs = 0;
|
|
840
|
+
var DEFAULT_MAX_DESYNC = 500;
|
|
841
|
+
var DEFAULT_MAX_BRIDGE_GAP = 250;
|
|
842
|
+
var SnapshotInterpolation = class {
|
|
843
|
+
constructor(serverToLocal, capacity, delayMs, staleWindowMs, maxDesyncMs = DEFAULT_MAX_DESYNC, nominalTickMs = 0, maxBridgeGapMs = DEFAULT_MAX_BRIDGE_GAP) {
|
|
844
|
+
this.clock = new SlewClock();
|
|
827
845
|
this.serverToLocal = serverToLocal;
|
|
828
|
-
this.
|
|
846
|
+
this.timeline = new Timeline(capacity, staleWindowMs);
|
|
829
847
|
this.delay = delayMs;
|
|
830
|
-
this.
|
|
848
|
+
this.maxDesync = maxDesyncMs;
|
|
849
|
+
this.nominalTickMs = nominalTickMs;
|
|
850
|
+
this.smoothedTickRateMs = nominalTickMs;
|
|
851
|
+
this.maxBridgeGap = maxBridgeGapMs;
|
|
852
|
+
}
|
|
853
|
+
get staleWindow() {
|
|
854
|
+
return this.timeline.staleWindow;
|
|
831
855
|
}
|
|
832
856
|
setDelay(delayMs) {
|
|
833
857
|
this.delay = delayMs;
|
|
834
858
|
}
|
|
835
859
|
setStaleWindow(staleWindowMs) {
|
|
836
|
-
this.
|
|
860
|
+
this.timeline.setStaleWindow(staleWindowMs);
|
|
861
|
+
}
|
|
862
|
+
setMaxDesync(maxDesyncMs) {
|
|
863
|
+
this.maxDesync = maxDesyncMs;
|
|
864
|
+
}
|
|
865
|
+
setMaxBridgeGap(maxBridgeGapMs) {
|
|
866
|
+
this.maxBridgeGap = maxBridgeGapMs;
|
|
837
867
|
}
|
|
838
868
|
record(snapshot) {
|
|
839
|
-
const
|
|
840
|
-
if (
|
|
841
|
-
this.
|
|
842
|
-
this.
|
|
843
|
-
this.latestReceivedAt = -Infinity;
|
|
844
|
-
this.smoothedTickRateMs = 0;
|
|
845
|
-
}
|
|
846
|
-
if (snapshot.receivedAt > this.latestReceivedAt) {
|
|
847
|
-
this.latestReceivedAt = snapshot.receivedAt;
|
|
869
|
+
const reset = this.timeline.record(snapshot.serverTick, snapshot.receivedAt, snapshot);
|
|
870
|
+
if (reset) {
|
|
871
|
+
this.smoothedTickRateMs = this.nominalTickMs;
|
|
872
|
+
this.clock.reset();
|
|
848
873
|
}
|
|
849
|
-
let insertAt = this.buffer.length;
|
|
850
|
-
while (insertAt > 0 && this.buffer[insertAt - 1].serverTick >= snapshot.serverTick) {
|
|
851
|
-
if (this.buffer[insertAt - 1].serverTick === snapshot.serverTick)
|
|
852
|
-
return;
|
|
853
|
-
insertAt--;
|
|
854
|
-
}
|
|
855
|
-
this.buffer.splice(insertAt, 0, snapshot);
|
|
856
|
-
while (this.buffer.length > this.capacity)
|
|
857
|
-
this.buffer.shift();
|
|
858
874
|
}
|
|
859
875
|
clear() {
|
|
860
|
-
this.
|
|
861
|
-
this.
|
|
862
|
-
this.
|
|
863
|
-
this.smoothedTickRateMs = 0;
|
|
876
|
+
this.timeline.clear();
|
|
877
|
+
this.clock.reset();
|
|
878
|
+
this.smoothedTickRateMs = this.nominalTickMs;
|
|
864
879
|
}
|
|
865
880
|
apply(world, now, components, shouldSkip) {
|
|
866
|
-
|
|
881
|
+
const tl = this.timeline;
|
|
882
|
+
if (tl.length === 0)
|
|
867
883
|
return;
|
|
868
|
-
const newest =
|
|
869
|
-
const oldest =
|
|
884
|
+
const newest = tl.newest().sample;
|
|
885
|
+
const oldest = tl.oldest().sample;
|
|
870
886
|
let tickRateMs = 0;
|
|
871
|
-
if (
|
|
887
|
+
if (tl.length >= 2) {
|
|
872
888
|
const tickSpan = newest.serverTick - oldest.serverTick;
|
|
873
|
-
const wallSpan =
|
|
889
|
+
const wallSpan = tl.latestReceivedAt - oldest.receivedAt;
|
|
874
890
|
const rawTickRateMs = tickSpan > 0 && wallSpan > 0 ? wallSpan / tickSpan : 0;
|
|
875
891
|
if (rawTickRateMs > 0) {
|
|
876
892
|
if (this.smoothedTickRateMs === 0)
|
|
@@ -888,42 +904,23 @@ var InterpolationBuffer = class {
|
|
|
888
904
|
}
|
|
889
905
|
const ageBeyondDelay = now - newest.receivedAt - this.delay;
|
|
890
906
|
const targetTick = newest.serverTick + ageBeyondDelay / tickRateMs;
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
const drift = targetTick - this.renderTick;
|
|
895
|
-
if (drift > 2) {
|
|
896
|
-
this.renderTick = targetTick;
|
|
897
|
-
} else {
|
|
898
|
-
const warp = Math.max(0.9, Math.min(1.1, 1 + drift * 0.05));
|
|
899
|
-
this.renderTick += warp;
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
const renderTick = this.renderTick;
|
|
903
|
-
let a = null;
|
|
904
|
-
let b = null;
|
|
905
|
-
for (let i = 0; i < this.buffer.length - 1; i++) {
|
|
906
|
-
const s0 = this.buffer[i];
|
|
907
|
-
const s1 = this.buffer[i + 1];
|
|
908
|
-
if (s0.serverTick <= renderTick && renderTick <= s1.serverTick) {
|
|
909
|
-
a = s0;
|
|
910
|
-
b = s1;
|
|
911
|
-
break;
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
if (a === null || b === null) {
|
|
907
|
+
const renderTick = this.clock.advance(targetTick, this.maxDesync / tickRateMs);
|
|
908
|
+
const straddle = tl.straddle(renderTick);
|
|
909
|
+
if (straddle === null) {
|
|
915
910
|
if (renderTick < oldest.serverTick)
|
|
916
911
|
return;
|
|
917
912
|
this.writeSnapshot(world, newest, components, shouldSkip);
|
|
918
913
|
return;
|
|
919
914
|
}
|
|
915
|
+
const [aIndex, bIndex] = straddle;
|
|
916
|
+
const a = tl.at(aIndex).sample;
|
|
917
|
+
const b = tl.at(bIndex).sample;
|
|
920
918
|
const seen = /* @__PURE__ */ new Set();
|
|
921
919
|
for (const eid of a.entityIds)
|
|
922
920
|
seen.add(eid);
|
|
923
921
|
for (const eid of b.entityIds)
|
|
924
922
|
seen.add(eid);
|
|
925
|
-
const
|
|
926
|
-
const bIndex = this.buffer.indexOf(b);
|
|
923
|
+
const maxBridgeTicks = this.maxBridgeGap / tickRateMs;
|
|
927
924
|
for (const serverEid of seen) {
|
|
928
925
|
const localEid = this.serverToLocal.get(serverEid);
|
|
929
926
|
if (localEid === void 0)
|
|
@@ -935,13 +932,13 @@ var InterpolationBuffer = class {
|
|
|
935
932
|
let va = a.componentValuesByEntity.get(serverEid)?.get(c);
|
|
936
933
|
while (va === void 0 && aIdx > 0) {
|
|
937
934
|
aIdx--;
|
|
938
|
-
va =
|
|
935
|
+
va = tl.at(aIdx).sample.componentValuesByEntity.get(serverEid)?.get(c);
|
|
939
936
|
}
|
|
940
937
|
let bIdx = bIndex;
|
|
941
938
|
let vb = b.componentValuesByEntity.get(serverEid)?.get(c);
|
|
942
|
-
while (vb === void 0 && bIdx <
|
|
939
|
+
while (vb === void 0 && bIdx < tl.length - 1) {
|
|
943
940
|
bIdx++;
|
|
944
|
-
vb =
|
|
941
|
+
vb = tl.at(bIdx).sample.componentValuesByEntity.get(serverEid)?.get(c);
|
|
945
942
|
}
|
|
946
943
|
if (va === void 0 && vb === void 0)
|
|
947
944
|
continue;
|
|
@@ -955,8 +952,10 @@ var InterpolationBuffer = class {
|
|
|
955
952
|
if (mode === "none") {
|
|
956
953
|
toWrite = vb;
|
|
957
954
|
} else {
|
|
958
|
-
const
|
|
959
|
-
|
|
955
|
+
const bTick = tl.at(bIdx).sample.serverTick;
|
|
956
|
+
let aTick = tl.at(aIdx).sample.serverTick;
|
|
957
|
+
if (bTick - aTick > maxBridgeTicks)
|
|
958
|
+
aTick = bTick - 1;
|
|
960
959
|
const wideSpan = bTick - aTick;
|
|
961
960
|
const wideT = wideSpan > 0 ? Math.min(1, Math.max(0, (renderTick - aTick) / wideSpan)) : 0;
|
|
962
961
|
if (mode === "step") {
|
|
@@ -1033,7 +1032,6 @@ var GameClient = class extends Network {
|
|
|
1033
1032
|
"error"
|
|
1034
1033
|
]);
|
|
1035
1034
|
this.predictionMap = null;
|
|
1036
|
-
this.predictionHistory = [];
|
|
1037
1035
|
this.localTick = 0;
|
|
1038
1036
|
this.intentSequence = 0;
|
|
1039
1037
|
this.lastServerTick = 0;
|
|
@@ -1044,6 +1042,8 @@ var GameClient = class extends Network {
|
|
|
1044
1042
|
this.predictedEntities = /* @__PURE__ */ new Set();
|
|
1045
1043
|
/** Set when MSG_ASSIGN_ENTITY lands before the matching spawn. */
|
|
1046
1044
|
this.pendingAssignedServerEid = null;
|
|
1045
|
+
/** Spawns discovered during a decode, emitted once values are written. */
|
|
1046
|
+
this.deferredSpawns = [];
|
|
1047
1047
|
/** Resolved local entity for the server's assignment. Default for sendIntent. */
|
|
1048
1048
|
this._assignedEntity = null;
|
|
1049
1049
|
this._rttMs = null;
|
|
@@ -1064,12 +1064,60 @@ var GameClient = class extends Network {
|
|
|
1064
1064
|
this.rng = new SimpleRNG2(1);
|
|
1065
1065
|
this.lastDt = opts.loop.ticker.intervalMs / 1e3;
|
|
1066
1066
|
this.now = opts.now ?? (() => performance.now());
|
|
1067
|
-
this.interpBuffer = new
|
|
1067
|
+
this.interpBuffer = new SnapshotInterpolation(
|
|
1068
1068
|
this.serverToLocal,
|
|
1069
1069
|
16,
|
|
1070
1070
|
interpolationDelay,
|
|
1071
|
-
staleWindowMs
|
|
1071
|
+
staleWindowMs,
|
|
1072
|
+
strategy.maxDesync,
|
|
1073
|
+
opts.loop.ticker.intervalMs,
|
|
1074
|
+
strategy.maxBridgeGap
|
|
1072
1075
|
);
|
|
1076
|
+
this.reconciler = new Reconciler({
|
|
1077
|
+
bufferSize: this.predictionBufferSize,
|
|
1078
|
+
restore: ({ decoded, resetEntities }) => {
|
|
1079
|
+
for (const serverEid of decoded.serverEntityIds) {
|
|
1080
|
+
const localEid = this.serverToLocal.get(serverEid);
|
|
1081
|
+
if (localEid === void 0)
|
|
1082
|
+
continue;
|
|
1083
|
+
if (!this.predictedEntities.has(localEid))
|
|
1084
|
+
continue;
|
|
1085
|
+
const comps = decoded.valuesByServerEntity.get(serverEid);
|
|
1086
|
+
if (comps === void 0)
|
|
1087
|
+
continue;
|
|
1088
|
+
for (const [c, value] of comps) {
|
|
1089
|
+
if (this.world.has(localEid, c)) {
|
|
1090
|
+
this.world.update(localEid, c, value);
|
|
1091
|
+
} else {
|
|
1092
|
+
this.world.add(localEid, c, value);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
resetEntities.add(localEid);
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
replay: (preds, { decoded, resetEntities }) => {
|
|
1099
|
+
let replayedCount = 0;
|
|
1100
|
+
for (const pred of preds) {
|
|
1101
|
+
if (!resetEntities.has(pred.entity))
|
|
1102
|
+
continue;
|
|
1103
|
+
const predFn = this.predictionMap?.[pred.name];
|
|
1104
|
+
if (predFn === void 0)
|
|
1105
|
+
continue;
|
|
1106
|
+
const ctx = {
|
|
1107
|
+
world: this.world,
|
|
1108
|
+
entity: pred.entity,
|
|
1109
|
+
tick: pred.tick,
|
|
1110
|
+
deltaTime: pred.deltaTime,
|
|
1111
|
+
rng: this.rng,
|
|
1112
|
+
fields: makeFieldsAccessor(this.world, pred.entity),
|
|
1113
|
+
markDirty: makeMarkDirty(this.world, pred.entity)
|
|
1114
|
+
};
|
|
1115
|
+
predFn(pred.payload, ctx);
|
|
1116
|
+
replayedCount++;
|
|
1117
|
+
}
|
|
1118
|
+
this.emit("reconciled", { rewindTick: decoded.clientAckTick, replayed: replayedCount });
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1073
1121
|
this.discoverSyncedComponents();
|
|
1074
1122
|
this.wireTransport();
|
|
1075
1123
|
this.wireLoop();
|
|
@@ -1156,6 +1204,7 @@ var GameClient = class extends Network {
|
|
|
1156
1204
|
}
|
|
1157
1205
|
handleSnapshot(payload) {
|
|
1158
1206
|
try {
|
|
1207
|
+
this.deferredSpawns.length = 0;
|
|
1159
1208
|
const decoded = decodeDelta(
|
|
1160
1209
|
this.world,
|
|
1161
1210
|
payload,
|
|
@@ -1169,6 +1218,12 @@ var GameClient = class extends Network {
|
|
|
1169
1218
|
() => false
|
|
1170
1219
|
);
|
|
1171
1220
|
this.lastServerTick = decoded.tick;
|
|
1221
|
+
for (const spawn of this.deferredSpawns) {
|
|
1222
|
+
this.emit("spawn", { entity: spawn.entity, components: spawn.components });
|
|
1223
|
+
if (spawn.assigned)
|
|
1224
|
+
this.emit("assigned", { entity: spawn.entity });
|
|
1225
|
+
}
|
|
1226
|
+
this.deferredSpawns.length = 0;
|
|
1172
1227
|
this.emit("snapshot", { tick: decoded.tick, byteSize: payload.length + 1 });
|
|
1173
1228
|
this.interpBuffer.record({
|
|
1174
1229
|
receivedAt: this.now(),
|
|
@@ -1201,13 +1256,14 @@ var GameClient = class extends Network {
|
|
|
1201
1256
|
const components = {};
|
|
1202
1257
|
for (const c of present)
|
|
1203
1258
|
components[c.name] = true;
|
|
1204
|
-
|
|
1259
|
+
let assigned = false;
|
|
1205
1260
|
if (this.pendingAssignedServerEid === serverEid) {
|
|
1206
1261
|
this.pendingAssignedServerEid = null;
|
|
1207
1262
|
this.predictedEntities.add(localEid);
|
|
1208
1263
|
this._assignedEntity = localEid;
|
|
1209
|
-
|
|
1264
|
+
assigned = true;
|
|
1210
1265
|
}
|
|
1266
|
+
this.deferredSpawns.push({ entity: localEid, components, assigned });
|
|
1211
1267
|
}
|
|
1212
1268
|
return localEid;
|
|
1213
1269
|
}
|
|
@@ -1226,60 +1282,9 @@ var GameClient = class extends Network {
|
|
|
1226
1282
|
* the server has acked, replay the rest on top.
|
|
1227
1283
|
*/
|
|
1228
1284
|
reconcile(decoded) {
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
if (localEid === void 0)
|
|
1233
|
-
continue;
|
|
1234
|
-
if (!this.predictedEntities.has(localEid))
|
|
1235
|
-
continue;
|
|
1236
|
-
const comps = decoded.valuesByServerEntity.get(serverEid);
|
|
1237
|
-
if (comps === void 0)
|
|
1238
|
-
continue;
|
|
1239
|
-
for (const [c, value] of comps) {
|
|
1240
|
-
if (this.world.has(localEid, c)) {
|
|
1241
|
-
this.world.update(localEid, c, value);
|
|
1242
|
-
} else {
|
|
1243
|
-
this.world.add(localEid, c, value);
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
resetEntities.add(localEid);
|
|
1247
|
-
}
|
|
1248
|
-
const ackSequence = decoded.clientAckTick;
|
|
1249
|
-
let cut = 0;
|
|
1250
|
-
while (cut < this.predictionHistory.length && this.predictionHistory[cut].sequence <= ackSequence) {
|
|
1251
|
-
cut++;
|
|
1252
|
-
}
|
|
1253
|
-
if (cut > 0)
|
|
1254
|
-
this.predictionHistory.splice(0, cut);
|
|
1255
|
-
const remaining = this.predictionHistory.length;
|
|
1256
|
-
if (remaining === 0) {
|
|
1257
|
-
this.emit("reconciled", { rewindTick: ackSequence, replayed: 0 });
|
|
1258
|
-
return;
|
|
1259
|
-
}
|
|
1260
|
-
let replayedCount = 0;
|
|
1261
|
-
for (let i = 0; i < remaining; i++) {
|
|
1262
|
-
const pred = this.predictionHistory[i];
|
|
1263
|
-
if (!resetEntities.has(pred.entity))
|
|
1264
|
-
continue;
|
|
1265
|
-
const predFn = this.predictionMap?.[pred.name];
|
|
1266
|
-
if (predFn === void 0)
|
|
1267
|
-
continue;
|
|
1268
|
-
const ctx = {
|
|
1269
|
-
world: this.world,
|
|
1270
|
-
entity: pred.entity,
|
|
1271
|
-
tick: pred.tick,
|
|
1272
|
-
deltaTime: pred.deltaTime,
|
|
1273
|
-
rng: this.rng,
|
|
1274
|
-
fields: makeFieldsAccessor(this.world, pred.entity),
|
|
1275
|
-
markDirty: makeMarkDirty(this.world, pred.entity)
|
|
1276
|
-
};
|
|
1277
|
-
predFn(pred.payload, ctx);
|
|
1278
|
-
replayedCount++;
|
|
1279
|
-
}
|
|
1280
|
-
this.emit("reconciled", {
|
|
1281
|
-
rewindTick: ackSequence,
|
|
1282
|
-
replayed: replayedCount
|
|
1285
|
+
this.reconciler.reconcile(decoded.clientAckTick, {
|
|
1286
|
+
decoded,
|
|
1287
|
+
resetEntities: /* @__PURE__ */ new Set()
|
|
1283
1288
|
});
|
|
1284
1289
|
}
|
|
1285
1290
|
handleRpc(payload) {
|
|
@@ -1353,7 +1358,7 @@ var GameClient = class extends Network {
|
|
|
1353
1358
|
markDirty: makeMarkDirty(this.world, entity)
|
|
1354
1359
|
};
|
|
1355
1360
|
predFn(payload, ctx);
|
|
1356
|
-
this.
|
|
1361
|
+
this.reconciler.record(sequence, {
|
|
1357
1362
|
tick: this.localTick,
|
|
1358
1363
|
sequence,
|
|
1359
1364
|
name,
|
|
@@ -1362,9 +1367,6 @@ var GameClient = class extends Network {
|
|
|
1362
1367
|
deltaTime
|
|
1363
1368
|
});
|
|
1364
1369
|
this.predictedEntities.add(entity);
|
|
1365
|
-
while (this.predictionHistory.length > this.predictionBufferSize) {
|
|
1366
|
-
this.predictionHistory.shift();
|
|
1367
|
-
}
|
|
1368
1370
|
}
|
|
1369
1371
|
return true;
|
|
1370
1372
|
}
|
|
@@ -1395,7 +1397,7 @@ var GameClient = class extends Network {
|
|
|
1395
1397
|
return this.localTick;
|
|
1396
1398
|
}
|
|
1397
1399
|
getPredictionDepth() {
|
|
1398
|
-
return this.
|
|
1400
|
+
return this.reconciler.pending;
|
|
1399
1401
|
}
|
|
1400
1402
|
get interpolationDelay() {
|
|
1401
1403
|
return this.interpBuffer.delay;
|
|
@@ -1403,6 +1405,12 @@ var GameClient = class extends Network {
|
|
|
1403
1405
|
setInterpolationDelay(ms) {
|
|
1404
1406
|
this.interpBuffer.setDelay(ms);
|
|
1405
1407
|
}
|
|
1408
|
+
setMaxDesync(ms) {
|
|
1409
|
+
this.interpBuffer.setMaxDesync(ms);
|
|
1410
|
+
}
|
|
1411
|
+
setMaxBridgeGap(ms) {
|
|
1412
|
+
this.interpBuffer.setMaxBridgeGap(ms);
|
|
1413
|
+
}
|
|
1406
1414
|
};
|
|
1407
1415
|
|
|
1408
1416
|
// src/transports/memory-transport.ts
|
|
@@ -1505,11 +1513,6 @@ var MemoryPeerTransport = class {
|
|
|
1505
1513
|
queueMicrotask(() => this.clientOpenHandler?.());
|
|
1506
1514
|
}
|
|
1507
1515
|
};
|
|
1508
|
-
|
|
1509
|
-
// src/components/sync-spec.ts
|
|
1510
|
-
function networked(spec) {
|
|
1511
|
-
return spec;
|
|
1512
|
-
}
|
|
1513
1516
|
export {
|
|
1514
1517
|
AoiGrid,
|
|
1515
1518
|
GameClient,
|
|
@@ -1526,5 +1529,6 @@ export {
|
|
|
1526
1529
|
encodeDelta,
|
|
1527
1530
|
makeFieldsAccessor,
|
|
1528
1531
|
makeMarkDirty,
|
|
1529
|
-
networked
|
|
1532
|
+
networked,
|
|
1533
|
+
rateIncludes
|
|
1530
1534
|
};
|
|
@@ -2,7 +2,7 @@ import type { Entity, World } from 'murow/ecs';
|
|
|
2
2
|
import type { GameLoop } from 'murow/game';
|
|
3
3
|
import type { TransportAdapter } from 'murow/net';
|
|
4
4
|
import { Network } from '../network/base';
|
|
5
|
-
import {
|
|
5
|
+
import { SnapshotInterpolation } from './strategies/snapshot-interpolation';
|
|
6
6
|
import type { DefinedIntents, IntentPayload, IntentSchemaMap } from '../intents/define-intents';
|
|
7
7
|
import type { DefinedRpcs, RpcPayload, RpcSchemaMap } from '../rpcs/define-rpcs';
|
|
8
8
|
import type { DefinedPredictions } from '../predictions/define-predictions';
|
|
@@ -22,6 +22,16 @@ export interface SnapshotInterpolationStrategy {
|
|
|
22
22
|
* Defaults to `delay * 2 + 100`.
|
|
23
23
|
*/
|
|
24
24
|
staleWindow?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Desync past which the play-out clock snaps instead of warping, ms.
|
|
27
|
+
* Scaled by tick rate internally. Default 500.
|
|
28
|
+
*/
|
|
29
|
+
maxDesync?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Largest data gap a peer may have before its value is held instead of
|
|
32
|
+
* interpolated across, ms. Smaller gaps are bridged. Default 250.
|
|
33
|
+
*/
|
|
34
|
+
maxBridgeGap?: number;
|
|
25
35
|
}
|
|
26
36
|
/**
|
|
27
37
|
* How peer entities are rendered on the client. Local-player rendering
|
|
@@ -59,7 +69,7 @@ export declare class GameClient<I extends IntentSchemaMap = IntentSchemaMap, R e
|
|
|
59
69
|
readonly rpcs: DefinedRpcs<R>;
|
|
60
70
|
private predictionMap;
|
|
61
71
|
private predictionBufferSize;
|
|
62
|
-
private
|
|
72
|
+
private reconciler;
|
|
63
73
|
private localTick;
|
|
64
74
|
private intentSequence;
|
|
65
75
|
private lastServerTick;
|
|
@@ -72,9 +82,11 @@ export declare class GameClient<I extends IntentSchemaMap = IntentSchemaMap, R e
|
|
|
72
82
|
private predictedEntities;
|
|
73
83
|
/** Set when MSG_ASSIGN_ENTITY lands before the matching spawn. */
|
|
74
84
|
private pendingAssignedServerEid;
|
|
85
|
+
/** Spawns discovered during a decode, emitted once values are written. */
|
|
86
|
+
private deferredSpawns;
|
|
75
87
|
/** Resolved local entity for the server's assignment. Default for sendIntent. */
|
|
76
88
|
private _assignedEntity;
|
|
77
|
-
interpBuffer:
|
|
89
|
+
interpBuffer: SnapshotInterpolation;
|
|
78
90
|
private _rttMs;
|
|
79
91
|
private now;
|
|
80
92
|
/**
|
|
@@ -122,4 +134,6 @@ export declare class GameClient<I extends IntentSchemaMap = IntentSchemaMap, R e
|
|
|
122
134
|
getPredictionDepth(): number;
|
|
123
135
|
get interpolationDelay(): number;
|
|
124
136
|
setInterpolationDelay(ms: number): void;
|
|
137
|
+
setMaxDesync(ms: number): void;
|
|
138
|
+
setMaxBridgeGap(ms: number): void;
|
|
125
139
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Component, Entity, World } from 'murow/ecs';
|
|
2
|
+
export interface BufferedSnapshot {
|
|
3
|
+
receivedAt: number;
|
|
4
|
+
serverTick: number;
|
|
5
|
+
entityIds: number[];
|
|
6
|
+
componentValuesByEntity: Map<number, Map<Component<any>, Record<string, number>>>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Snapshot-interpolation strategy: renders peer entities `delay` ms behind the
|
|
10
|
+
* newest snapshot, lerping between the two snapshots straddling that past time.
|
|
11
|
+
* Composes a core `Timeline` (snapshot history) and a core `SlewClock`
|
|
12
|
+
* (play-out clock). Predicted entities are skipped; reconciliation drives them.
|
|
13
|
+
*/
|
|
14
|
+
export declare class SnapshotInterpolation {
|
|
15
|
+
private serverToLocal;
|
|
16
|
+
private timeline;
|
|
17
|
+
private clock;
|
|
18
|
+
private smoothedTickRateMs;
|
|
19
|
+
private nominalTickMs;
|
|
20
|
+
delay: number;
|
|
21
|
+
maxDesync: number;
|
|
22
|
+
maxBridgeGap: number;
|
|
23
|
+
constructor(serverToLocal: Map<number, Entity>, capacity: number, delayMs: number, staleWindowMs: number, maxDesyncMs?: number, nominalTickMs?: number, maxBridgeGapMs?: number);
|
|
24
|
+
get staleWindow(): number;
|
|
25
|
+
setDelay(delayMs: number): void;
|
|
26
|
+
setStaleWindow(staleWindowMs: number): void;
|
|
27
|
+
setMaxDesync(maxDesyncMs: number): void;
|
|
28
|
+
setMaxBridgeGap(maxBridgeGapMs: number): void;
|
|
29
|
+
record(snapshot: BufferedSnapshot): void;
|
|
30
|
+
clear(): void;
|
|
31
|
+
apply(world: World, now: number, components: Component<any>[], shouldSkip: (entity: Entity) => boolean): void;
|
|
32
|
+
private writeSnapshot;
|
|
33
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Component, World } from 'murow/ecs';
|
|
2
|
-
export declare function encodeDelta(world: World, tick: number, entities: number[], components: Component<any>[], numMaskWords: number, despawned?: number[], clientAckTick?: number): Uint8Array;
|
|
2
|
+
export declare function encodeDelta(world: World, tick: number, entities: number[], components: Component<any>[], numMaskWords: number, despawned?: number[], clientAckTick?: number, includeComponent?: (entity: number, component: Component<any>) => boolean): Uint8Array;
|
|
3
3
|
export interface DecodedDelta {
|
|
4
4
|
tick: number;
|
|
5
5
|
clientAckTick: number;
|
|
@@ -41,3 +41,9 @@ export interface SyncSpec {
|
|
|
41
41
|
* });
|
|
42
42
|
*/
|
|
43
43
|
export declare function networked(spec: SyncSpec): SyncSpec;
|
|
44
|
+
/**
|
|
45
|
+
* Whether a component's value rides along in the snapshot for the given
|
|
46
|
+
* tick. `every-tick` always; `on-change` only when the field changed
|
|
47
|
+
* since the last snapshot; `{ every: N }` on every Nth tick or on change.
|
|
48
|
+
*/
|
|
49
|
+
export declare function rateIncludes(rate: SyncRate, dirty: boolean, tick: number): boolean;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface SlewClockOptions {
|
|
2
|
+
/** Drift under this advances at nominal rate instead of warping. Default 0.25. */
|
|
3
|
+
deadZone?: number;
|
|
4
|
+
/** Step bounds while warping to close a gap. */
|
|
5
|
+
warp?: {
|
|
6
|
+
/** Minimum step when warping to close a gap. Default 0.6. */
|
|
7
|
+
min?: number;
|
|
8
|
+
/** Maximum step when warping to close a gap. Default 1.4. */
|
|
9
|
+
max?: number;
|
|
10
|
+
};
|
|
11
|
+
/** Per-unit-of-drift warp gain. Default 0.1. */
|
|
12
|
+
gain?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A scalar that advances one nominal step per `advance` toward a moving
|
|
16
|
+
* target, slewing within a band to close drift, holding steady inside a
|
|
17
|
+
* dead-zone, and snapping when the forward gap is too large to chase.
|
|
18
|
+
*
|
|
19
|
+
* Domain-agnostic: the caller supplies the target and the snap threshold.
|
|
20
|
+
*/
|
|
21
|
+
export declare class SlewClock {
|
|
22
|
+
private _value;
|
|
23
|
+
private readonly deadZone;
|
|
24
|
+
private readonly warpMin;
|
|
25
|
+
private readonly warpMax;
|
|
26
|
+
private readonly gain;
|
|
27
|
+
constructor(opts?: SlewClockOptions);
|
|
28
|
+
get value(): number;
|
|
29
|
+
get initialized(): boolean;
|
|
30
|
+
reset(): void;
|
|
31
|
+
/**
|
|
32
|
+
* Advance toward `target`. Seeds to `target` on first call. Forward drift
|
|
33
|
+
* beyond `snap` jumps to `target`; drift inside the dead-zone advances one
|
|
34
|
+
* nominal step; otherwise the step warps within the band to close the gap.
|
|
35
|
+
*/
|
|
36
|
+
advance(target: number, snap: number): number;
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SlewClock, type SlewClockOptions } from './clock';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Hitbox } from './hitbox';
|
|
2
|
+
/**
|
|
3
|
+
* HitboxLibrary — a named, mode-typed registry of `Hitbox` definitions.
|
|
4
|
+
*
|
|
5
|
+
* The canonical set of collision archetypes a game uses, declared once in
|
|
6
|
+
* shared code and read by the renderer (client picking), game logic, and a
|
|
7
|
+
* headless server alike. Resolves by name (`get`, serializable/authoring)
|
|
8
|
+
* or by index (`at`, for ECS components that store a numeric archetype).
|
|
9
|
+
*
|
|
10
|
+
* `add` accumulates the name union, so `bucket.hitboxes(lib)` can offer the
|
|
11
|
+
* registered names as an autocompleting, typo-checked literal type.
|
|
12
|
+
*/
|
|
13
|
+
export declare class HitboxLibrary<M extends '2d' | '3d' = '3d', Names extends string = never> {
|
|
14
|
+
readonly mode: M;
|
|
15
|
+
private readonly names;
|
|
16
|
+
private readonly boxes;
|
|
17
|
+
private readonly index;
|
|
18
|
+
constructor(mode: M);
|
|
19
|
+
/** Register a hitbox under `name`. Returns a library whose type carries the new name. */
|
|
20
|
+
add<const N extends string>(name: N, hitbox: Hitbox<M>): HitboxLibrary<M, Names | N>;
|
|
21
|
+
/** Resolve by name. Throws on an unknown name. */
|
|
22
|
+
get(name: Names): Hitbox<M>;
|
|
23
|
+
/** Resolve by index — the fast path for ECS components storing a numeric archetype. */
|
|
24
|
+
at(index: number): Hitbox<M>;
|
|
25
|
+
/** Numeric index for a name, for storing on an entity. Throws on an unknown name. */
|
|
26
|
+
indexOf(name: Names): number;
|
|
27
|
+
/** Registered names, in insertion order. */
|
|
28
|
+
keys(): readonly string[];
|
|
29
|
+
}
|