@woosh/meep-engine 3.60.9 → 3.61.1

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 (109) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/package.json +1 -1
  3. package/src/engine/ecs/EntityManager.d.ts +3 -3
  4. package/src/engine/ecs/EntityManager.js +3 -3
  5. package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
  6. package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +4 -0
  7. package/src/engine/graphics/ecs/decal/v2/Decal.d.ts +46 -2
  8. package/src/engine/graphics/ecs/decal/v2/Decal.d.ts.map +1 -1
  9. package/src/engine/graphics/ecs/decal/v2/Decal.js +74 -5
  10. package/src/engine/graphics/ecs/decal/v2/DecalSerializationAdapter.d.ts.map +1 -1
  11. package/src/engine/graphics/ecs/decal/v2/DecalSerializationAdapter.js +13 -1
  12. package/src/engine/graphics/ecs/decal/v2/DecalSerializationUpgrader_1_2.d.ts +9 -0
  13. package/src/engine/graphics/ecs/decal/v2/DecalSerializationUpgrader_1_2.d.ts.map +1 -0
  14. package/src/engine/graphics/ecs/decal/v2/DecalSerializationUpgrader_1_2.js +37 -0
  15. package/src/engine/graphics3/DecalSystem.d.ts.map +1 -1
  16. package/src/engine/graphics3/DecalSystem.js +462 -447
  17. package/src/engine/graphics3/decal/DECAL_GPU_RECORD.d.ts +14 -0
  18. package/src/engine/graphics3/decal/DECAL_GPU_RECORD.d.ts.map +1 -1
  19. package/src/engine/graphics3/decal/DECAL_GPU_RECORD.js +53 -13
  20. package/src/engine/graphics3/decal/chunk_decal_record.d.ts +6 -1
  21. package/src/engine/graphics3/decal/chunk_decal_record.d.ts.map +1 -1
  22. package/src/engine/graphics3/decal/chunk_decal_record.js +28 -9
  23. package/src/engine/graphics3/decal/decal_gpu_records.d.ts +2 -1
  24. package/src/engine/graphics3/decal/decal_gpu_records.d.ts.map +1 -1
  25. package/src/engine/graphics3/decal/decal_gpu_records.js +26 -5
  26. package/src/engine/graphics3/decal/decal_material_samplers.d.ts +6 -0
  27. package/src/engine/graphics3/decal/decal_material_samplers.d.ts.map +1 -1
  28. package/src/engine/graphics3/decal/decal_material_samplers.js +13 -0
  29. package/src/engine/graphics3/decal/shader_cluster_assign_decals.js +1 -1
  30. package/src/engine/graphics3/decal/shader_cull_decals_frustum.js +1 -1
  31. package/src/engine/graphics3/decal/shader_decal_composite.d.ts +5 -0
  32. package/src/engine/graphics3/decal/shader_decal_composite.d.ts.map +1 -1
  33. package/src/engine/graphics3/decal/shader_decal_composite.js +69 -28
  34. package/src/engine/network/NetworkSession.d.ts +3 -0
  35. package/src/engine/network/NetworkSession.d.ts.map +1 -1
  36. package/src/engine/network/NetworkSession.js +68 -10
  37. package/src/engine/network/README.md +6 -3
  38. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +51 -5
  39. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
  40. package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +308 -34
  41. package/src/engine/network/replication/Replicator.d.ts +8 -2
  42. package/src/engine/network/replication/Replicator.d.ts.map +1 -1
  43. package/src/engine/network/replication/Replicator.js +13 -4
  44. package/src/engine/network/sim/ActionLog.d.ts +16 -2
  45. package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
  46. package/src/engine/network/sim/ActionLog.js +33 -2
  47. package/src/engine/network/sim/RewindEngine.d.ts +9 -0
  48. package/src/engine/network/sim/RewindEngine.d.ts.map +1 -1
  49. package/src/engine/network/sim/RewindEngine.js +15 -1
  50. package/src/engine/network/state/FrameBufferRing.d.ts +12 -5
  51. package/src/engine/network/state/FrameBufferRing.d.ts.map +1 -1
  52. package/src/engine/network/state/FrameBufferRing.js +32 -6
  53. package/src/engine/network/time/TimeDilation.d.ts +30 -14
  54. package/src/engine/network/time/TimeDilation.d.ts.map +1 -1
  55. package/src/engine/network/time/TimeDilation.js +47 -17
  56. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  57. package/src/generation/foliage/skeleton/ShootWoodHash.d.ts +3 -2
  58. package/src/generation/foliage/skeleton/ShootWoodHash.d.ts.map +1 -1
  59. package/src/generation/foliage/skeleton/ShootWoodHash.js +25 -2
  60. package/src/shade/renderer/buffer/graph_read_buffer.d.ts +3 -1
  61. package/src/shade/renderer/buffer/graph_read_buffer.d.ts.map +1 -1
  62. package/src/shade/renderer/buffer/graph_read_buffer.js +9 -6
  63. package/src/shade/renderer/buffer/util/shader_buffer_copy_storage_to_storage.d.ts +5 -0
  64. package/src/shade/renderer/buffer/util/shader_buffer_copy_storage_to_storage.d.ts.map +1 -1
  65. package/src/shade/renderer/buffer/util/shader_buffer_copy_storage_to_storage.js +48 -43
  66. package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
  67. package/src/shade/renderer/material/GPUMaterialManager.js +117 -35
  68. package/src/shade/renderer/material/standard/fragment_gbuffer.d.ts +2 -2
  69. package/src/shade/renderer/material/standard/fragment_gbuffer.d.ts.map +1 -1
  70. package/src/shade/renderer/material/standard/fragment_gbuffer.js +10 -3
  71. package/src/shade/renderer/material/standard/material_gbuffer_vertex.d.ts +15 -3
  72. package/src/shade/renderer/material/standard/material_gbuffer_vertex.d.ts.map +1 -1
  73. package/src/shade/renderer/material/standard/material_gbuffer_vertex.js +25 -4
  74. package/src/shade/renderer/particles/GPUParticleSystem.d.ts +23 -5
  75. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  76. package/src/shade/renderer/particles/GPUParticleSystem.js +294 -20
  77. package/src/shade/renderer/particles/resize/shader_particle_shrink_alive_list.d.ts +21 -0
  78. package/src/shade/renderer/particles/resize/shader_particle_shrink_alive_list.d.ts.map +1 -0
  79. package/src/shade/renderer/particles/resize/shader_particle_shrink_alive_list.js +83 -0
  80. package/src/shade/renderer/particles/resize/shader_particle_shrink_free_list.d.ts +21 -0
  81. package/src/shade/renderer/particles/resize/shader_particle_shrink_free_list.d.ts.map +1 -0
  82. package/src/shade/renderer/particles/resize/shader_particle_shrink_free_list.js +64 -0
  83. package/src/shade/renderer/restir/di/chunk_restir_resolve.d.ts.map +1 -1
  84. package/src/shade/renderer/restir/di/chunk_restir_resolve.js +91 -94
  85. package/src/shade/renderer/restir/reservoir/chunk_rtxdi_stream_sample.js +1 -1
  86. package/src/shade/renderer/shader/chunk/color/rgbe9995/chunk_rgbe9995_decode.d.ts +14 -0
  87. package/src/shade/renderer/shader/chunk/color/rgbe9995/chunk_rgbe9995_decode.d.ts.map +1 -1
  88. package/src/shade/renderer/shader/chunk/color/rgbe9995/chunk_rgbe9995_decode.js +36 -21
  89. package/src/shade/renderer/shader/chunk/color/rgbe9995/chunk_rgbe9995_sample_gather.d.ts +22 -0
  90. package/src/shade/renderer/shader/chunk/color/rgbe9995/chunk_rgbe9995_sample_gather.d.ts.map +1 -0
  91. package/src/shade/renderer/shader/chunk/color/rgbe9995/chunk_rgbe9995_sample_gather.js +64 -0
  92. package/src/shade/renderer/shader/chunk/geometry/normal/chunk_decode_vertex_normal.d.ts.map +1 -1
  93. package/src/shade/renderer/shader/chunk/geometry/normal/chunk_decode_vertex_normal.js +13 -18
  94. package/src/shade/renderer/shader/chunk/geometry/normal/chunk_encode_vertex_normal.d.ts.map +1 -1
  95. package/src/shade/renderer/shader/chunk/geometry/normal/chunk_encode_vertex_normal.js +28 -30
  96. package/src/shade/renderer/shader/chunk/geometry/vec2/chunk_unit_u32_to_vec2.d.ts.map +1 -1
  97. package/src/shade/renderer/shader/chunk/geometry/vec2/chunk_unit_u32_to_vec2.js +15 -16
  98. package/src/shade/renderer/shader/chunk/geometry/vec2/chunk_unit_vec2_to_u32.d.ts +0 -1
  99. package/src/shade/renderer/shader/chunk/geometry/vec2/chunk_unit_vec2_to_u32.d.ts.map +1 -1
  100. package/src/shade/renderer/shader/chunk/geometry/vec2/chunk_unit_vec2_to_u32.js +15 -18
  101. package/src/shade/renderer/shader/chunk/subgroup/chunk_subgroup_bitset_or.d.ts +32 -0
  102. package/src/shade/renderer/shader/chunk/subgroup/chunk_subgroup_bitset_or.d.ts.map +1 -0
  103. package/src/shade/renderer/shader/chunk/subgroup/chunk_subgroup_bitset_or.js +73 -0
  104. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +8 -1
  105. package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
  106. package/src/shade/wgsl/emulator/WGSLLanguageCore.js +7 -3
  107. package/src/shade/renderer/global_illumination/brick4/cpu/brick4_sh3_color_encode.d.ts +0 -2
  108. package/src/shade/renderer/global_illumination/brick4/cpu/brick4_sh3_color_encode.d.ts.map +0 -1
  109. package/src/shade/renderer/global_illumination/brick4/cpu/brick4_sh3_color_encode.js +0 -6
@@ -1,5 +1,6 @@
1
1
  import { assert } from "../../../core/assert.js";
2
2
  import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
3
+ import { PairUint32Map } from "../../../core/collection/PairUint32Map.js";
3
4
  import { Uint32Map } from "../../../core/collection/Uint32Map.js";
4
5
  import Signal from "../../../core/events/signal/Signal.js";
5
6
  import { AlwaysRelevantScope } from "../replication/ScopeFilter.js";
@@ -74,6 +75,27 @@ const HARVEST_STRIDE = 4;
74
75
  * third. Here the authored half is `onReplay`'s job and the arrived half is
75
76
  * this one's.
76
77
  *
78
+ * - Records that arrive from a peer for a frame this client has already
79
+ * predicted past belong in that frame, under the prediction since, and
80
+ * before the frame's corrections, which already hold what they did. The
81
+ * frames after it were predicted without them and hold the prior state they
82
+ * captured then, so a rewind across those frames would put back what the
83
+ * records changed — a rewind about any other entity included. Where the
84
+ * records write something a frame after theirs, or a correction of their
85
+ * own frame, holds prior state of, the client takes them back off the
86
+ * world, rewinds to the end of their frame and back past its corrections,
87
+ * applies them there, the corrections after them, and replays what came
88
+ * after, as a reconciliation replays. Records about nothing written since
89
+ * stay where they landed: for everything they write, the present still holds
90
+ * what the end of their frame held. The test is on what records write, not
91
+ * what they read: one that only reads state written since — copying a
92
+ * predicted value into an entity nothing else writes — keeps what it read at
93
+ * the present until a replay crosses its frame. The replay is a
94
+ * reconciliation's, so a local record {@link onReplay} does not produce is
95
+ * lost from the frames it rebuilds. Records for a frame the action log has
96
+ * moved past have no frame to go in: they are applied where they land, and
97
+ * not logged.
98
+ *
77
99
  * - A no-op short-circuit is built in: before doing the rewind+replay, the
78
100
  * orchestrator gives the user a chance to compute what the replay WOULD
79
101
  * produce via {@link onComputeExpected}; if that matches the current
@@ -138,11 +160,25 @@ export class ServerAuthoritativeClient {
138
160
  */
139
161
  #correction_payload = new BinaryBuffer();
140
162
 
163
+ /**
164
+ * The `(entity, component type)` pairs that state written after an arrived frame's records holds
165
+ * prior state of — what those records are checked against. Filled by
166
+ * {@link #overwrites_later_state}.
167
+ * @type {PairUint32Map}
168
+ */
169
+ #written_later = new PairUint32Map();
170
+
171
+ /** @type {ActionRecordCursor} */
172
+ #arrival_cursor = new ActionRecordCursor();
173
+
141
174
  // Counters
142
175
  /** @type {number} */
143
176
  reconcile_count = 0;
144
177
 
145
- /** @type {number} */
178
+ /**
179
+ * Frames replayed, by reconciliations and by records put back in their frame.
180
+ * @type {number}
181
+ */
146
182
  replay_frame_count = 0;
147
183
 
148
184
  /**
@@ -161,6 +197,14 @@ export class ServerAuthoritativeClient {
161
197
  */
162
198
  reconcile_abandoned_count = 0;
163
199
 
200
+ /**
201
+ * Frames of records from a peer that arrived after this client had predicted past them, wrote
202
+ * something written later — in a frame after theirs, or by a correction of their own — and so
203
+ * were put back in their frame by a rewind and a replay. See {@link #land_arrived}.
204
+ * @type {number}
205
+ */
206
+ rebase_count = 0;
207
+
164
208
  /**
165
209
  * @param {{
166
210
  * world: EntityComponentDataset,
@@ -176,6 +220,7 @@ export class ServerAuthoritativeClient {
176
220
  * time_dilation?: TimeDilation,
177
221
  * max_packets_per_tick?: number,
178
222
  * auto_reconcile?: boolean,
223
+ * rebase_on_arrival?: boolean,
179
224
  * clock: function(): number,
180
225
  * tick_period_ms: number,
181
226
  * send_rate_bytes_per_second?: number,
@@ -189,8 +234,8 @@ export class ServerAuthoritativeClient {
189
234
  * `time_dilation`: optional pre-configured {@link TimeDilation}.
190
235
  * On TIME_DILATION feedback the client recomputes
191
236
  * {@link dilation_factor} for the caller to scale its tick cadence.
192
- * Defaults to a stock instance; `dilation_factor` is 1.0 until the
193
- * first feedback arrives.
237
+ * Defaults to one built for round trips up to `frame_capacity` frames;
238
+ * `dilation_factor` is 1.0 until the first feedback arrives.
194
239
  *
195
240
  * `max_packets_per_tick`: forwarded to {@link NetworkPeer}; how many
196
241
  * action-stream packets a tick may send the server.
@@ -200,6 +245,13 @@ export class ServerAuthoritativeClient {
200
245
  * own reconciliation — `NetworkSession` corrects by comparison instead —
201
246
  * turns it off and listens to `peer.onAuthState` itself; {@link reconcile}
202
247
  * remains available to it.
248
+ *
249
+ * `rebase_on_arrival` (default true): put the records a peer sends for a
250
+ * frame this client has already predicted past in that frame, under what it
251
+ * predicted since, by rewind and replay — see the class notes. It keeps the
252
+ * log true for the rewinds this client reconciles by. A caller that never
253
+ * rewinds — `NetworkSession` correcting by comparison — turns it off, and
254
+ * the records are applied to the world as it is when they arrive.
203
255
  */
204
256
  constructor({
205
257
  world,
@@ -212,9 +264,10 @@ export class ServerAuthoritativeClient {
212
264
  input_ring_capacity = 256,
213
265
  input_byte_size = 4,
214
266
  reconcile_epsilon = 1e-4,
215
- time_dilation = new TimeDilation(),
267
+ time_dilation = new TimeDilation({ max_round_trip_frames: frame_capacity }),
216
268
  max_packets_per_tick,
217
269
  auto_reconcile = true,
270
+ rebase_on_arrival = true,
218
271
  clock,
219
272
  tick_period_ms,
220
273
  send_rate_bytes_per_second,
@@ -294,7 +347,10 @@ export class ServerAuthoritativeClient {
294
347
  /**
295
348
  * Tick-period multiplier from the latest TIME_DILATION feedback.
296
349
  * `< 1.0` ⇒ caller should run faster, `> 1.0` ⇒ slower. 1.0 until
297
- * the first feedback arrives.
350
+ * the first feedback arrives. It follows every report, and holds while
351
+ * none come: a caller that seeks its frames back, or loses the host,
352
+ * judges whether the latest report still holds before running at it, as
353
+ * `NetworkSession` does.
298
354
  *
299
355
  * scheduled_tick_period_ms = TICK_PERIOD_MS * client.dilation_factor;
300
356
  *
@@ -401,6 +457,9 @@ export class ServerAuthoritativeClient {
401
457
  * longer holds the frames its rewind needed. Args:
402
458
  * `(server_frame, network_id)`.
403
459
  *
460
+ * Nothing of it is done: the world stays at the present, and the
461
+ * server's state is not applied.
462
+ *
404
463
  * This is a real loss of replicated state and not a hiccup: state a peer
405
464
  * does not own reaches it on the action stream, and an owner that
406
465
  * publishes on change never sends it again — so whatever that window
@@ -426,6 +485,12 @@ export class ServerAuthoritativeClient {
426
485
  this.#handle_auth_state(server_frame, network_id, buffer);
427
486
  });
428
487
  }
488
+
489
+ if (rebase_on_arrival) {
490
+ this.peer.replicator.onFrameApplied.add((_peer_id, frame, records_start) => {
491
+ this.#land_arrived(frame, records_start);
492
+ });
493
+ }
429
494
  }
430
495
 
431
496
  /**
@@ -617,21 +682,26 @@ export class ServerAuthoritativeClient {
617
682
  to the same frame undid the first one's correction again.
618
683
  */
619
684
  if (this.#current_frame > server_frame) {
620
- try {
621
- this.rewind_engine.rewind_to(this.#current_frame, server_frame);
622
- } catch (e) {
623
- /*
624
- The action_log rolled past `server_frame + 1`, so the window this
625
- reconciliation needs no longer exists and skipping it is the
626
- only option left. Counted and announced rather than swallowed:
627
- what the window carried for every entity other than this one is
628
- gone for good, and an application that cannot see that happen
629
- cannot ask for the back-fill that would repair it.
630
- */
685
+ /*
686
+ The rewind undoes every frame after `server_frame`. Where the action
687
+ log no longer holds one of them, the window this reconciliation
688
+ needs is gone and skipping it is the only option left — found
689
+ before anything is undone, so the world stays at the present this
690
+ client predicts on from. Counted and announced rather than
691
+ swallowed: what the window carried for every entity other than this
692
+ one is gone for good, and an application that cannot see that
693
+ happen cannot ask for the back-fill that would repair it.
694
+ */
695
+ const log = this.action_log;
696
+ for (let f = server_frame + 1; f <= this.#current_frame; f++) {
697
+ if (log.has_frame(f)) continue;
698
+
631
699
  this.reconcile_abandoned_count++;
632
700
  this.onReconcileAbandoned.send2(server_frame, network_id);
633
701
  return;
634
702
  }
703
+
704
+ this.rewind_engine.rewind_to(this.#current_frame, server_frame);
635
705
  }
636
706
 
637
707
  // Apply the server's authoritative state bytes, logged as a correction
@@ -639,12 +709,34 @@ export class ServerAuthoritativeClient {
639
709
  // deserialize, and what it read is the correction.
640
710
  this.#apply_correction(server_frame, network_id, local, buffer);
641
711
 
642
- // Replay forward from (server_frame + 1) through current_frame.
712
+ const replay_count = this.#replay(server_frame + 1);
713
+
714
+ this.reconcile_count++;
715
+ this.replay_frame_count += replay_count;
716
+ this.onReconcileComplete.send2(server_frame, replay_count);
717
+ }
718
+
719
+ /**
720
+ * Replay every frame from `from` through the current frame over the world as it stands, the end
721
+ * of frame `from - 1`: each frame's local input through {@link onReplay}, then the records that
722
+ * arrived from a peer, then corrections, last in their frame.
723
+ *
724
+ * Every frame is replayed whatever one of them throws: a frame that throws keeps what it applied
725
+ * before the throw, and its corrections; the first error is thrown once the world is back at the
726
+ * current frame. Stopped there instead, the world would be left in the past and the frames after
727
+ * it holding their old records, and the client would predict on from there.
728
+ *
729
+ * @param {number} from
730
+ * @returns {number} how many frames were replayed
731
+ * @private
732
+ */
733
+ #replay(from) {
643
734
  let replay_count = 0;
644
- for (let f = server_frame + 1; f <= this.#current_frame; f++) {
735
+ let failure = null;
736
+ for (let f = from; f <= this.#current_frame; f++) {
645
737
  // Harvested before `begin_frame`, which rewinds the frame's buffer
646
738
  // over the bytes being read.
647
- const harvested = this.#harvest(f);
739
+ const harvested = this.#harvest(f, 0);
648
740
 
649
741
  this.action_log.begin_frame(f);
650
742
  try {
@@ -663,20 +755,194 @@ export class ServerAuthoritativeClient {
663
755
  the one that should win where both touch a component.
664
756
  */
665
757
  if (harvested > 0) {
666
- this.#execute_harvested();
667
-
668
- // Last of all: a correction is the server's word on the END of its frame.
669
- this.#reapply_corrections(f);
758
+ this.replayed_arrived_count += this.#apply_harvested(f);
670
759
  }
760
+ } catch (e) {
761
+ if (failure === null) failure = e;
671
762
  } finally {
672
763
  this.action_log.end_frame();
673
764
  }
674
765
  replay_count++;
675
766
  }
676
767
 
677
- this.reconcile_count++;
678
- this.replay_frame_count += replay_count;
679
- this.onReconcileComplete.send2(server_frame, replay_count);
768
+ if (failure !== null) throw failure;
769
+
770
+ return replay_count;
771
+ }
772
+
773
+ /**
774
+ * Put the records a peer's frame has just had appended, from byte `arrived_start` on, where they
775
+ * belong on this client's timeline.
776
+ *
777
+ * The {@link Replicator} appends a peer's records to the frame the peer sent them for, after
778
+ * whatever the frame holds, and applies them to the world as it is — on a client, usually some
779
+ * frames past that one. The records' prior state is then the present's, every frame after
780
+ * `frame` holds prior state captured before they arrived, and a correction already in `frame`
781
+ * stands before them where a replay would write it after them. Where none of that holds prior
782
+ * state of anything the records wrote — nothing written later wrote it — the present held what
783
+ * the end of `frame` held there, and they stay. Otherwise they are taken back off the world,
784
+ * which puts it back as the frames after `frame` left it; those frames are rewound to the end of
785
+ * `frame` and its corrections undone; the records are applied there, the corrections after them,
786
+ * and what this client predicted since is replayed over them.
787
+ *
788
+ * A record that throws where it belongs is left out of the frame, as the executor leaves out any
789
+ * action that throws. The frame keeps its corrections, the world is brought back to the present
790
+ * all the same, and the error is thrown after.
791
+ *
792
+ * Records for a frame the log has moved past, which it applied without logging, and records for
793
+ * a frame the log no longer holds the frames after, are left where they landed: a rewind could
794
+ * not cross them either.
795
+ *
796
+ * @param {number} frame
797
+ * @param {number} arrived_start where in the frame the records just appended begin
798
+ * @private
799
+ */
800
+ #land_arrived(frame, arrived_start) {
801
+ const present = this.#current_frame;
802
+ if (frame >= present) return;
803
+
804
+ const log = this.action_log;
805
+ for (let f = frame; f <= present; f++) {
806
+ if (!log.has_frame(f)) return;
807
+ }
808
+ if (arrived_start >= log.write_end_for(frame)) return;
809
+
810
+ // Before the records just appended: this client's own records, then what a replay puts
811
+ // back — what arrived before them, if their frame was applied before, and the corrections.
812
+ const cursor = this.#arrival_cursor;
813
+ let tail_start = arrived_start;
814
+ cursor.open(log, frame);
815
+ while (cursor.next() && cursor.record_start < arrived_start) {
816
+ const sender_id = cursor.sender_id;
817
+ if (sender_id === SENDER_CORRECTED || this.peer.is_connected(sender_id)) {
818
+ tail_start = cursor.record_start;
819
+ break;
820
+ }
821
+ }
822
+
823
+ if (!this.#overwrites_later_state(frame, tail_start, arrived_start)) return;
824
+
825
+ // Copies of the frame's tail — its corrections, then the records just appended — taken
826
+ // before any of it is written over.
827
+ this.#harvest(frame, tail_start);
828
+
829
+ // The records just appended, off the world and off the frame: the world is the present as
830
+ // the frames after `frame` left it, and the frame is what it was before they came.
831
+ this.rewind_engine.undo_frame_from(frame, arrived_start);
832
+ log.resume_frame_at(frame, arrived_start);
833
+ log.end_frame();
834
+
835
+ // To the end of the frame, and back past its corrections.
836
+ this.rewind_engine.rewind_to(present, frame);
837
+ this.rewind_engine.undo_frame_from(frame, tail_start);
838
+
839
+ // The tail again, in the order a replay writes a frame: what arrived, then the corrections.
840
+ // Then forward to the present whatever became of the records: a world left in the past would
841
+ // be predicted on from there.
842
+ let failure = null;
843
+ log.resume_frame_at(frame, tail_start);
844
+ try {
845
+ this.#apply_harvested(frame);
846
+ } catch (e) {
847
+ failure = e;
848
+ } finally {
849
+ log.end_frame();
850
+ }
851
+
852
+ this.replay_frame_count += this.#replay(frame + 1);
853
+ this.rebase_count++;
854
+
855
+ if (failure !== null) throw failure;
856
+ }
857
+
858
+ /**
859
+ * Whether the records from byte `arrived_start` on in `frame` write anything that state written
860
+ * after them holds prior state of: a correction in the frame from `tail_start` up to them, which
861
+ * a replay writes after them, and any record in a frame after `frame`.
862
+ *
863
+ * @param {number} frame
864
+ * @param {number} tail_start
865
+ * @param {number} arrived_start
866
+ * @returns {boolean}
867
+ * @private
868
+ */
869
+ #overwrites_later_state(frame, tail_start, arrived_start) {
870
+ const later = this.#written_later;
871
+ later.clear();
872
+
873
+ const cursor = this.#arrival_cursor;
874
+
875
+ cursor.open(this.action_log, frame);
876
+ while (cursor.next() && cursor.record_start < arrived_start) {
877
+ if (cursor.record_start >= tail_start && cursor.sender_id === SENDER_CORRECTED) {
878
+ this.#collect_prior_entries(cursor, later);
879
+ }
880
+ }
881
+
882
+ for (let f = frame + 1; f <= this.#current_frame; f++) {
883
+ cursor.open(this.action_log, f);
884
+ while (cursor.next()) {
885
+ this.#collect_prior_entries(cursor, later);
886
+ }
887
+ }
888
+
889
+ if (later.size === 0) return false;
890
+
891
+ cursor.open(this.action_log, frame);
892
+ while (cursor.next()) {
893
+ if (cursor.record_start < arrived_start) continue;
894
+
895
+ const buffer = cursor.buffer;
896
+ buffer.position = cursor.prior_start;
897
+ for (let i = 0; i < cursor.prior_count; i++) {
898
+ const entity_id = buffer.readUintVar();
899
+ const component_type_id = buffer.readUint8();
900
+ buffer.position += buffer.readUint32();
901
+
902
+ if (later.has(entity_id, component_type_id)) return true;
903
+ }
904
+ }
905
+
906
+ return false;
907
+ }
908
+
909
+ /**
910
+ * Add the `(entity, component type)` of every prior-state entry of the record under `cursor`
911
+ * that captured something to `into` — an entry that captured nothing puts nothing back.
912
+ *
913
+ * @param {ActionRecordCursor} cursor
914
+ * @param {PairUint32Map} into
915
+ * @private
916
+ */
917
+ #collect_prior_entries(cursor, into) {
918
+ const buffer = cursor.buffer;
919
+ buffer.position = cursor.prior_start;
920
+ for (let i = 0; i < cursor.prior_count; i++) {
921
+ const entity_id = buffer.readUintVar();
922
+ const component_type_id = buffer.readUint8();
923
+ const prior_length = buffer.readUint32();
924
+ buffer.position += prior_length;
925
+
926
+ if (prior_length > 0) into.set(entity_id, component_type_id, 1);
927
+ }
928
+ }
929
+
930
+ /**
931
+ * Apply what {@link #harvest} took from `frame` into it, open: the records that arrived from a
932
+ * peer, then the corrections, last — those whatever the records do, since the frame has been
933
+ * opened over them and they live nowhere else.
934
+ *
935
+ * @param {number} frame
936
+ * @returns {number} how many arrived records were applied
937
+ * @private
938
+ */
939
+ #apply_harvested(frame) {
940
+ try {
941
+ return this.#execute_harvested();
942
+ } finally {
943
+ // Last of all: a correction is the server's word on the END of its frame.
944
+ this.#reapply_corrections(frame);
945
+ }
680
946
  }
681
947
 
682
948
  /**
@@ -769,12 +1035,13 @@ export class ServerAuthoritativeClient {
769
1035
  }
770
1036
 
771
1037
  /**
772
- * Copy the records of `frame` that a replay puts back out of the action
773
- * log, ready for {@link #execute_harvested} and {@link #reapply_corrections}.
1038
+ * Copy the records of `frame` from byte `from` on that a replay puts back out
1039
+ * of the action log, ready for {@link #execute_harvested} and
1040
+ * {@link #reapply_corrections}.
774
1041
  *
775
- * MUST be called before `action_log.begin_frame(frame)`. Opening a frame
776
- * rewinds its buffer to zero without clearing it, and the re-execution
777
- * writes over exactly the bytes this reads, so the payloads are copied into
1042
+ * MUST be called before the frame is opened over them. Opening a frame
1043
+ * rewinds its buffer without clearing it, and the re-execution writes over
1044
+ * exactly the bytes this reads, so the payloads are copied into
778
1045
  * {@link #harvest_payloads} rather than read in place.
779
1046
  *
780
1047
  * Provenance decides what a replay does with a record, and this client keeps
@@ -797,10 +1064,11 @@ export class ServerAuthoritativeClient {
797
1064
  * itself, so its own records can never match.
798
1065
  *
799
1066
  * @param {number} frame
1067
+ * @param {number} from offset of the first record looked at, a record boundary in the frame
800
1068
  * @returns {number} how many records were harvested
801
1069
  * @private
802
1070
  */
803
- #harvest(frame) {
1071
+ #harvest(frame, from) {
804
1072
  this.#harvest_count = 0;
805
1073
  this.#harvest_payloads.position = 0;
806
1074
 
@@ -811,6 +1079,8 @@ export class ServerAuthoritativeClient {
811
1079
  const bytes = cursor.buffer.raw_bytes;
812
1080
 
813
1081
  while (cursor.next()) {
1082
+ if (cursor.record_start < from) continue;
1083
+
814
1084
  const sender_id = cursor.sender_id;
815
1085
  if (sender_id !== SENDER_CORRECTED && !this.peer.is_connected(sender_id)) continue;
816
1086
 
@@ -841,10 +1111,12 @@ export class ServerAuthoritativeClient {
841
1111
  * A record whose action class is no longer registered is skipped — the same
842
1112
  * thing `Replicator.#apply_groups` does on the way in.
843
1113
  *
1114
+ * @returns {number} how many were applied
844
1115
  * @private
845
1116
  */
846
1117
  #execute_harvested() {
847
1118
  const payloads = this.#harvest_payloads;
1119
+ let applied = 0;
848
1120
 
849
1121
  for (let record = 0; record < this.#harvest_count; record++) {
850
1122
  const i = record * HARVEST_STRIDE;
@@ -862,14 +1134,16 @@ export class ServerAuthoritativeClient {
862
1134
  action.deserialize(payloads);
863
1135
  // `execute` returns false when an authorization policy rejects
864
1136
  // the action, in which case nothing was applied or logged — so
865
- // the counter follows the return value rather than the attempt.
1137
+ // the count follows the return value rather than the attempt.
866
1138
  if (this.executor.execute(action, sender_id)) {
867
- this.replayed_arrived_count++;
1139
+ applied++;
868
1140
  }
869
1141
  } finally {
870
1142
  this.action_registry.release(action);
871
1143
  }
872
1144
  }
1145
+
1146
+ return applied;
873
1147
  }
874
1148
 
875
1149
  /**
@@ -90,7 +90,11 @@ export class Replicator {
90
90
  };
91
91
  /**
92
92
  * Fired after each per-frame action group is fully applied via
93
- * {@link unpack_from_peer}. Args: `(peer_id, frame_number)`.
93
+ * {@link unpack_from_peer}. Args: `(peer_id, frame_number, records_start)`:
94
+ * the records just applied are logged in the action log's frame from
95
+ * byte `records_start` to its end, after whatever it held before. A frame
96
+ * the action log has moved past is applied without being logged, and the
97
+ * log does not hold it.
94
98
  *
95
99
  * Subscribe to drive interpolation buffers, replay logs, diagnostics,
96
100
  * or anything else that needs to know "frame N from peer P has just
@@ -177,7 +181,9 @@ export class Replicator {
177
181
  * Parse and apply actions from `in_buffer` into the local world via the executor.
178
182
  * Each frame in the packet opens a new entry in the local action log under the
179
183
  * sender's frame number, ensuring the rewind/replay machinery sees consistent
180
- * frame indexing across peers.
184
+ * frame indexing across peers. A frame the log has moved past — older than the
185
+ * frame its slot holds — is applied to the world as it stands and not logged:
186
+ * no rewind reaches it, and logging it would erase that later frame.
181
187
  *
182
188
  * `slice` is the sender's description of the packet, from the
183
189
  * {@link NetworkPeer} slice header: the frame range it covers and whether
@@ -1 +1 @@
1
- {"version":3,"file":"Replicator.d.ts","sourceRoot":"","sources":["../../../../../src/engine/network/replication/Replicator.js"],"names":[],"mappings":"AAsBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH;IAmLI;;;;;;;;OAQG;IACH,iFARW;QACV,UAAc,YAAY;QAC1B,eAAmB,oBAAoB;QACvC,QAAY,oBAAoB;QAChC,UAAc,uBAAuB;QACrC,YAAgB,EAAE;YAAE,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAA;SAAE,CAAC;KACpF,EA6BH;IA9ID;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,wBAHW,MAAM,GACJ;QAAC,iBAAiB,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAC,CAc1F;IA2BD;;;;;;;;;;;;;OAaG;IACH,gBAFU,MAAM,CAEc;IAE9B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,mBAFU,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,CAExJ;IAmBrB;;OAEG;IACH,sBAA4B;IAE5B;;OAEG;IACH,mCAAsC;IAEtC;;OAEG;IACH,4BAAwB;IAExB;;OAEG;IACH,iCAA4B;IAE5B;;;;OAIG;IACH,cAFU;QAAE,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAE9C;IAGpC;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,uBAVW,MAAM,eACN,MAAM,aACN,MAAM,cACN,YAAY,cACZ,MAAM,GACJ,MAAM,CA0FlB;IAED;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,0BALW,MAAM,aACN,YAAY,iBACZ,MAAM,UACN;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAC,GAAC,IAAI,QA2EtE;IA+RD;;;;;;;OAOG;IACH,qBAFW,MAAM,QAKhB;;CA8CJ;mBA/zBkB,uCAAuC;6BAD7B,sCAAsC"}
1
+ {"version":3,"file":"Replicator.d.ts","sourceRoot":"","sources":["../../../../../src/engine/network/replication/Replicator.js"],"names":[],"mappings":"AAsBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH;IAuLI;;;;;;;;OAQG;IACH,iFARW;QACV,UAAc,YAAY;QAC1B,eAAmB,oBAAoB;QACvC,QAAY,oBAAoB;QAChC,UAAc,uBAAuB;QACrC,YAAgB,EAAE;YAAE,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAA;SAAE,CAAC;KACpF,EA6BH;IAlJD;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,wBAHW,MAAM,GACJ;QAAC,iBAAiB,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAC,CAc1F;IA2BD;;;;;;;;;;;;;;;;;OAiBG;IACH,gBAFU,MAAM,CAEc;IAE9B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,mBAFU,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,CAExJ;IAmBrB;;OAEG;IACH,sBAA4B;IAE5B;;OAEG;IACH,mCAAsC;IAEtC;;OAEG;IACH,4BAAwB;IAExB;;OAEG;IACH,iCAA4B;IAE5B;;;;OAIG;IACH,cAFU;QAAE,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAE9C;IAGpC;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,uBAVW,MAAM,eACN,MAAM,aACN,MAAM,cACN,YAAY,cACZ,MAAM,GACJ,MAAM,CA0FlB;IAED;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,0BALW,MAAM,aACN,YAAY,iBACZ,MAAM,UACN;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAC,GAAC,IAAI,QA2EtE;IAkSD;;;;;;;OAOG;IACH,qBAFW,MAAM,QAKhB;;CA8CJ;mBAx0BkB,uCAAuC;6BAD7B,sCAAsC"}
@@ -200,7 +200,11 @@ export class Replicator {
200
200
 
201
201
  /**
202
202
  * Fired after each per-frame action group is fully applied via
203
- * {@link unpack_from_peer}. Args: `(peer_id, frame_number)`.
203
+ * {@link unpack_from_peer}. Args: `(peer_id, frame_number, records_start)`:
204
+ * the records just applied are logged in the action log's frame from
205
+ * byte `records_start` to its end, after whatever it held before. A frame
206
+ * the action log has moved past is applied without being logged, and the
207
+ * log does not hold it.
204
208
  *
205
209
  * Subscribe to drive interpolation buffers, replay logs, diagnostics,
206
210
  * or anything else that needs to know "frame N from peer P has just
@@ -396,7 +400,9 @@ export class Replicator {
396
400
  * Parse and apply actions from `in_buffer` into the local world via the executor.
397
401
  * Each frame in the packet opens a new entry in the local action log under the
398
402
  * sender's frame number, ensuring the rewind/replay machinery sees consistent
399
- * frame indexing across peers.
403
+ * frame indexing across peers. A frame the log has moved past — older than the
404
+ * frame its slot holds — is applied to the world as it stands and not logged:
405
+ * no rewind reaches it, and logging it would erase that later frame.
400
406
  *
401
407
  * `slice` is the sender's description of the packet, from the
402
408
  * {@link NetworkPeer} slice header: the frame range it covers and whether
@@ -576,7 +582,10 @@ export class Replicator {
576
582
  }
577
583
 
578
584
  // After what the frame already holds: on a client this frame is usually one it has
579
- // already predicted, and its own records are what a rewind undoes.
585
+ // already predicted, and its own records are what a rewind undoes. A frame the log has
586
+ // moved past is not logged at all (ActionLog.resume_frame): the frame in its slot is
587
+ // one of this peer's own.
588
+ const records_start = this.action_log.has_frame(frame_number) ? this.action_log.write_end_for(frame_number) : 0;
580
589
  this.action_log.resume_frame(frame_number);
581
590
  try {
582
591
  for (let i = 0; i < action_count; i++) {
@@ -651,7 +660,7 @@ export class Replicator {
651
660
 
652
661
  // Fire AFTER end_frame so handlers see consistent state and can
653
662
  // safely query the action log for the just-applied frame.
654
- this.onFrameApplied.send2(peer_id, frame_number);
663
+ this.onFrameApplied.send3(peer_id, frame_number, records_start);
655
664
  }
656
665
 
657
666
  return last_frame;
@@ -76,17 +76,31 @@ export class ActionLog {
76
76
  begin_frame(frame: number): void;
77
77
  /**
78
78
  * Open a frame for writing after the records it already holds, rather than over them. A frame
79
- * the ring does not hold is opened empty, as {@link begin_frame} opens it.
79
+ * the ring does not hold is opened empty, as {@link begin_frame} opens it — unless the log has
80
+ * moved past it, its slot holding a later frame: the later frame keeps its records, and what is
81
+ * written for this one is dropped at {@link end_frame}.
80
82
  *
81
83
  * For a writer adding to a frame someone else wrote: records arriving from a peer for a frame
82
84
  * this peer has already simulated. Opened with {@link begin_frame} instead, they would erase
83
85
  * this peer's own records for the frame — and with them the prior state a rewind needs to undo
84
86
  * that input, so a replay would apply it a second time, and the outbound stream, which packs
85
- * from this log, would lose it before the peer it was for had confirmed it.
87
+ * from this log, would lose it before the peer it was for had confirmed it. A frame the log has
88
+ * moved past would erase the later frame's records the same way.
86
89
  *
87
90
  * @param {number} frame absolute frame number
88
91
  */
89
92
  resume_frame(frame: number): void;
93
+ /**
94
+ * Open a frame the log holds for writing from byte `offset` of what it holds: the records from
95
+ * there on are dropped, and what is written takes their place.
96
+ *
97
+ * For a writer that has taken records back off the world and writes them again — the frame's
98
+ * earlier records stay, the later ones are written over.
99
+ *
100
+ * @param {number} frame absolute frame number
101
+ * @param {number} offset a record boundary in the frame, at most its end
102
+ */
103
+ resume_frame_at(frame: number, offset: number): void;
90
104
  /**
91
105
  * Drop every record the log holds, keeping each frame it holds present and empty.
92
106
  *
@@ -1 +1 @@
1
- {"version":3,"file":"ActionLog.d.ts","sourceRoot":"","sources":["../../../../../src/engine/network/sim/ActionLog.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH;IAoBI;;OAEG;IACH,qDAFW;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;KAAE,EAIlE;IAED;;;OAGG;IACH,6BAEC;IAED;;;;OAIG;IACH,mBAFW,MAAM,QAShB;IAED;;;;;;;;;;;OAWG;IACH,oBAFW,MAAM,QAShB;IAED;;;;;;;;OAQG;IACH,qBAMC;IAED;;OAEG;IACH,kBASC;IAED;;;;;;OAMG;IACH,kBAFa,OAAO,CAInB;IAED;;;;OAIG;IACH,+BAMC;IAED;;;;;OAKG;IACH,iBAHW,MAAM,GACJ,OAAO,CAInB;IAED;;;;;;;;;;;OAWG;IACH,kBAHW,MAAM,gBAKhB;IAED;;;;OAIG;IACH,qBAHW,MAAM,GACJ,MAAM,CAIlB;;CACJ"}
1
+ {"version":3,"file":"ActionLog.d.ts","sourceRoot":"","sources":["../../../../../src/engine/network/sim/ActionLog.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH;IAoBI;;OAEG;IACH,qDAFW;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;KAAE,EAIlE;IAED;;;OAGG;IACH,6BAEC;IAED;;;;OAIG;IACH,mBAFW,MAAM,QAShB;IAED;;;;;;;;;;;;;;OAcG;IACH,oBAFW,MAAM,QAShB;IAED;;;;;;;;;OASG;IACH,uBAHW,MAAM,UACN,MAAM,QAkBhB;IAED;;;;;;;;OAQG;IACH,qBAMC;IAED;;OAEG;IACH,kBASC;IAED;;;;;;OAMG;IACH,kBAFa,OAAO,CAInB;IAED;;;;OAIG;IACH,+BAMC;IAED;;;;;OAKG;IACH,iBAHW,MAAM,GACJ,OAAO,CAInB;IAED;;;;;;;;;;;OAWG;IACH,kBAHW,MAAM,gBAKhB;IAED;;;;OAIG;IACH,qBAHW,MAAM,GACJ,MAAM,CAIlB;;CACJ"}