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.
Files changed (105) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/core/clock/clock.js +1 -0
  3. package/dist/cjs/core/clock/index.js +1 -0
  4. package/dist/cjs/core/hitbox/hitbox-library.js +1 -0
  5. package/dist/cjs/core/hitbox/hitbox.js +1 -0
  6. package/dist/cjs/core/hitbox/index.js +1 -0
  7. package/dist/cjs/core/hitbox/test.js +1 -0
  8. package/dist/cjs/core/index.js +1 -1
  9. package/dist/cjs/core/prediction/prediction.js +1 -1
  10. package/dist/cjs/core/ray/ray-3d.js +1 -1
  11. package/dist/cjs/core/raycast/hit-buffer.js +1 -0
  12. package/dist/cjs/core/raycast/index.js +1 -0
  13. package/dist/cjs/core/raycast/raycaster.js +1 -0
  14. package/dist/cjs/core/slot-map/index.js +1 -0
  15. package/dist/cjs/core/slot-map/slot-map.js +1 -0
  16. package/dist/cjs/core/state-machine/index.js +1 -0
  17. package/dist/cjs/core/state-machine/state-machine.js +1 -0
  18. package/dist/cjs/core/timeline/index.js +1 -0
  19. package/dist/cjs/core/timeline/timeline.js +1 -0
  20. package/dist/cjs/game/loop/loop.js +1 -1
  21. package/dist/cjs/game/loop/ticker-schedule.js +1 -0
  22. package/dist/cjs/renderer/index.js +1 -1
  23. package/dist/cjs/renderer/prefab-bucket/concrete.js +1 -1
  24. package/dist/cjs/renderer/prefab-bucket/index.js +1 -1
  25. package/dist/cjs/renderer/prefab-bucket/parsers.js +1 -1
  26. package/dist/cjs/renderer/prefab-bucket/specs.js +1 -1
  27. package/dist/cjs/renderer/raycast/index.js +1 -0
  28. package/dist/cjs/renderer/raycast/raycast.js +1 -0
  29. package/dist/esm/core/clock/clock.js +1 -0
  30. package/dist/esm/core/clock/index.js +1 -0
  31. package/dist/esm/core/hitbox/hitbox-library.js +1 -0
  32. package/dist/esm/core/hitbox/hitbox.js +1 -0
  33. package/dist/esm/core/hitbox/index.js +1 -0
  34. package/dist/esm/core/hitbox/test.js +1 -0
  35. package/dist/esm/core/index.js +1 -1
  36. package/dist/esm/core/prediction/prediction.js +1 -1
  37. package/dist/esm/core/ray/ray-3d.js +1 -1
  38. package/dist/esm/core/raycast/hit-buffer.js +1 -0
  39. package/dist/esm/core/raycast/index.js +1 -0
  40. package/dist/esm/core/raycast/raycaster.js +1 -0
  41. package/dist/esm/core/slot-map/index.js +1 -0
  42. package/dist/esm/core/slot-map/slot-map.js +1 -0
  43. package/dist/esm/core/state-machine/index.js +1 -0
  44. package/dist/esm/core/state-machine/state-machine.js +1 -0
  45. package/dist/esm/core/timeline/index.js +1 -0
  46. package/dist/esm/core/timeline/timeline.js +1 -0
  47. package/dist/esm/game/loop/loop.js +1 -1
  48. package/dist/esm/game/loop/ticker-schedule.js +1 -0
  49. package/dist/esm/renderer/index.js +1 -1
  50. package/dist/esm/renderer/prefab-bucket/concrete.js +1 -1
  51. package/dist/esm/renderer/prefab-bucket/index.js +1 -1
  52. package/dist/esm/renderer/prefab-bucket/parsers.js +1 -1
  53. package/dist/esm/renderer/raycast/index.js +1 -0
  54. package/dist/esm/renderer/raycast/raycast.js +1 -0
  55. package/dist/netcode/cjs/index.js +144 -140
  56. package/dist/netcode/esm/index.js +144 -140
  57. package/dist/netcode/types/client/game-client.d.ts +17 -3
  58. package/dist/netcode/types/client/strategies/snapshot-interpolation.d.ts +33 -0
  59. package/dist/netcode/types/codec/delta-codec.d.ts +1 -1
  60. package/dist/netcode/types/components/sync-spec.d.ts +6 -0
  61. package/dist/types/core/clock/clock.d.ts +37 -0
  62. package/dist/types/core/clock/index.d.ts +1 -0
  63. package/dist/types/core/hitbox/hitbox-library.d.ts +29 -0
  64. package/dist/types/core/hitbox/hitbox.d.ts +50 -0
  65. package/dist/types/core/hitbox/index.d.ts +3 -0
  66. package/dist/types/core/hitbox/test.d.ts +44 -0
  67. package/dist/types/core/index.d.ts +6 -0
  68. package/dist/types/core/prediction/prediction.d.ts +35 -58
  69. package/dist/types/core/ray/ray-3d.d.ts +21 -1
  70. package/dist/types/core/raycast/hit-buffer.d.ts +43 -0
  71. package/dist/types/core/raycast/index.d.ts +2 -0
  72. package/dist/types/core/raycast/raycaster.d.ts +54 -0
  73. package/dist/types/core/slot-map/index.d.ts +1 -0
  74. package/dist/types/core/slot-map/slot-map.d.ts +109 -0
  75. package/dist/types/core/state-machine/index.d.ts +1 -0
  76. package/dist/types/core/state-machine/state-machine.d.ts +114 -0
  77. package/dist/types/core/timeline/index.d.ts +1 -0
  78. package/dist/types/core/timeline/timeline.d.ts +34 -0
  79. package/dist/types/game/loop/loop.d.ts +30 -0
  80. package/dist/types/game/loop/ticker-schedule.d.ts +52 -0
  81. package/dist/types/renderer/index.d.ts +1 -0
  82. package/dist/types/renderer/prefab-bucket/concrete.d.ts +16 -6
  83. package/dist/types/renderer/prefab-bucket/index.d.ts +11 -7
  84. package/dist/types/renderer/prefab-bucket/specs.d.ts +10 -0
  85. package/dist/types/renderer/raycast/index.d.ts +1 -0
  86. package/dist/types/renderer/raycast/raycast.d.ts +24 -0
  87. package/dist/types/renderer/types.d.ts +1 -0
  88. package/dist/webgpu/cjs/index.js +1777 -587
  89. package/dist/webgpu/esm/index.js +1769 -573
  90. package/dist/webgpu/types/2d/raycast.d.ts +45 -0
  91. package/dist/webgpu/types/2d/renderer.d.ts +11 -0
  92. package/dist/webgpu/types/2d/sprite-accessor.d.ts +3 -1
  93. package/dist/webgpu/types/3d/hitbox.d.ts +32 -0
  94. package/dist/webgpu/types/3d/lights.d.ts +113 -0
  95. package/dist/webgpu/types/3d/lights.test.d.ts +1 -0
  96. package/dist/webgpu/types/3d/raycast.d.ts +44 -0
  97. package/dist/webgpu/types/3d/renderer.d.ts +50 -1
  98. package/dist/webgpu/types/3d/shader.d.ts +88 -5
  99. package/dist/webgpu/types/core/types.d.ts +55 -0
  100. package/dist/webgpu/types/geometry/geometry-builder.d.ts +1 -4
  101. package/dist/webgpu/types/index.d.ts +1 -0
  102. package/dist/webgpu/types/shaders/utils.d.ts +24 -0
  103. package/package.json +1 -1
  104. package/dist/netcode/types/client/interpolation-buffer.d.ts +0 -37
  105. /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-buffer.ts
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 InterpolationBuffer = class {
822
- constructor(serverToLocal, capacity, delayMs, staleWindowMs) {
823
- this.buffer = [];
824
- this.renderTick = -Infinity;
825
- this.latestReceivedAt = -Infinity;
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.capacity = capacity;
846
+ this.timeline = new Timeline(capacity, staleWindowMs);
829
847
  this.delay = delayMs;
830
- this.staleWindow = staleWindowMs;
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.staleWindow = staleWindowMs;
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 gap = snapshot.receivedAt - this.latestReceivedAt;
840
- if (this.buffer.length > 0 && gap > this.staleWindow) {
841
- this.buffer.length = 0;
842
- this.renderTick = -Infinity;
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.buffer.length = 0;
861
- this.renderTick = -Infinity;
862
- this.latestReceivedAt = -Infinity;
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
- if (this.buffer.length === 0)
881
+ const tl = this.timeline;
882
+ if (tl.length === 0)
867
883
  return;
868
- const newest = this.buffer[this.buffer.length - 1];
869
- const oldest = this.buffer[0];
884
+ const newest = tl.newest().sample;
885
+ const oldest = tl.oldest().sample;
870
886
  let tickRateMs = 0;
871
- if (this.buffer.length >= 2) {
887
+ if (tl.length >= 2) {
872
888
  const tickSpan = newest.serverTick - oldest.serverTick;
873
- const wallSpan = this.latestReceivedAt - oldest.receivedAt;
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
- if (this.renderTick === -Infinity) {
892
- this.renderTick = targetTick;
893
- } else {
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 aIndex = this.buffer.indexOf(a);
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 = this.buffer[aIdx].componentValuesByEntity.get(serverEid)?.get(c);
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 < this.buffer.length - 1) {
939
+ while (vb === void 0 && bIdx < tl.length - 1) {
943
940
  bIdx++;
944
- vb = this.buffer[bIdx].componentValuesByEntity.get(serverEid)?.get(c);
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 aTick = this.buffer[aIdx].serverTick;
959
- const bTick = this.buffer[bIdx].serverTick;
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 InterpolationBuffer(
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
- this.emit("spawn", { entity: localEid, components });
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
- this.emit("assigned", { entity: localEid });
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
- const resetEntities = /* @__PURE__ */ new Set();
1230
- for (const serverEid of decoded.serverEntityIds) {
1231
- const localEid = this.serverToLocal.get(serverEid);
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.predictionHistory.push({
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.predictionHistory.length;
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 { InterpolationBuffer } from './interpolation-buffer';
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 predictionHistory;
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: InterpolationBuffer;
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
+ }