@woosh/meep-engine 3.46.0 → 3.47.0

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 (230) hide show
  1. package/editor/actions/concrete/TransformModifyAction.d.ts.map +1 -1
  2. package/editor/actions/concrete/TransformModifyAction.js +10 -2
  3. package/editor/ecs/component/FieldDescriptor.js +15 -0
  4. package/editor/ecs/component/createObjectEditor.js +41 -12
  5. package/editor/ecs/component/editors/ecs/Transform64Editor.d.ts.map +1 -1
  6. package/editor/ecs/component/editors/ecs/Transform64Editor.js +77 -70
  7. package/editor/tools/TransformToolV2.d.ts.map +1 -1
  8. package/editor/tools/TransformToolV2.js +4 -0
  9. package/editor/tools/v2/TransformControls.d.ts.map +1 -1
  10. package/editor/tools/v2/TransformControls.js +7 -0
  11. package/editor/view/ecs/EntityEditor.d.ts.map +1 -1
  12. package/editor/view/ecs/EntityEditor.js +4 -2
  13. package/package.json +1 -1
  14. package/src/core/collection/array/array_insert_at.d.ts +16 -0
  15. package/src/core/collection/array/array_insert_at.d.ts.map +1 -0
  16. package/src/core/collection/array/array_insert_at.js +32 -0
  17. package/src/core/collection/array/array_remove_at.d.ts +15 -0
  18. package/src/core/collection/array/array_remove_at.d.ts.map +1 -0
  19. package/src/core/collection/array/array_remove_at.js +30 -0
  20. package/src/core/geom/3d/quaternion/quat3_look_rotation.d.ts +37 -0
  21. package/src/core/geom/3d/quaternion/quat3_look_rotation.d.ts.map +1 -0
  22. package/src/core/geom/3d/quaternion/quat3_look_rotation.js +172 -0
  23. package/src/core/geom/Quaternion.d.ts +1 -0
  24. package/src/core/geom/Quaternion.d.ts.map +1 -1
  25. package/src/core/geom/Quaternion.js +12 -48
  26. package/src/engine/Engine.d.ts.map +1 -1
  27. package/src/engine/Engine.js +9 -4
  28. package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
  29. package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +10 -0
  30. package/src/engine/ecs/transform/t64_look_rotation.d.ts +4 -4
  31. package/src/engine/ecs/transform/t64_look_rotation.d.ts.map +1 -1
  32. package/src/engine/ecs/transform/t64_look_rotation.js +7 -18
  33. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
  34. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +30 -1
  35. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts +6 -0
  36. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts.map +1 -1
  37. package/src/engine/graphics3/GPUParticleEmitterSystem.js +72 -29
  38. package/src/engine/input/devices/KeyboardDevice.d.ts +1 -0
  39. package/src/engine/input/devices/KeyboardDevice.d.ts.map +1 -1
  40. package/src/engine/input/devices/KeyboardDevice.js +7 -6
  41. package/src/engine/navigation/ecs/path_following/PathFollowingSystem.d.ts.map +1 -1
  42. package/src/engine/navigation/ecs/path_following/PathFollowingSystem.js +15 -2
  43. package/src/engine/network/CONGESTION_CONTROL.md +9 -9
  44. package/src/engine/network/MESSAGE_TRANSPORT.md +364 -0
  45. package/src/engine/network/NetworkSession.d.ts +31 -11
  46. package/src/engine/network/NetworkSession.d.ts.map +1 -1
  47. package/src/engine/network/NetworkSession.js +453 -286
  48. package/src/engine/network/README.md +77 -27
  49. package/src/engine/network/SMOOTHNESS_DESIGN.md +13 -5
  50. package/src/engine/network/diagnostics/BandwidthMeter.d.ts +1 -1
  51. package/src/engine/network/diagnostics/BandwidthMeter.js +1 -1
  52. package/src/engine/network/orchestrator/HostJoin.d.ts +111 -0
  53. package/src/engine/network/orchestrator/HostJoin.d.ts.map +1 -0
  54. package/src/engine/network/orchestrator/HostJoin.js +271 -0
  55. package/src/engine/network/orchestrator/NetworkPeer.d.ts +151 -112
  56. package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
  57. package/src/engine/network/orchestrator/NetworkPeer.js +450 -499
  58. package/src/engine/network/orchestrator/PendingSlices.d.ts +30 -0
  59. package/src/engine/network/orchestrator/PendingSlices.d.ts.map +1 -0
  60. package/src/engine/network/orchestrator/PendingSlices.js +83 -0
  61. package/src/engine/network/orchestrator/ReliableFrames.d.ts +34 -0
  62. package/src/engine/network/orchestrator/ReliableFrames.d.ts.map +1 -0
  63. package/src/engine/network/orchestrator/ReliableFrames.js +96 -0
  64. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +77 -10
  65. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
  66. package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +236 -61
  67. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +20 -6
  68. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
  69. package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +33 -8
  70. package/src/engine/network/replication/Replicator.d.ts.map +1 -1
  71. package/src/engine/network/replication/Replicator.js +7 -1
  72. package/src/engine/network/sim/ActionLog.d.ts +32 -5
  73. package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
  74. package/src/engine/network/sim/ActionLog.js +47 -5
  75. package/src/engine/network/sim/SimActionExecutor.d.ts +42 -2
  76. package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
  77. package/src/engine/network/sim/SimActionExecutor.js +100 -4
  78. package/src/engine/network/sim/Snapshotter.d.ts +12 -0
  79. package/src/engine/network/sim/Snapshotter.d.ts.map +1 -1
  80. package/src/engine/network/sim/Snapshotter.js +43 -0
  81. package/src/engine/network/state/Baseline.d.ts +1 -1
  82. package/src/engine/network/state/Baseline.js +1 -1
  83. package/src/engine/network/state/FrameBufferRing.d.ts +25 -6
  84. package/src/engine/network/state/FrameBufferRing.d.ts.map +1 -1
  85. package/src/engine/network/state/FrameBufferRing.js +51 -6
  86. package/src/engine/network/state/InputRing.d.ts +4 -0
  87. package/src/engine/network/state/InputRing.d.ts.map +1 -1
  88. package/src/engine/network/state/InputRing.js +7 -0
  89. package/src/engine/network/transport/DeferredTransport.d.ts.map +1 -1
  90. package/src/engine/network/transport/DeferredTransport.js +1 -0
  91. package/src/engine/network/transport/LoopbackTransport.d.ts.map +1 -1
  92. package/src/engine/network/transport/LoopbackTransport.js +4 -2
  93. package/src/engine/network/transport/Transport.d.ts +7 -0
  94. package/src/engine/network/transport/Transport.d.ts.map +1 -1
  95. package/src/engine/network/transport/Transport.js +8 -0
  96. package/src/engine/network/transport/adapters/NodeUDPTransport.d.ts.map +1 -1
  97. package/src/engine/network/transport/adapters/NodeUDPTransport.js +20 -5
  98. package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.d.ts.map +1 -1
  99. package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.js +8 -0
  100. package/src/engine/network/transport/adapters/WebTransportTransport.d.ts.map +1 -1
  101. package/src/engine/network/transport/adapters/WebTransportTransport.js +6 -0
  102. package/src/engine/network/transport/connection/Connection.d.ts +191 -0
  103. package/src/engine/network/transport/connection/Connection.d.ts.map +1 -0
  104. package/src/engine/network/transport/connection/Connection.js +1262 -0
  105. package/src/engine/network/transport/connection/RangeSet.d.ts +94 -0
  106. package/src/engine/network/transport/connection/RangeSet.d.ts.map +1 -0
  107. package/src/engine/network/transport/connection/RangeSet.js +260 -0
  108. package/src/engine/network/transport/connection/RxStream.d.ts +48 -0
  109. package/src/engine/network/transport/connection/RxStream.d.ts.map +1 -0
  110. package/src/engine/network/transport/connection/RxStream.js +211 -0
  111. package/src/engine/network/transport/connection/TxStream.d.ts +90 -0
  112. package/src/engine/network/transport/connection/TxStream.d.ts.map +1 -0
  113. package/src/engine/network/transport/connection/TxStream.js +243 -0
  114. package/src/engine/network/transport/connection/bytes_copy.d.ts +17 -0
  115. package/src/engine/network/transport/connection/bytes_copy.d.ts.map +1 -0
  116. package/src/engine/network/transport/connection/bytes_copy.js +34 -0
  117. package/src/engine/network/transport/connection/varint.d.ts +46 -0
  118. package/src/engine/network/transport/connection/varint.d.ts.map +1 -0
  119. package/src/engine/network/transport/connection/varint.js +107 -0
  120. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  121. package/src/engine/sound/ecs/audio/AudioEmitter.d.ts +38 -7
  122. package/src/engine/sound/ecs/audio/AudioEmitter.d.ts.map +1 -1
  123. package/src/engine/sound/ecs/audio/AudioEmitter.js +35 -7
  124. package/src/engine/sound/ecs/audio/AudioEmitterSerializationAdapter.d.ts.map +1 -1
  125. package/src/engine/sound/ecs/audio/AudioEmitterSerializationAdapter.js +5 -1
  126. package/src/engine/sound/ecs/audio/AudioEmitterSerializationUpgrader_0_1.d.ts +18 -0
  127. package/src/engine/sound/ecs/audio/AudioEmitterSerializationUpgrader_0_1.d.ts.map +1 -0
  128. package/src/engine/sound/ecs/audio/AudioEmitterSerializationUpgrader_0_1.js +48 -0
  129. package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts +12 -2
  130. package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts.map +1 -1
  131. package/src/engine/sound/ecs/audio/AudioEmitterSystem.js +173 -9
  132. package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts +9 -3
  133. package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts.map +1 -1
  134. package/src/engine/sound/ecs/audio/LiveEmitterSet.js +28 -8
  135. package/src/engine/sound/ecs/trigger/AudioEventTrigger.d.ts +8 -0
  136. package/src/engine/sound/ecs/trigger/AudioEventTrigger.d.ts.map +1 -1
  137. package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationAdapter.d.ts.map +1 -1
  138. package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationAdapter.js +5 -1
  139. package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationUpgrader_0_1.d.ts +18 -0
  140. package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationUpgrader_0_1.d.ts.map +1 -0
  141. package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationUpgrader_0_1.js +51 -0
  142. package/src/engine/sound/ecs/trigger/AudioEventTriggerSystem.d.ts.map +1 -1
  143. package/src/engine/sound/ecs/trigger/AudioEventTriggerSystem.js +15 -1
  144. package/src/engine/sound/sopra/README.md +768 -644
  145. package/src/engine/sound/sopra/SopraEngine.d.ts +104 -8
  146. package/src/engine/sound/sopra/SopraEngine.d.ts.map +1 -1
  147. package/src/engine/sound/sopra/SopraEngine.js +327 -12
  148. package/src/engine/sound/sopra/definition/EventDescription.d.ts +28 -2
  149. package/src/engine/sound/sopra/definition/EventDescription.d.ts.map +1 -1
  150. package/src/engine/sound/sopra/definition/EventDescription.js +55 -5
  151. package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.d.ts +10 -0
  152. package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.d.ts.map +1 -1
  153. package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.js +77 -55
  154. package/src/engine/sound/sopra/definition/EventDescriptionSerializationUpgrader_0_1.d.ts +31 -0
  155. package/src/engine/sound/sopra/definition/EventDescriptionSerializationUpgrader_0_1.d.ts.map +1 -0
  156. package/src/engine/sound/sopra/definition/EventDescriptionSerializationUpgrader_0_1.js +75 -0
  157. package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.d.ts +38 -9
  158. package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.d.ts.map +1 -1
  159. package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.js +34 -5
  160. package/src/engine/sound/sopra/definition/effect/CompressorEffect.d.ts +7 -2
  161. package/src/engine/sound/sopra/definition/effect/CompressorEffect.d.ts.map +1 -1
  162. package/src/engine/sound/sopra/definition/effect/CompressorEffect.js +137 -122
  163. package/src/engine/sound/sopra/definition/effect/EqEffect.d.ts +3 -1
  164. package/src/engine/sound/sopra/definition/effect/EqEffect.d.ts.map +1 -1
  165. package/src/engine/sound/sopra/definition/effect/EqEffect.js +146 -131
  166. package/src/engine/sound/sopra/definition/effect/ReverbEffect.d.ts +1 -0
  167. package/src/engine/sound/sopra/definition/effect/ReverbEffect.d.ts.map +1 -1
  168. package/src/engine/sound/sopra/definition/effect/ReverbEffect.js +6 -1
  169. package/src/engine/sound/sopra/runtime/BusGraph.d.ts +44 -2
  170. package/src/engine/sound/sopra/runtime/BusGraph.d.ts.map +1 -1
  171. package/src/engine/sound/sopra/runtime/BusGraph.js +83 -43
  172. package/src/engine/sound/sopra/runtime/EffectChain.d.ts +96 -0
  173. package/src/engine/sound/sopra/runtime/EffectChain.d.ts.map +1 -0
  174. package/src/engine/sound/sopra/runtime/EffectChain.js +286 -0
  175. package/src/engine/sound/sopra/runtime/EffectParamOverrides.d.ts +123 -0
  176. package/src/engine/sound/sopra/runtime/EffectParamOverrides.d.ts.map +1 -0
  177. package/src/engine/sound/sopra/runtime/EffectParamOverrides.js +320 -0
  178. package/src/engine/sound/sopra/runtime/EventInstance.d.ts +55 -5
  179. package/src/engine/sound/sopra/runtime/EventInstance.d.ts.map +1 -1
  180. package/src/engine/sound/sopra/runtime/EventInstance.js +201 -26
  181. package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.d.ts +2 -1
  182. package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.d.ts.map +1 -1
  183. package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.js +6 -1
  184. package/src/engine/sound/sopra/serialization/sopraSerializationHarness.d.ts +15 -0
  185. package/src/engine/sound/sopra/serialization/sopraSerializationHarness.d.ts.map +1 -1
  186. package/src/engine/sound/sopra/serialization/sopraSerializationHarness.js +32 -0
  187. package/src/engine/sound/sopra/util/MockAudioContext.d.ts.map +1 -1
  188. package/src/engine/sound/sopra/util/MockAudioContext.js +3 -0
  189. package/src/engine/sound/sopra/util/fadeOutAndStop.d.ts +13 -3
  190. package/src/engine/sound/sopra/util/fadeOutAndStop.d.ts.map +1 -1
  191. package/src/engine/sound/sopra/util/fadeOutAndStop.js +25 -13
  192. package/src/shade/RENDERER_CONTRACT.md +6 -3
  193. package/src/shade/device/SHADE_DEVICE_FLOOR.d.ts +24 -0
  194. package/src/shade/device/SHADE_DEVICE_FLOOR.d.ts.map +1 -0
  195. package/src/shade/device/SHADE_DEVICE_FLOOR.js +34 -0
  196. package/src/shade/device/ShadeDeviceFailure.d.ts +12 -5
  197. package/src/shade/device/ShadeDeviceFailure.d.ts.map +1 -1
  198. package/src/shade/device/ShadeDeviceFailure.js +17 -8
  199. package/src/shade/device/ShadeDeviceFailureReason.d.ts +1 -1
  200. package/src/shade/device/ShadeDeviceFailureReason.js +2 -2
  201. package/src/shade/device/shade_device_floor_unmet.d.ts +13 -0
  202. package/src/shade/device/shade_device_floor_unmet.d.ts.map +1 -0
  203. package/src/shade/device/shade_device_floor_unmet.js +32 -0
  204. package/src/shade/renderer/Renderer.d.ts +2 -0
  205. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  206. package/src/shade/renderer/Renderer.js +29 -55
  207. package/src/shade/renderer/buffer/GPUBufferAllocator.d.ts.map +1 -1
  208. package/src/shade/renderer/buffer/GPUBufferAllocator.js +9 -0
  209. package/src/shade/renderer/buffer/buffer_pool_ops.d.ts +21 -12
  210. package/src/shade/renderer/buffer/buffer_pool_ops.d.ts.map +1 -1
  211. package/src/shade/renderer/buffer/buffer_pool_ops.js +18 -19
  212. package/src/shade/renderer/gpu_primitive/test/gpu_test.d.ts.map +1 -1
  213. package/src/shade/renderer/gpu_primitive/test/gpu_test.js +8 -11
  214. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  215. package/src/shade/renderer/pool/TransientResourcePool.d.ts +24 -0
  216. package/src/shade/renderer/pool/TransientResourcePool.d.ts.map +1 -1
  217. package/src/shade/renderer/pool/TransientResourcePool.js +41 -10
  218. package/src/shade/renderer/shader/ComputeShader.d.ts.map +1 -1
  219. package/src/shade/renderer/shader/ComputeShader.js +18 -2
  220. package/src/shade/renderer/shader/ImageShader.d.ts.map +1 -1
  221. package/src/shade/renderer/shader/ImageShader.js +230 -218
  222. package/src/view/graphics/GraphicsContextFailureView.d.ts +1 -2
  223. package/src/view/graphics/GraphicsContextFailureView.d.ts.map +1 -1
  224. package/src/view/graphics/GraphicsContextFailureView.js +1 -2
  225. package/src/engine/network/transport/Channel.js +0 -336
  226. package/src/engine/network/transport/ReliableCommandPipeline.js +0 -335
  227. package/src/engine/network/transport/fragments/FragmentAssembler.js +0 -350
  228. package/src/engine/network/transport/fragments/FragmentRetention.js +0 -191
  229. package/src/engine/network/transport/fragments/fragment_send.js +0 -157
  230. package/src/engine/network/transport/fragments/packet_size.js +0 -107
@@ -7,14 +7,48 @@ server-authoritative rollback orchestrator, layered over a pluggable transport.
7
7
 
8
8
  | Layer | Pieces | Responsibility |
9
9
  | --- | --- | --- |
10
- | Transport | `transport/adapters/*` (`WebRTCDataChannel`, `WebTransport`, `WebSocket`, `NodeUDP`, `Simulated`, `Loopback`) | Move opaque byte buffers. UDP-style unless `reliable`/`ordered` say otherwise. |
11
- | Channel | `transport/Channel.js` | Seq + ack-bitfield header; surfaces `onPacketAcked` / `onPacketLost` / `onPayload`. No retransmit. |
12
- | Reliability / fragmentation | `transport/ReliableCommandPipeline.js`, `transport/fragments/*` | At-least-once commands; MTU fragmentation + NACK retransmit. |
10
+ | Transport | `transport/adapters/*` (`WebRTCDataChannel`, `WebTransport`, `WebSocket`, `NodeUDP`, `Simulated`, `Loopback`) | Move opaque datagrams of at most `max_datagram_bytes`. UDP-style unless `reliable` says otherwise. |
11
+ | Connection | `transport/connection/*` (`Connection`, `TxStream`, `RxStream`, `RangeSet`) | Numbered packets acknowledged by ranges, loss detection and probe timeout after QUIC; unreliable messages of up to a datagram with a delivery notice; reliable lanes for messages of any size up to `max_message_bytes`, paced. Everything for a peer leaves in as few datagrams as fit, once a tick. Design: [`MESSAGE_TRANSPORT.md`](./MESSAGE_TRANSPORT.md). |
13
12
  | Sim core | `sim/*` (`ActionLog`, `SimAction`, `SimActionExecutor`, `RewindEngine`, `Snapshotter`, registries) | Record/apply/rewind replicated state mutations. |
14
13
  | Replication | `replication/Replicator.js`, `replication/ScopeFilter.js` | Pack/unpack action records per peer, scope-filtered. |
15
14
  | Orchestrator | `orchestrator/NetworkPeer.js`, `orchestrator/ServerAuthoritative{Server,Client}.js` | Per-tick send/recv, rollback (server), predict-reconcile (client). |
16
15
  | Facade | `NetworkSession.js` | One config + lifecycle surface; wires everything below. |
17
16
 
17
+ ## What the session assumes, and what a tick costs
18
+
19
+ The stack is built for fully networked worlds of millions of objects. A tick's cost is in what
20
+ changed and what is owned — never in how many networked entities there are — and that holds only on
21
+ these terms, which the game has to keep:
22
+
23
+ - **Every replicated change goes through the host's action executor.** An action — `send`, a
24
+ client's input — carries its own change. A system that writes a replicated component directly
25
+ (physics moving its bodies, a controller) reports each write:
26
+ `dataset.sendEvent(entity, "net_mutate_component", { component_type })`. The host records the
27
+ component as it stands, at full precision — the wire's quantised image is never written back into
28
+ the host's state. **A write nobody reports stays on the host**: the session never looks for
29
+ changes by itself. (Until 2026-10-15 it compared every replicated component of every networked
30
+ entity at the end of each host step: 7–8 ms a tick for 20,000 entities with nothing changing.)
31
+ - **Ownership is set when a `NetworkIdentity` attaches, or by the join** — never by writing
32
+ `owner_peer_id` or `network_id` afterwards. Each peer's owned entities are indexed when ownership
33
+ is set; what a tick does per owned entity (AUTH_STATE to its owner, the client's record of what it
34
+ predicted) walks that index.
35
+
36
+ Measured (one-component trees, one client, 30 Hz, Node 22, ms per tick):
37
+
38
+ | | ms |
39
+ | --- | --- |
40
+ | Host, 20,000 entities, nothing changing, client synced | 0.01–0.02 (was 14–15) |
41
+ | Client, 20,000 entities, nothing changing | 0.02 (was 0.7) |
42
+ | Host, 100 / 1,000 actions a tick, building them included | 0.4 / 3.8 |
43
+ | Client, applying 100 / 1,000 actions a tick | 1.3 / 4.2 |
44
+
45
+ What does scale with the world, by design:
46
+
47
+ - **A join** sends every networked entity once per joining peer, paged and paced; host memory per
48
+ joiner is the lane's window and 12 bytes an entity (`MESSAGE_TRANSPORT.md` §3.7).
49
+ - **Interpolation**: every networked entity gets an `Interpolated`, and the `InterpolationSystem`
50
+ records and blends each one every frame (below). Not measured at this scale.
51
+
18
52
  ## Smooth by default
19
53
 
20
54
  A game that wires a `NetworkSession` the obvious way renders smoothly at any display rate, with
@@ -47,10 +81,9 @@ nothing else to set up. What that takes, and where it lives:
47
81
  `Interpolated.snap` for a teleport (`PhysicsSystem.setPose` sets it), or
48
82
  `InterpolationSystem.restore()` / `session.normalize_if_dirty()` before writing the truth. A write
49
83
  over a blend without either is a write to a drawn pose, and the next step puts the truth back.
50
- - **The host publishes what changed.** Replicated components the host's simulation wrote directly
51
- (physics, controllers) are published at the end of each step, recorded without being written
52
- back — the host keeps full precision — and nothing an action already carried is sent twice.
53
- `publish_changes: false` to publish by hand.
84
+ - **What travels is what goes through the executor** (see "What the session assumes" above):
85
+ actions, and direct writes reported with `net_mutate_component`, which the host records without
86
+ writing them back — the host keeps full precision.
54
87
  - **Clients reconcile by comparison** (`reconcile: 'correct'`, the default): the host's state for
55
88
  frame S is compared with what the client predicted for S, and a component that differs is
56
89
  carried into the present by its `Correction` (`network/correction`) — the session knows no
@@ -94,34 +127,51 @@ connection:
94
127
  is ever sent, on this connection or a later one that resumes it: that input was predicted on a
95
128
  world that is gone. The client's input starts at the frame the snapshot sets its counter to.
96
129
 
97
- The first answer is the answer. The host sends each `INITIAL_SYNC` more than once, against loss,
98
- and a host that has finished with a client's old session readmits its new connection as a fresh
99
- peer before it reads the `RESUME_HELLO` on it — so a `RESUME_REJECT` can arrive after the snapshot it
100
- announces. A client acts on neither once the connection has its answer: the copies would put what it
101
- owns back behind input it has already sent, and the reject would throw away the token the snapshot
102
- issued.
130
+ The first answer is the answer. A host that has finished with a client's old session readmits its
131
+ new connection as a fresh peer before it reads the `RESUME_HELLO` on it — so a `RESUME_REJECT` can
132
+ arrive after the snapshot it announces. The snapshot's first page is the answer: a client acts on no
133
+ later accept or reject on that connection, and the reconnect ladder stops there, so a join that takes
134
+ a while is not torn down and restarted by the next attempt.
103
135
 
104
136
  The host keeps to the same rule from its side, because what a peer can make it do has to be bounded
105
137
  by what the host decided, not by how often the peer asks. It answers a connection's `RESUME_HELLO`
106
- once and ignores any later one on that connection, and it sends a connection its `INITIAL_SYNC` —
107
- the copies against loss included — once, when it takes the connection for a fresh peer: as it binds
108
- it, or after refusing the connection's claim to resume. Each is the whole world, serialized; no
109
- number of claims buys another. A connection here is a transport the host binds with `connect()`, so
138
+ once and ignores any later one on that connection, and it gives a connection one join, when it takes
139
+ the connection for a fresh peer: as it binds it, or after refusing the connection's claim to resume.
140
+ A join is the whole world, once, and then what changed while it was going out, for at most eight
141
+ rounds; no number of claims buys another. A peer that drops before its join finished is never
142
+ resumed: it holds part of a world. A connection here is a transport the host binds with `connect()`, so
110
143
  a reconnect has to reach the host as a new one — the fresh transport a `transport_factory` returns,
111
144
  bound once the host has seen the old one end. One that lands on a connection the host still holds
112
145
  is a second claim on that connection, and is not sent the world again.
113
146
 
114
- A chunk of a fragmented message that goes missing — of an `INITIAL_SYNC`, typically — is re-sent when
115
- the receiver asks with a `NACK_FRAGMENT`, and that repair is bounded the same way. A NACK re-sends
116
- each chunk of the message at most once however many times it names it, and nothing for an index the
117
- message does not have; the host answers three NACKs per message. So repair adds at most three copies
118
- of any message the host chose to send.
147
+ ### Joining a world of any size
148
+
149
+ A join is pages of whole entities on the connection's paced BULK lane, each built when it is sent;
150
+ what is lost is re-sent by the lane. The host holds the joining peer's action stream and AUTH_STATE
151
+ meanwhile. A round that goes out within one tick is built at one frame D and is the last: the FINAL
152
+ page follows it, and the action stream is released from D + 1. The first round — the whole world —
153
+ gets one tick's budget (256 KiB); a larger world goes out over as many ticks as the lane's window
154
+ takes, and when it has all arrived, every entity changed since its page — by an action the host's
155
+ executor ran — or created since, is the next round, which may fill the lane. The client holds the
156
+ action stream and AUTH_STATE from `connect()` until its world is whole, applies each page as it
157
+ lands, and on FINAL seeks its frame counter to the host's newest frame, forgets what it predicted on
158
+ the partial world, applies what it held, releases its own input and fires `onSynced`. Host memory
159
+ per joining peer is the lane's window and 12 bytes per entity; client memory is a page and what it
160
+ holds, at most `max_queued_bytes`.
161
+
162
+ ### A connection that breaks
163
+
164
+ A connection does not authenticate datagrams; its transport delivers only the peer's (DTLS, TLS, and
165
+ `NodeUDP` by source address). When a connection stops being whole — a peer past its limits, or an
166
+ acknowledgement of a packet never sent, the mark of a datagram that was not the peer's — the session
167
+ handles it as a dropped transport and closes the transport: the host puts the peer in its grace
168
+ window and the client resumes on a new connection. See [`MESSAGE_TRANSPORT.md`](./MESSAGE_TRANSPORT.md) §3.9.
119
169
 
120
170
  ## Wired automatically by `NetworkSession`
121
171
 
122
- `NetworkPeer` (and through it `Channel`, `Replicator`, `ActionLog`,
123
- `SimActionExecutor`, the registries, `ReplicationSlotTable`, `Baseline`,
124
- the fragment layer, and `ReliableCommandPipeline`), the two orchestrators
172
+ `NetworkPeer` (and through it a `Connection` per peer, `Replicator`, `ActionLog`,
173
+ `SimActionExecutor`, the registries, `ReplicationSlotTable`, `Baseline`, and the
174
+ host's `HostJoin`s), the two orchestrators
125
175
  (and through them `RewindEngine` and `InputRing`), plus `TimeDilation`,
126
176
  `AdaptiveRenderDelay`, `RenderPlayout`, `InterpolationLog`, `NetworkSystem`,
127
177
  `NetworkIdentity`, `Snapshotter`, `SmoothingState` (through the
@@ -149,7 +199,7 @@ If you wire one of these into the facade, move its row up to the section above.
149
199
 
150
200
  ## Deferred / known gaps
151
201
 
152
- - **Congestion control / send pacing** — there is none on the meep-owned send
153
- path. Masked on the browser transports (DTLS/SCTP, TLS/TCP do it underneath);
202
+ - **Congestion control** — the send rate is a fixed token bucket
203
+ (`send_rate_bytes_per_second`); nothing adapts it to the link. Masked on the browser transports (DTLS/SCTP, TLS/TCP do it underneath);
154
204
  only matters for raw `NodeUDP` / `Simulated` on constrained links. Design and
155
205
  rationale for the deferral are in [`CONGESTION_CONTROL.md`](./CONGESTION_CONTROL.md).
@@ -92,7 +92,15 @@ avoided is corrected again by the next AUTH_STATE).
92
92
  The action-log rewind + replay path stays, as an opt-in (`reconcile: 'replay'`), for games whose
93
93
  predicted state is entirely produced by deterministic `SimAction`s and that want exactness.
94
94
 
95
- **The host publishes what changed.** At the end of every host step, each replicated component of
95
+ **Superseded 2026-10-15: the host no longer looks for changes.** What follows describes the
96
+ end-of-step comparison as built. It cost every host tick a pass over every networked entity — 7–8 ms
97
+ for 20,000 entities with nothing changing — and the stack is for worlds of millions. It is gone: what
98
+ travels is what goes through the executor, actions and direct writes reported with
99
+ `net_mutate_component`, which the host records (`executor.record`, no write-back, D9's second half
100
+ kept fixed). A direct write nobody reports stays on the host — D9's first half is the contract now,
101
+ stated in the README's "What the session assumes".
102
+
103
+ **The host publishes what changed** (as built, superseded above). At the end of every host step, each replicated component of
96
104
  each networked entity is encoded with its replication adapter and compared with what was last
97
105
  published; changed ones go out as `ReplaceComponentAction` records. They are recorded without
98
106
  applying (the host's live state is not overwritten with its quantised wire image — D9) and
@@ -171,9 +179,9 @@ path stays for `ServerAuthoritativeServer` users and for `reconcile: 'replay'` s
171
179
  no component type, and no physics. `POSE_CORRECTION` (Transform64, through physics for a body,
172
180
  hidden by `InterpolationSystem.correct`), `VELOCITY_CORRECTION` and `HOST_VALUE_CORRECTION` are
173
181
  given. `reconcile: 'correct' | 'replay'`, default `'correct'`.
174
- - Host: publishes changed replicated state at the end of each step, against a baseline of what the
175
- peers hold that every executed action also updates; `late_input: 'apply' | 'rollback'`, default
176
- `'apply'`.
182
+ - Host: ~~publishes changed replicated state at the end of each step~~ (removed 2026-10-15: what
183
+ travels is what goes through the executor; a reported direct write is recorded, not executed);
184
+ `late_input: 'apply' | 'rollback'`, default `'apply'`.
177
185
 
178
186
  ### Orchestrators
179
187
  - `ServerAuthoritativeServer`: `begin_tick(frame)` / `end_tick()` split, so the EM's systems run
@@ -204,7 +212,7 @@ path stays for `ServerAuthoritativeServer` users and for `reconcile: 'replay'` s
204
212
  the next step. This was the rule for interpolated physics bodies already; it now holds for every
205
213
  `Interpolated` entity. **Behaviour change** for code that relied on the old absence of any restore
206
214
  for non-physics entities.
207
- - Peer ids are `[0, 252]`; 253 is `SENDER_RECORDED`.
215
+ - Peer ids are `[0, 251]`; 252 is `SENDER_CORRECTED`, 253 is `SENDER_RECORDED`.
208
216
  - `PhysicsSystem.interpolationLog` is deprecated (ignored, warns once).
209
217
  - Wire format: a new `FRAME_MARKER` packet; nothing else.
210
218
  - `scene-editor`'s `RemotePresence` (the only in-repo `registerSource` user) needed one change: a
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Aggregates `getStats()` snapshots from one or more named sources (typically
3
- * a `Transport`, `Channel`, or `NetworkPeer.channel_for(peer)`) and reports
3
+ * a `Transport`, or `NetworkPeer.connection_for(peer)`) and reports
4
4
  * recent throughput rates.
5
5
  *
6
6
  * Per-source stats are queried via the source's `getStats()` method. A "sample"
@@ -2,7 +2,7 @@ import { assert } from "../../../core/assert.js";
2
2
 
3
3
  /**
4
4
  * Aggregates `getStats()` snapshots from one or more named sources (typically
5
- * a `Transport`, `Channel`, or `NetworkPeer.channel_for(peer)`) and reports
5
+ * a `Transport`, or `NetworkPeer.connection_for(peer)`) and reports
6
6
  * recent throughput rates.
7
7
  *
8
8
  * Per-source stats are queried via the source's `getStats()` method. A "sample"
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Bytes of entity records a page is filled to. A page is a BULK message, so this is also how much
3
+ * the receiver holds of a join at a time. An entity larger than this is a page by itself.
4
+ */
5
+ export const JOIN_PAGE_BYTES: number;
6
+ /**
7
+ * Bytes of pages the first round — the whole world — may queue in one tick to finish there. A
8
+ * round that finishes in one tick is the join's last: every entity it carries is at the same
9
+ * frame, and every entity it leaves out is unchanged since its state went out, so the receiver
10
+ * holds the whole world at that frame. A world larger than this goes out over several ticks, paced
11
+ * by the lane's window, and a catch-up round follows.
12
+ *
13
+ * A catch-up round always tries to finish in its tick, with what the BULK lane may still hold as
14
+ * its budget: what changed while a world went out is a fraction of it, and a round that may only
15
+ * page would never shrink for a world that changes faster than a tick's budget.
16
+ */
17
+ export const JOIN_FINAL_ROUND_BYTES: number;
18
+ /**
19
+ * Catch-up rounds a join may take before it is given up. A catch-up round finishes in its tick
20
+ * unless what changed outgrows what the BULK lane may hold, so a join that needs more than one or
21
+ * two is a world changing faster than a peer can be sent it.
22
+ */
23
+ export const JOIN_MAX_ROUNDS: 8;
24
+ /**
25
+ * What a join has to say after {@link HostJoinadvance }.
26
+ */
27
+ export type JoinProgress = number;
28
+ /**
29
+ * What a join has to say after {@link HostJoin#advance}.
30
+ * @readonly
31
+ * @enum {number}
32
+ */
33
+ export const JoinProgress: Readonly<{
34
+ /** Pages are going out, or the host is waiting to hear they arrived. */
35
+ Sending: 0;
36
+ /**
37
+ * The FINAL page has arrived: the peer holds the whole world. (Its action stream was released
38
+ * when FINAL was queued.)
39
+ */
40
+ Done: 1;
41
+ /** The world changed faster than it could be sent, round after round: drop the peer. */
42
+ DidNotConverge: 2;
43
+ }>;
44
+ /**
45
+ * The host's side of one peer joining: the world, sent as pages built when they go out, then what
46
+ * changed meanwhile, until the peer holds every entity's state at one frame. See
47
+ * `network/MESSAGE_TRANSPORT.md`, "Joining a world of any size".
48
+ *
49
+ * 1. The peer's action stream is held from the start (the caller holds it).
50
+ * 2. A round starts with a list of entities. If its pages fit {@link JOIN_FINAL_ROUND_BYTES}, they
51
+ * all go in the tick the round starts, at that frame D; the FINAL page follows them, and the
52
+ * peer's action stream is released from D + 1. The join is over. (The stream's datagrams can
53
+ * overtake the last pages; the peer holds the stream until FINAL — `NetworkPeer.hold_inbound`.)
54
+ * 3. Otherwise the round's pages go out over as many ticks as it takes, each built at the frame it
55
+ * is sent, while the BULK lane has less than {@link window_bytes} unacknowledged. Each entity's
56
+ * network id is noted with the frame its state was sent at. Host memory: the window, and 8
57
+ * bytes per entity.
58
+ * 4. When every page of such a round has arrived, every networked entity is checked: one changed
59
+ * since its state was sent (`NetworkPeer.last_changed_frame`), or created since, is the next
60
+ * round's list. An empty list ends the join with the FINAL page alone.
61
+ *
62
+ * The first round is the world. For a world that fits one tick's budget, the join is one tick.
63
+ *
64
+ * Built per join; allocates its id list and send-frame table once and grows them, nothing per tick.
65
+ *
66
+ * @author Alex Goldring
67
+ * @copyright Company Named Limited (c) 2026
68
+ */
69
+ export class HostJoin {
70
+ /**
71
+ * @param {{
72
+ * peer: import("./NetworkPeer.js").NetworkPeer,
73
+ * peer_id: number,
74
+ * session_token: Uint8Array,
75
+ * world: import("../../ecs/EntityComponentDataset.js").EntityComponentDataset,
76
+ * for_each_entity: function(function(number): void): void,
77
+ * page_buffer: BinaryBuffer,
78
+ * window_bytes: number,
79
+ * }} options
80
+ * `for_each_entity(cb)` visits every networked entity id. `page_buffer` is scratch the caller
81
+ * may share between joins. `window_bytes` is how far ahead of the peer's acknowledgements
82
+ * pages are built.
83
+ */
84
+ constructor({ peer, peer_id, session_token, world, for_each_entity, page_buffer, window_bytes }: {
85
+ peer: import("./NetworkPeer.js").NetworkPeer;
86
+ peer_id: number;
87
+ session_token: Uint8Array;
88
+ world: import("../../ecs/EntityComponentDataset.js").EntityComponentDataset;
89
+ for_each_entity: (arg0: (arg0: number) => void) => void;
90
+ page_buffer: BinaryBuffer;
91
+ window_bytes: number;
92
+ });
93
+ peer: import("./NetworkPeer.js").NetworkPeer;
94
+ peer_id: number;
95
+ session_token: Uint8Array;
96
+ world: import("../../ecs/EntityComponentDataset.js").EntityComponentDataset;
97
+ for_each_entity: (arg0: (arg0: number) => void) => void;
98
+ page_buffer: BinaryBuffer;
99
+ window_bytes: number;
100
+ /** Catch-up rounds taken so far. */
101
+ get round(): number;
102
+ /**
103
+ * One tick of the join, at `frame` — the frame just simulated and published.
104
+ * @param {number} frame
105
+ * @returns {JoinProgress}
106
+ */
107
+ advance(frame: number): JoinProgress;
108
+ #private;
109
+ }
110
+ import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
111
+ //# sourceMappingURL=HostJoin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HostJoin.d.ts","sourceRoot":"","sources":["../../../../../src/engine/network/orchestrator/HostJoin.js"],"names":[],"mappings":"AAKA;;;GAGG;AACH,qCAAyC;AAEzC;;;;;;;;;;GAUG;AACH,4CAAiD;AAEjD;;;;GAIG;AACH,gCAAiC;;;;2BAKvB,MAAM;AAHhB;;;;GAIG;AACH;IACI,wEAAwE;;IAExE;;;OAGG;;IAEH,wFAAwF;;GAEzF;AAOH;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH;IAcI;;;;;;;;;;;;;OAaG;IACH,iGAbW;QACV,IAAQ,EAAE,OAAO,kBAAkB,EAAE,WAAW,CAAC;QACjD,OAAW,EAAE,MAAM,CAAC;QACpB,aAAiB,EAAE,UAAU,CAAC;QAC9B,KAAS,EAAE,OAAO,qCAAqC,EAAE,sBAAsB,CAAC;QAChF,eAAmB,gBAAoB,MAAM,KAAG,IAAI,KAAG,IAAI,CAAC;QAC5D,WAAe,EAAE,YAAY,CAAC;QAC9B,YAAgB,EAAE,MAAM,CAAC;KACtB,EAgBH;IAVG,6CAAgB;IAChB,gBAAsB;IACtB,0BAAkC;IAClC,4EAAkB;IAClB,+BAboC,MAAM,KAAG,IAAI,KAAG,IAAI,CAalB;IACtC,0BAA8B;IAC9B,qBAAgC;IAMpC,oCAAoC;IACpC,oBAEC;IAED;;;;OAIG;IACH,eAHW,MAAM,GACJ,YAAY,CA6DxB;;CAkFJ;6BA9Q4B,sCAAsC"}
@@ -0,0 +1,271 @@
1
+ import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
2
+ import { snapshotter_write_entity } from "../sim/Snapshotter.js";
3
+ import { Lane } from "../transport/connection/Connection.js";
4
+ import { InitialSyncPage } from "./NetworkPeer.js";
5
+
6
+ /**
7
+ * Bytes of entity records a page is filled to. A page is a BULK message, so this is also how much
8
+ * the receiver holds of a join at a time. An entity larger than this is a page by itself.
9
+ */
10
+ export const JOIN_PAGE_BYTES = 16 * 1024;
11
+
12
+ /**
13
+ * Bytes of pages the first round — the whole world — may queue in one tick to finish there. A
14
+ * round that finishes in one tick is the join's last: every entity it carries is at the same
15
+ * frame, and every entity it leaves out is unchanged since its state went out, so the receiver
16
+ * holds the whole world at that frame. A world larger than this goes out over several ticks, paced
17
+ * by the lane's window, and a catch-up round follows.
18
+ *
19
+ * A catch-up round always tries to finish in its tick, with what the BULK lane may still hold as
20
+ * its budget: what changed while a world went out is a fraction of it, and a round that may only
21
+ * page would never shrink for a world that changes faster than a tick's budget.
22
+ */
23
+ export const JOIN_FINAL_ROUND_BYTES = 256 * 1024;
24
+
25
+ /**
26
+ * Catch-up rounds a join may take before it is given up. A catch-up round finishes in its tick
27
+ * unless what changed outgrows what the BULK lane may hold, so a join that needs more than one or
28
+ * two is a world changing faster than a peer can be sent it.
29
+ */
30
+ export const JOIN_MAX_ROUNDS = 8;
31
+
32
+ /**
33
+ * What a join has to say after {@link HostJoin#advance}.
34
+ * @readonly
35
+ * @enum {number}
36
+ */
37
+ export const JoinProgress = Object.freeze({
38
+ /** Pages are going out, or the host is waiting to hear they arrived. */
39
+ Sending: 0,
40
+ /**
41
+ * The FINAL page has arrived: the peer holds the whole world. (Its action stream was released
42
+ * when FINAL was queued.)
43
+ */
44
+ Done: 1,
45
+ /** The world changed faster than it could be sent, round after round: drop the peer. */
46
+ DidNotConverge: 2,
47
+ });
48
+
49
+ const PHASE_PAGING = 0;
50
+ const PHASE_AWAITING = 1;
51
+ /** FINAL is queued and the stream released; the join is over once FINAL has arrived */
52
+ const PHASE_FINAL_SENT = 2;
53
+
54
+ /**
55
+ * The host's side of one peer joining: the world, sent as pages built when they go out, then what
56
+ * changed meanwhile, until the peer holds every entity's state at one frame. See
57
+ * `network/MESSAGE_TRANSPORT.md`, "Joining a world of any size".
58
+ *
59
+ * 1. The peer's action stream is held from the start (the caller holds it).
60
+ * 2. A round starts with a list of entities. If its pages fit {@link JOIN_FINAL_ROUND_BYTES}, they
61
+ * all go in the tick the round starts, at that frame D; the FINAL page follows them, and the
62
+ * peer's action stream is released from D + 1. The join is over. (The stream's datagrams can
63
+ * overtake the last pages; the peer holds the stream until FINAL — `NetworkPeer.hold_inbound`.)
64
+ * 3. Otherwise the round's pages go out over as many ticks as it takes, each built at the frame it
65
+ * is sent, while the BULK lane has less than {@link window_bytes} unacknowledged. Each entity's
66
+ * network id is noted with the frame its state was sent at. Host memory: the window, and 8
67
+ * bytes per entity.
68
+ * 4. When every page of such a round has arrived, every networked entity is checked: one changed
69
+ * since its state was sent (`NetworkPeer.last_changed_frame`), or created since, is the next
70
+ * round's list. An empty list ends the join with the FINAL page alone.
71
+ *
72
+ * The first round is the world. For a world that fits one tick's budget, the join is one tick.
73
+ *
74
+ * Built per join; allocates its id list and send-frame table once and grows them, nothing per tick.
75
+ *
76
+ * @author Alex Goldring
77
+ * @copyright Company Named Limited (c) 2026
78
+ */
79
+ export class HostJoin {
80
+ /** entity ids still to send this round */
81
+ #entities;
82
+ #count = 0;
83
+ #cursor = 0;
84
+ /** network id → frame its state was last sent at, -1 for never */
85
+ #sent_frame = new Float64Array(256).fill(-1);
86
+ #round = 0;
87
+ #phase = PHASE_PAGING;
88
+ #mark = -1;
89
+ #first_sent = false;
90
+ /** the first round's list is the world as it is when the join first advances */
91
+ #started = false;
92
+
93
+ /**
94
+ * @param {{
95
+ * peer: import("./NetworkPeer.js").NetworkPeer,
96
+ * peer_id: number,
97
+ * session_token: Uint8Array,
98
+ * world: import("../../ecs/EntityComponentDataset.js").EntityComponentDataset,
99
+ * for_each_entity: function(function(number): void): void,
100
+ * page_buffer: BinaryBuffer,
101
+ * window_bytes: number,
102
+ * }} options
103
+ * `for_each_entity(cb)` visits every networked entity id. `page_buffer` is scratch the caller
104
+ * may share between joins. `window_bytes` is how far ahead of the peer's acknowledgements
105
+ * pages are built.
106
+ */
107
+ constructor({ peer, peer_id, session_token, world, for_each_entity, page_buffer, window_bytes }) {
108
+ this.peer = peer;
109
+ this.peer_id = peer_id;
110
+ this.session_token = session_token;
111
+ this.world = world;
112
+ this.for_each_entity = for_each_entity;
113
+ this.page_buffer = page_buffer;
114
+ this.window_bytes = window_bytes;
115
+
116
+ this.#entities = new Int32Array(64);
117
+ this.#count = 0;
118
+ }
119
+
120
+ /** Catch-up rounds taken so far. */
121
+ get round() {
122
+ return this.#round;
123
+ }
124
+
125
+ /**
126
+ * One tick of the join, at `frame` — the frame just simulated and published.
127
+ * @param {number} frame
128
+ * @returns {JoinProgress}
129
+ */
130
+ advance(frame) {
131
+ const connection = this.peer.connection_for(this.peer_id);
132
+ if (connection === undefined) return JoinProgress.Sending;
133
+
134
+ if (!this.#started) {
135
+ // the world at the first frame the join runs on, not at connect: what is built between
136
+ // the two is part of it
137
+ this.#started = true;
138
+ this.for_each_entity(this.#push_entity);
139
+ }
140
+
141
+ if (this.#phase === PHASE_FINAL_SENT) {
142
+ // Over only once FINAL has arrived: until then the peer holds part of a world, and a
143
+ // peer that drops now must not be resumed into it.
144
+ return connection.is_delivered(Lane.BULK, this.#mark) ? JoinProgress.Done : JoinProgress.Sending;
145
+ }
146
+
147
+ for (; ;) {
148
+ if (this.#phase === PHASE_PAGING) {
149
+ if (this.#cursor === 0) {
150
+ // A round starting now: if it all goes in this tick, it is the last. The world
151
+ // is held to one tick's budget; a catch-up round may fill the lane, leaving
152
+ // room for one more page of the largest a message may be.
153
+ const lane_room = connection.max_queued_bytes - connection.unacked_bytes(Lane.BULK)
154
+ - connection.max_message_bytes;
155
+ const budget = this.#round === 0 ? Math.min(JOIN_FINAL_ROUND_BYTES, lane_room) : lane_room;
156
+ let queued = 0;
157
+ while (this.#cursor < this.#count && queued < budget) {
158
+ queued += this.#send_page(frame, 0);
159
+ }
160
+ if (this.#cursor >= this.#count) {
161
+ this.#send_page(frame, InitialSyncPage.FINAL);
162
+ this.peer.release_outbound(this.peer_id, frame + 1);
163
+ this.#phase = PHASE_FINAL_SENT;
164
+ return JoinProgress.Sending;
165
+ }
166
+ }
167
+ while (this.#cursor < this.#count && connection.unacked_bytes(Lane.BULK) < this.window_bytes) {
168
+ this.#send_page(frame, 0);
169
+ }
170
+ if (this.#cursor < this.#count) return JoinProgress.Sending;
171
+ this.#phase = PHASE_AWAITING;
172
+ }
173
+
174
+ // every page of the round has been queued: has it all arrived?
175
+ if (!connection.is_delivered(Lane.BULK, this.#mark)) return JoinProgress.Sending;
176
+
177
+ this.#collect_changed();
178
+ if (this.#count === 0) {
179
+ this.#send_page(frame, InitialSyncPage.FINAL);
180
+ this.peer.release_outbound(this.peer_id, frame + 1);
181
+ this.#phase = PHASE_FINAL_SENT;
182
+ return JoinProgress.Sending;
183
+ }
184
+ this.#round++;
185
+ if (this.#round > JOIN_MAX_ROUNDS) return JoinProgress.DidNotConverge;
186
+ this.#cursor = 0;
187
+ this.#phase = PHASE_PAGING;
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Fill and queue the next page from the cursor, or an empty one when `flags` asks for FINAL.
193
+ * @param {number} frame
194
+ * @param {number} flags
195
+ * @returns {number} bytes of entity records queued
196
+ */
197
+ #send_page(frame, flags) {
198
+ const buffer = this.page_buffer;
199
+ const slot_table = this.peer.slot_table;
200
+ buffer.position = 0;
201
+ buffer.writeUint32(0); // entity count, written at the end
202
+
203
+ let written = 0;
204
+ if ((flags & InitialSyncPage.FINAL) === 0) {
205
+ while (this.#cursor < this.#count) {
206
+ const entity_id = this.#entities[this.#cursor];
207
+ const network_id = slot_table.network_for(entity_id);
208
+ if (network_id < 0) {
209
+ // gone since the round began
210
+ this.#cursor++;
211
+ continue;
212
+ }
213
+ if (written > 0 && buffer.position >= JOIN_PAGE_BYTES) break;
214
+ snapshotter_write_entity(buffer, this.world, slot_table, this.peer.component_registry, entity_id);
215
+ this.#set_sent(network_id, frame);
216
+ written++;
217
+ this.#cursor++;
218
+ }
219
+ }
220
+
221
+ const end = buffer.position;
222
+ buffer.position = 0;
223
+ buffer.writeUint32(written);
224
+ buffer.position = end;
225
+
226
+ if (!this.#first_sent) {
227
+ flags |= InitialSyncPage.FIRST;
228
+ this.#first_sent = true;
229
+ }
230
+ this.#mark = this.peer.send_sync_page(this.peer_id, this.session_token, frame, flags, buffer.raw_bytes, end);
231
+ return end;
232
+ }
233
+
234
+ /**
235
+ * The next round's list: networked entities changed since their state was sent, or never sent.
236
+ */
237
+ #collect_changed() {
238
+ this.#count = 0;
239
+ this.for_each_entity(this.#push_if_changed);
240
+ }
241
+
242
+ /** @type {function(number): void} */
243
+ #push_entity = (entity_id) => {
244
+ if (this.#count === this.#entities.length) {
245
+ const next = new Int32Array(this.#entities.length * 2);
246
+ next.set(this.#entities);
247
+ this.#entities = next;
248
+ }
249
+ this.#entities[this.#count++] = entity_id;
250
+ };
251
+
252
+ /** @type {function(number): void} */
253
+ #push_if_changed = (entity_id) => {
254
+ const network_id = this.peer.slot_table.network_for(entity_id);
255
+ if (network_id < 0) return;
256
+ const sent = network_id < this.#sent_frame.length ? this.#sent_frame[network_id] : -1;
257
+ if (sent >= 0 && this.peer.last_changed_frame(network_id) <= sent) return;
258
+ this.#push_entity(entity_id);
259
+ };
260
+
261
+ #set_sent(network_id, frame) {
262
+ if (network_id >= this.#sent_frame.length) {
263
+ let size = this.#sent_frame.length * 2;
264
+ while (size <= network_id) size *= 2;
265
+ const next = new Float64Array(size).fill(-1);
266
+ next.set(this.#sent_frame);
267
+ this.#sent_frame = next;
268
+ }
269
+ this.#sent_frame[network_id] = frame;
270
+ }
271
+ }