@woosh/meep-engine 3.45.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 (435) hide show
  1. package/build/bundle-worker-terrain.js +1 -1
  2. package/editor/actions/concrete/TransformModifyAction.d.ts.map +1 -1
  3. package/editor/actions/concrete/TransformModifyAction.js +10 -2
  4. package/editor/ecs/component/FieldDescriptor.js +15 -0
  5. package/editor/ecs/component/createObjectEditor.js +41 -12
  6. package/editor/ecs/component/editors/ecs/Transform64Editor.d.ts.map +1 -1
  7. package/editor/ecs/component/editors/ecs/Transform64Editor.js +77 -70
  8. package/editor/tools/TransformToolV2.d.ts.map +1 -1
  9. package/editor/tools/TransformToolV2.js +4 -0
  10. package/editor/tools/v2/TransformControls.d.ts.map +1 -1
  11. package/editor/tools/v2/TransformControls.js +7 -0
  12. package/editor/view/ecs/EntityEditor.d.ts.map +1 -1
  13. package/editor/view/ecs/EntityEditor.js +4 -2
  14. package/package.json +1 -1
  15. package/src/core/assert.d.ts.map +1 -1
  16. package/src/core/assert.js +5 -1
  17. package/src/core/binary/hash/XXH128Hash.d.ts +1 -37
  18. package/src/core/binary/hash/XXH128Hash.d.ts.map +1 -1
  19. package/src/core/binary/hash/XXH128Hash.js +4 -55
  20. package/src/core/binary/hash/XXH3_SECRET.d.ts +1 -12
  21. package/src/core/binary/hash/XXH3_SECRET.d.ts.map +1 -1
  22. package/src/core/binary/hash/XXH3_SECRET.js +4 -25
  23. package/src/core/binary/hash/crc32.d.ts +1 -15
  24. package/src/core/binary/hash/crc32.d.ts.map +1 -1
  25. package/src/core/binary/hash/crc32.js +4 -96
  26. package/src/core/binary/hash/xxh3_128.d.ts +1 -9
  27. package/src/core/binary/hash/xxh3_128.d.ts.map +1 -1
  28. package/src/core/binary/hash/xxh3_128.js +4 -341
  29. package/src/core/binary/hash/xxh3_64.d.ts +1 -8
  30. package/src/core/binary/hash/xxh3_64.d.ts.map +1 -1
  31. package/src/core/binary/hash/xxh3_64.js +4 -241
  32. package/src/core/binary/hash/xxh3_common.d.ts +1 -223
  33. package/src/core/binary/hash/xxh3_common.d.ts.map +1 -1
  34. package/src/core/binary/hash/xxh3_common.js +90 -476
  35. package/src/core/collection/array/array_insert_at.d.ts +16 -0
  36. package/src/core/collection/array/array_insert_at.d.ts.map +1 -0
  37. package/src/core/collection/array/array_insert_at.js +32 -0
  38. package/src/core/collection/array/array_remove_at.d.ts +15 -0
  39. package/src/core/collection/array/array_remove_at.d.ts.map +1 -0
  40. package/src/core/collection/array/array_remove_at.js +30 -0
  41. package/src/core/collection/array/combine_hash.d.ts +6 -0
  42. package/src/core/collection/array/combine_hash.d.ts.map +1 -1
  43. package/src/core/collection/array/combine_hash.js +6 -0
  44. package/src/core/function/FunctionCompiler.js +2 -2
  45. package/src/core/geom/3d/mat4/m4_multiply.d.ts +3 -0
  46. package/src/core/geom/3d/mat4/m4_multiply.d.ts.map +1 -1
  47. package/src/core/geom/3d/mat4/m4_multiply.js +3 -0
  48. package/src/core/geom/3d/mat4/m4_multiply_f64.d.ts +30 -0
  49. package/src/core/geom/3d/mat4/m4_multiply_f64.d.ts.map +1 -0
  50. package/src/core/geom/3d/mat4/m4_multiply_f64.js +106 -0
  51. package/src/core/geom/3d/quaternion/quat3_look_rotation.d.ts +37 -0
  52. package/src/core/geom/3d/quaternion/quat3_look_rotation.d.ts.map +1 -0
  53. package/src/core/geom/3d/quaternion/quat3_look_rotation.js +172 -0
  54. package/src/core/geom/ConicRay.js +2 -2
  55. package/src/core/geom/Quaternion.d.ts +1 -0
  56. package/src/core/geom/Quaternion.d.ts.map +1 -1
  57. package/src/core/geom/Quaternion.js +12 -48
  58. package/src/core/geom/Vector4.d.ts.map +1 -1
  59. package/src/core/geom/Vector4.js +8 -5
  60. package/src/core/math/hash/XXH128Hash.d.ts +38 -0
  61. package/src/core/math/hash/XXH128Hash.d.ts.map +1 -0
  62. package/src/core/math/hash/XXH128Hash.js +55 -0
  63. package/src/core/math/hash/XXH3_SECRET.d.ts +13 -0
  64. package/src/core/math/hash/XXH3_SECRET.d.ts.map +1 -0
  65. package/src/core/math/hash/XXH3_SECRET.js +25 -0
  66. package/src/core/math/hash/crc32.d.ts +16 -0
  67. package/src/core/math/hash/crc32.d.ts.map +1 -0
  68. package/src/core/math/hash/crc32.js +96 -0
  69. package/src/core/math/hash/xxh3_128.d.ts +10 -0
  70. package/src/core/math/hash/xxh3_128.d.ts.map +1 -0
  71. package/src/core/math/hash/xxh3_128.js +341 -0
  72. package/src/core/math/hash/xxh3_64.d.ts +9 -0
  73. package/src/core/math/hash/xxh3_64.d.ts.map +1 -0
  74. package/src/core/math/hash/xxh3_64.js +241 -0
  75. package/src/core/math/hash/xxh3_common.d.ts +224 -0
  76. package/src/core/math/hash/xxh3_common.d.ts.map +1 -0
  77. package/src/core/math/hash/xxh3_common.js +476 -0
  78. package/src/core/math/lookup/ParameterLookupTable.js +2 -2
  79. package/src/core/model/node-graph/node/NodeInstancePortReference.js +2 -2
  80. package/src/core/model/reactive/model/ReactiveBinaryExpression.js +2 -2
  81. package/src/core/model/reactive/model/terminal/ReactiveReference.js +2 -2
  82. package/src/engine/Engine.d.ts.map +1 -1
  83. package/src/engine/Engine.js +9 -4
  84. package/src/engine/asset/AssetDescription.js +2 -2
  85. package/src/engine/ecs/animation/Animation.js +2 -2
  86. package/src/engine/ecs/animation/AnimationClip.d.ts.map +1 -1
  87. package/src/engine/ecs/animation/AnimationClip.js +7 -5
  88. package/src/engine/ecs/gui/GUIElement.d.ts.map +1 -1
  89. package/src/engine/ecs/gui/GUIElement.js +7 -5
  90. package/src/engine/ecs/gui/hud/HeadsUpDisplay.js +2 -2
  91. package/src/engine/ecs/gui/position/ViewportPosition.d.ts.map +1 -1
  92. package/src/engine/ecs/gui/position/ViewportPosition.js +11 -7
  93. package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
  94. package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +10 -0
  95. package/src/engine/ecs/transform/Transform64.d.ts +1 -1
  96. package/src/engine/ecs/transform/Transform64.d.ts.map +1 -1
  97. package/src/engine/ecs/transform/Transform64.js +5 -4
  98. package/src/engine/ecs/transform/t64_look_rotation.d.ts +4 -4
  99. package/src/engine/ecs/transform/t64_look_rotation.d.ts.map +1 -1
  100. package/src/engine/ecs/transform/t64_look_rotation.js +7 -18
  101. package/src/engine/graphics/ecs/animation/animator/AnimationClip.d.ts.map +1 -1
  102. package/src/engine/graphics/ecs/animation/animator/AnimationClip.js +8 -5
  103. package/src/engine/graphics/ecs/animation/animator/AnimationClipDefinition.js +2 -2
  104. package/src/engine/graphics/ecs/animation/animator/AnimationNotification.js +2 -2
  105. package/src/engine/graphics/ecs/animation/animator/AnimationNotificationDefinition.js +2 -2
  106. package/src/engine/graphics/ecs/animation/animator/graph/definition/AnimationGraphDefinition.js +2 -2
  107. package/src/engine/graphics/ecs/animation/animator/graph/definition/AnimationStateDefinition.js +2 -2
  108. package/src/engine/graphics/ecs/animation/animator/graph/definition/AnimationTransitionDefinition.d.ts.map +1 -1
  109. package/src/engine/graphics/ecs/animation/animator/graph/definition/AnimationTransitionDefinition.js +8 -5
  110. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
  111. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +30 -1
  112. package/src/engine/graphics/ecs/decal/v2/Decal.d.ts.map +1 -1
  113. package/src/engine/graphics/ecs/decal/v2/Decal.js +9 -8
  114. package/src/engine/graphics/render/frame_graph/resource/TextureResourceDescriptor.d.ts.map +1 -1
  115. package/src/engine/graphics/render/frame_graph/resource/TextureResourceDescriptor.js +9 -6
  116. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts +6 -0
  117. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts.map +1 -1
  118. package/src/engine/graphics3/GPUParticleEmitterSystem.js +72 -29
  119. package/src/engine/graphics3/ParticipatingMedia.js +204 -204
  120. package/src/engine/graphics3/animation/AnimationClips.d.ts +5 -0
  121. package/src/engine/graphics3/animation/AnimationClips.d.ts.map +1 -1
  122. package/src/engine/graphics3/animation/AnimationClips.js +5 -0
  123. package/src/engine/grid/obstacle/GridObstacle.js +2 -2
  124. package/src/engine/input/devices/KeyboardDevice.d.ts +1 -0
  125. package/src/engine/input/devices/KeyboardDevice.d.ts.map +1 -1
  126. package/src/engine/input/devices/KeyboardDevice.js +7 -6
  127. package/src/engine/intelligence/behavior/selector/WeightedElement.js +2 -2
  128. package/src/engine/interpolation/Interpolated.js +2 -2
  129. package/src/engine/navigation/ecs/path_following/PathFollowingSystem.d.ts.map +1 -1
  130. package/src/engine/navigation/ecs/path_following/PathFollowingSystem.js +15 -2
  131. package/src/engine/network/CONGESTION_CONTROL.md +9 -9
  132. package/src/engine/network/MESSAGE_TRANSPORT.md +364 -0
  133. package/src/engine/network/NetworkSession.d.ts +31 -11
  134. package/src/engine/network/NetworkSession.d.ts.map +1 -1
  135. package/src/engine/network/NetworkSession.js +453 -286
  136. package/src/engine/network/README.md +77 -27
  137. package/src/engine/network/SMOOTHNESS_DESIGN.md +13 -5
  138. package/src/engine/network/diagnostics/BandwidthMeter.d.ts +1 -1
  139. package/src/engine/network/diagnostics/BandwidthMeter.js +1 -1
  140. package/src/engine/network/orchestrator/HostJoin.d.ts +111 -0
  141. package/src/engine/network/orchestrator/HostJoin.d.ts.map +1 -0
  142. package/src/engine/network/orchestrator/HostJoin.js +271 -0
  143. package/src/engine/network/orchestrator/NetworkPeer.d.ts +151 -112
  144. package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
  145. package/src/engine/network/orchestrator/NetworkPeer.js +450 -499
  146. package/src/engine/network/orchestrator/PendingSlices.d.ts +30 -0
  147. package/src/engine/network/orchestrator/PendingSlices.d.ts.map +1 -0
  148. package/src/engine/network/orchestrator/PendingSlices.js +83 -0
  149. package/src/engine/network/orchestrator/ReliableFrames.d.ts +34 -0
  150. package/src/engine/network/orchestrator/ReliableFrames.d.ts.map +1 -0
  151. package/src/engine/network/orchestrator/ReliableFrames.js +96 -0
  152. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +77 -10
  153. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
  154. package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +236 -61
  155. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +20 -6
  156. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
  157. package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +33 -8
  158. package/src/engine/network/replication/Replicator.d.ts.map +1 -1
  159. package/src/engine/network/replication/Replicator.js +7 -1
  160. package/src/engine/network/sim/ActionLog.d.ts +32 -5
  161. package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
  162. package/src/engine/network/sim/ActionLog.js +47 -5
  163. package/src/engine/network/sim/SimActionExecutor.d.ts +42 -2
  164. package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
  165. package/src/engine/network/sim/SimActionExecutor.js +100 -4
  166. package/src/engine/network/sim/Snapshotter.d.ts +12 -0
  167. package/src/engine/network/sim/Snapshotter.d.ts.map +1 -1
  168. package/src/engine/network/sim/Snapshotter.js +43 -0
  169. package/src/engine/network/state/Baseline.d.ts +1 -1
  170. package/src/engine/network/state/Baseline.js +1 -1
  171. package/src/engine/network/state/FrameBufferRing.d.ts +25 -6
  172. package/src/engine/network/state/FrameBufferRing.d.ts.map +1 -1
  173. package/src/engine/network/state/FrameBufferRing.js +51 -6
  174. package/src/engine/network/state/InputRing.d.ts +4 -0
  175. package/src/engine/network/state/InputRing.d.ts.map +1 -1
  176. package/src/engine/network/state/InputRing.js +7 -0
  177. package/src/engine/network/transport/DeferredTransport.d.ts.map +1 -1
  178. package/src/engine/network/transport/DeferredTransport.js +1 -0
  179. package/src/engine/network/transport/LoopbackTransport.d.ts.map +1 -1
  180. package/src/engine/network/transport/LoopbackTransport.js +4 -2
  181. package/src/engine/network/transport/Transport.d.ts +7 -0
  182. package/src/engine/network/transport/Transport.d.ts.map +1 -1
  183. package/src/engine/network/transport/Transport.js +8 -0
  184. package/src/engine/network/transport/adapters/NodeUDPTransport.d.ts.map +1 -1
  185. package/src/engine/network/transport/adapters/NodeUDPTransport.js +20 -5
  186. package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.d.ts.map +1 -1
  187. package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.js +8 -0
  188. package/src/engine/network/transport/adapters/WebTransportTransport.d.ts.map +1 -1
  189. package/src/engine/network/transport/adapters/WebTransportTransport.js +6 -0
  190. package/src/engine/network/transport/connection/Connection.d.ts +191 -0
  191. package/src/engine/network/transport/connection/Connection.d.ts.map +1 -0
  192. package/src/engine/network/transport/connection/Connection.js +1262 -0
  193. package/src/engine/network/transport/connection/RangeSet.d.ts +94 -0
  194. package/src/engine/network/transport/connection/RangeSet.d.ts.map +1 -0
  195. package/src/engine/network/transport/connection/RangeSet.js +260 -0
  196. package/src/engine/network/transport/connection/RxStream.d.ts +48 -0
  197. package/src/engine/network/transport/connection/RxStream.d.ts.map +1 -0
  198. package/src/engine/network/transport/connection/RxStream.js +211 -0
  199. package/src/engine/network/transport/connection/TxStream.d.ts +90 -0
  200. package/src/engine/network/transport/connection/TxStream.d.ts.map +1 -0
  201. package/src/engine/network/transport/connection/TxStream.js +243 -0
  202. package/src/engine/network/transport/connection/bytes_copy.d.ts +17 -0
  203. package/src/engine/network/transport/connection/bytes_copy.d.ts.map +1 -0
  204. package/src/engine/network/transport/connection/bytes_copy.js +34 -0
  205. package/src/engine/network/transport/connection/varint.d.ts +46 -0
  206. package/src/engine/network/transport/connection/varint.d.ts.map +1 -0
  207. package/src/engine/network/transport/connection/varint.js +107 -0
  208. package/src/engine/physics/cloth/ecs/Cloth.d.ts.map +1 -1
  209. package/src/engine/physics/cloth/ecs/Cloth.js +7 -5
  210. package/src/engine/physics/cloth/ecs/ClothCollider.d.ts.map +1 -1
  211. package/src/engine/physics/cloth/ecs/ClothCollider.js +7 -5
  212. package/src/engine/physics/cloth/ecs/ClothDynamics.d.ts.map +1 -1
  213. package/src/engine/physics/cloth/ecs/ClothDynamics.js +20 -11
  214. package/src/engine/physics/fluid/FluidField.d.ts.map +1 -1
  215. package/src/engine/physics/fluid/FluidField.js +7 -5
  216. package/src/engine/physics/fluid/ecs/FluidComponent.d.ts.map +1 -1
  217. package/src/engine/physics/fluid/ecs/FluidComponent.js +19 -11
  218. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  219. package/src/engine/physics/fluid/effector/AbstractFluidEffector.d.ts +2 -2
  220. package/src/engine/physics/fluid/effector/AbstractFluidEffector.js +2 -2
  221. package/src/engine/physics/fluid/effector/GlobalFluidEffector.d.ts.map +1 -1
  222. package/src/engine/physics/fluid/effector/GlobalFluidEffector.js +11 -7
  223. package/src/engine/physics/fluid/effector/ImpulseFluidEffector.d.ts.map +1 -1
  224. package/src/engine/physics/fluid/effector/ImpulseFluidEffector.js +11 -7
  225. package/src/engine/physics/fluid/effector/WakeFluidEffector.d.ts.map +1 -1
  226. package/src/engine/physics/fluid/effector/WakeFluidEffector.js +16 -9
  227. package/src/engine/sound/ecs/audio/AudioEmitter.d.ts +38 -7
  228. package/src/engine/sound/ecs/audio/AudioEmitter.d.ts.map +1 -1
  229. package/src/engine/sound/ecs/audio/AudioEmitter.js +47 -14
  230. package/src/engine/sound/ecs/audio/AudioEmitterSerializationAdapter.d.ts.map +1 -1
  231. package/src/engine/sound/ecs/audio/AudioEmitterSerializationAdapter.js +5 -1
  232. package/src/engine/sound/ecs/audio/AudioEmitterSerializationUpgrader_0_1.d.ts +18 -0
  233. package/src/engine/sound/ecs/audio/AudioEmitterSerializationUpgrader_0_1.d.ts.map +1 -0
  234. package/src/engine/sound/ecs/audio/AudioEmitterSerializationUpgrader_0_1.js +48 -0
  235. package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts +12 -2
  236. package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts.map +1 -1
  237. package/src/engine/sound/ecs/audio/AudioEmitterSystem.js +173 -9
  238. package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts +9 -3
  239. package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts.map +1 -1
  240. package/src/engine/sound/ecs/audio/LiveEmitterSet.js +28 -8
  241. package/src/engine/sound/ecs/trigger/AudioEventTrigger.d.ts +8 -0
  242. package/src/engine/sound/ecs/trigger/AudioEventTrigger.d.ts.map +1 -1
  243. package/src/engine/sound/ecs/trigger/AudioEventTrigger.js +192 -191
  244. package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationAdapter.d.ts.map +1 -1
  245. package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationAdapter.js +5 -1
  246. package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationUpgrader_0_1.d.ts +18 -0
  247. package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationUpgrader_0_1.d.ts.map +1 -0
  248. package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationUpgrader_0_1.js +51 -0
  249. package/src/engine/sound/ecs/trigger/AudioEventTriggerSystem.d.ts.map +1 -1
  250. package/src/engine/sound/ecs/trigger/AudioEventTriggerSystem.js +15 -1
  251. package/src/engine/sound/simulation/core/AcousticMedium.js +2 -2
  252. package/src/engine/sound/simulation/definition/AcousticDirectivity.js +2 -2
  253. package/src/engine/sound/simulation/definition/AcousticMaterial.d.ts.map +1 -1
  254. package/src/engine/sound/simulation/definition/AcousticMaterial.js +11 -7
  255. package/src/engine/sound/sopra/README.md +768 -644
  256. package/src/engine/sound/sopra/SopraEngine.d.ts +104 -8
  257. package/src/engine/sound/sopra/SopraEngine.d.ts.map +1 -1
  258. package/src/engine/sound/sopra/SopraEngine.js +327 -12
  259. package/src/engine/sound/sopra/definition/BusDefinition.d.ts.map +1 -1
  260. package/src/engine/sound/sopra/definition/BusDefinition.js +5 -4
  261. package/src/engine/sound/sopra/definition/DuckingRule.d.ts.map +1 -1
  262. package/src/engine/sound/sopra/definition/DuckingRule.js +7 -5
  263. package/src/engine/sound/sopra/definition/EventDescription.d.ts +28 -2
  264. package/src/engine/sound/sopra/definition/EventDescription.d.ts.map +1 -1
  265. package/src/engine/sound/sopra/definition/EventDescription.js +77 -17
  266. package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.d.ts +10 -0
  267. package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.d.ts.map +1 -1
  268. package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.js +77 -55
  269. package/src/engine/sound/sopra/definition/EventDescriptionSerializationUpgrader_0_1.d.ts +31 -0
  270. package/src/engine/sound/sopra/definition/EventDescriptionSerializationUpgrader_0_1.d.ts.map +1 -0
  271. package/src/engine/sound/sopra/definition/EventDescriptionSerializationUpgrader_0_1.js +75 -0
  272. package/src/engine/sound/sopra/definition/MixerSnapshot.js +2 -2
  273. package/src/engine/sound/sopra/definition/ParameterDefinition.d.ts.map +1 -1
  274. package/src/engine/sound/sopra/definition/ParameterDefinition.js +7 -5
  275. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.js +2 -2
  276. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.d.ts.map +1 -1
  277. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.js +4 -3
  278. package/src/engine/sound/sopra/definition/clip/ContainerAudioClip.js +2 -2
  279. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.js +3 -3
  280. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.d.ts.map +1 -1
  281. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.js +16 -9
  282. package/src/engine/sound/sopra/definition/clip/SilenceAudioClip.js +2 -2
  283. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts +1 -1
  284. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts.map +1 -1
  285. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.js +2 -2
  286. package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.d.ts +38 -9
  287. package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.d.ts.map +1 -1
  288. package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.js +34 -5
  289. package/src/engine/sound/sopra/definition/effect/CompressorEffect.d.ts +7 -2
  290. package/src/engine/sound/sopra/definition/effect/CompressorEffect.d.ts.map +1 -1
  291. package/src/engine/sound/sopra/definition/effect/CompressorEffect.js +137 -120
  292. package/src/engine/sound/sopra/definition/effect/EqEffect.d.ts +3 -1
  293. package/src/engine/sound/sopra/definition/effect/EqEffect.d.ts.map +1 -1
  294. package/src/engine/sound/sopra/definition/effect/EqEffect.js +146 -128
  295. package/src/engine/sound/sopra/definition/effect/ReverbEffect.d.ts +1 -0
  296. package/src/engine/sound/sopra/definition/effect/ReverbEffect.d.ts.map +1 -1
  297. package/src/engine/sound/sopra/definition/effect/ReverbEffect.js +8 -3
  298. package/src/engine/sound/sopra/legacy/SoundEmitter.d.ts.map +1 -1
  299. package/src/engine/sound/sopra/legacy/SoundEmitter.js +357 -352
  300. package/src/engine/sound/sopra/legacy/SoundTrack.d.ts.map +1 -1
  301. package/src/engine/sound/sopra/legacy/SoundTrack.js +281 -278
  302. package/src/engine/sound/sopra/runtime/BusGraph.d.ts +44 -2
  303. package/src/engine/sound/sopra/runtime/BusGraph.d.ts.map +1 -1
  304. package/src/engine/sound/sopra/runtime/BusGraph.js +83 -43
  305. package/src/engine/sound/sopra/runtime/EffectChain.d.ts +96 -0
  306. package/src/engine/sound/sopra/runtime/EffectChain.d.ts.map +1 -0
  307. package/src/engine/sound/sopra/runtime/EffectChain.js +286 -0
  308. package/src/engine/sound/sopra/runtime/EffectParamOverrides.d.ts +123 -0
  309. package/src/engine/sound/sopra/runtime/EffectParamOverrides.d.ts.map +1 -0
  310. package/src/engine/sound/sopra/runtime/EffectParamOverrides.js +320 -0
  311. package/src/engine/sound/sopra/runtime/EventInstance.d.ts +55 -5
  312. package/src/engine/sound/sopra/runtime/EventInstance.d.ts.map +1 -1
  313. package/src/engine/sound/sopra/runtime/EventInstance.js +201 -26
  314. package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.d.ts +2 -1
  315. package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.d.ts.map +1 -1
  316. package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.js +6 -1
  317. package/src/engine/sound/sopra/serialization/sopraSerializationHarness.d.ts +15 -0
  318. package/src/engine/sound/sopra/serialization/sopraSerializationHarness.d.ts.map +1 -1
  319. package/src/engine/sound/sopra/serialization/sopraSerializationHarness.js +32 -0
  320. package/src/engine/sound/sopra/util/MockAudioContext.d.ts.map +1 -1
  321. package/src/engine/sound/sopra/util/MockAudioContext.js +3 -0
  322. package/src/engine/sound/sopra/util/fadeOutAndStop.d.ts +13 -3
  323. package/src/engine/sound/sopra/util/fadeOutAndStop.d.ts.map +1 -1
  324. package/src/engine/sound/sopra/util/fadeOutAndStop.js +25 -13
  325. package/src/format/image/png/PNGReader.js +644 -644
  326. package/src/format/image/png/crc32.d.ts +1 -1
  327. package/src/format/image/png/crc32.js +7 -8
  328. package/src/shade/RENDERER_CONTRACT.md +6 -3
  329. package/src/shade/descriptor/binding/BindGroupLayoutDescriptor.js +2 -2
  330. package/src/shade/descriptor/binding/BindGroupLayoutEntry.d.ts.map +1 -1
  331. package/src/shade/descriptor/binding/BindGroupLayoutEntry.js +12 -8
  332. package/src/shade/descriptor/binding/StorageTextureBindingLayout.js +2 -2
  333. package/src/shade/descriptor/binding/TextureBindingLayout.js +2 -2
  334. package/src/shade/descriptor/blend/BlendComponent.js +2 -2
  335. package/src/shade/descriptor/pipeline/PipelineDescriptorBase.js +2 -2
  336. package/src/shade/descriptor/pipeline/PipelineLayoutDescriptor.js +2 -2
  337. package/src/shade/descriptor/pipeline/ProgrammableStage.js +2 -2
  338. package/src/shade/descriptor/pipeline/compute/ComputePipelineDescriptor.js +2 -2
  339. package/src/shade/descriptor/pipeline/render/ColorTargetState.js +2 -2
  340. package/src/shade/descriptor/pipeline/render/FragmentState.js +2 -2
  341. package/src/shade/descriptor/pipeline/render/PrimitiveState.js +2 -2
  342. package/src/shade/descriptor/pipeline/render/RenderPipelineDescriptor.d.ts.map +1 -1
  343. package/src/shade/descriptor/pipeline/render/RenderPipelineDescriptor.js +8 -5
  344. package/src/shade/descriptor/pipeline/render/VertexAttribute.js +2 -2
  345. package/src/shade/descriptor/texture/SamplerDescriptor.d.ts.map +1 -1
  346. package/src/shade/descriptor/texture/SamplerDescriptor.js +20 -12
  347. package/src/shade/descriptor/texture/TextureDescriptor.d.ts.map +1 -1
  348. package/src/shade/descriptor/texture/TextureDescriptor.js +11 -7
  349. package/src/shade/device/SHADE_DEVICE_FLOOR.d.ts +24 -0
  350. package/src/shade/device/SHADE_DEVICE_FLOOR.d.ts.map +1 -0
  351. package/src/shade/device/SHADE_DEVICE_FLOOR.js +34 -0
  352. package/src/shade/device/ShadeDeviceFailure.d.ts +12 -5
  353. package/src/shade/device/ShadeDeviceFailure.d.ts.map +1 -1
  354. package/src/shade/device/ShadeDeviceFailure.js +17 -8
  355. package/src/shade/device/ShadeDeviceFailureReason.d.ts +1 -1
  356. package/src/shade/device/ShadeDeviceFailureReason.js +2 -2
  357. package/src/shade/device/graph/BufferResourceDescriptor.d.ts.map +1 -1
  358. package/src/shade/device/graph/BufferResourceDescriptor.js +196 -193
  359. package/src/shade/device/graph/TextureResourceDescriptor.d.ts.map +1 -1
  360. package/src/shade/device/graph/TextureResourceDescriptor.js +178 -173
  361. package/src/shade/device/shade_device_floor_unmet.d.ts +13 -0
  362. package/src/shade/device/shade_device_floor_unmet.d.ts.map +1 -0
  363. package/src/shade/device/shade_device_floor_unmet.js +32 -0
  364. package/src/shade/device/timing/profile/sgpt_read_header.js +1 -1
  365. package/src/shade/device/timing/profile/sgpt_read_records.js +1 -1
  366. package/src/shade/device/timing/profile/sgpt_write_header.js +1 -1
  367. package/src/shade/device/timing/profile/sgpt_write_record.js +1 -1
  368. package/src/shade/renderer/Renderer.d.ts +2 -0
  369. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  370. package/src/shade/renderer/Renderer.js +29 -55
  371. package/src/shade/renderer/binding/BufferBinding.js +2 -2
  372. package/src/shade/renderer/buffer/GPUBufferAllocator.d.ts.map +1 -1
  373. package/src/shade/renderer/buffer/GPUBufferAllocator.js +9 -0
  374. package/src/shade/renderer/buffer/buffer_pool_ops.d.ts +21 -12
  375. package/src/shade/renderer/buffer/buffer_pool_ops.d.ts.map +1 -1
  376. package/src/shade/renderer/buffer/buffer_pool_ops.js +18 -19
  377. package/src/shade/renderer/camera/Camera.js +2 -2
  378. package/src/shade/renderer/geometry/Attribute.js +2 -2
  379. package/src/shade/renderer/geometry/Geometry.js +2 -2
  380. package/src/shade/renderer/geometry/geometry_deduplicate_vertices.js +2 -2
  381. package/src/shade/renderer/geometry/meshlet/MeshletBatch.js +2 -2
  382. package/src/shade/renderer/geometry/virtual/format/checksum/vgeo_compute_asset_id.js +13 -13
  383. package/src/shade/renderer/geometry/virtual/format/checksum/vgeo_compute_directory_block_checksum.js +41 -41
  384. package/src/shade/renderer/geometry/virtual/format/checksum/vgeo_compute_frame_checksum.js +41 -41
  385. package/src/shade/renderer/geometry/virtual/format/checksum/vgeo_compute_header_checksum.js +25 -25
  386. package/src/shade/renderer/gpu_primitive/test/gpu_test.d.ts.map +1 -1
  387. package/src/shade/renderer/gpu_primitive/test/gpu_test.js +8 -11
  388. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  389. package/src/shade/renderer/material/StandardShadeMaterial.d.ts.map +1 -1
  390. package/src/shade/renderer/material/StandardShadeMaterial.js +13 -8
  391. package/src/shade/renderer/pool/TransientResourcePool.d.ts +24 -0
  392. package/src/shade/renderer/pool/TransientResourcePool.d.ts.map +1 -1
  393. package/src/shade/renderer/pool/TransientResourcePool.js +41 -10
  394. package/src/shade/renderer/scene/optimization/geometry/internal/triangle_fingerprint_stream.d.ts.map +1 -1
  395. package/src/shade/renderer/scene/optimization/geometry/internal/triangle_fingerprint_stream.js +14 -5
  396. package/src/shade/renderer/scene/optimization/sampler2d_from_image_bitmap.js +1 -1
  397. package/src/shade/renderer/shader/ComputeShader.d.ts.map +1 -1
  398. package/src/shade/renderer/shader/ComputeShader.js +18 -2
  399. package/src/shade/renderer/shader/ImageShader.d.ts.map +1 -1
  400. package/src/shade/renderer/shader/ImageShader.js +230 -218
  401. package/src/shade/renderer/shader/type/WGSLStructField.js +2 -2
  402. package/src/shade/renderer/shader/type/WebGPUArray.js +2 -2
  403. package/src/shade/renderer/shader/type/WebGPUStruct.d.ts.map +1 -1
  404. package/src/shade/renderer/shader/type/WebGPUStruct.js +9 -6
  405. package/src/shade/renderer/texture/ShadeTexture.d.ts.map +1 -1
  406. package/src/shade/renderer/texture/ShadeTexture.js +17 -10
  407. package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.js +2 -2
  408. package/src/shade/renderer/water/surface/chunk_water_surface_sample.d.ts +4 -4
  409. package/src/shade/renderer/water/surface/chunk_water_surface_sample.js +6 -6
  410. package/src/shade/renderer/water/surface/particles/GPUWaterParticles.d.ts.map +1 -1
  411. package/src/shade/renderer/water/surface/particles/GPUWaterParticles.js +46 -55
  412. package/src/shade/renderer/water/surface/particles/WATER_PARTICLE_STRUCT.d.ts +3 -3
  413. package/src/shade/renderer/water/surface/particles/WATER_PARTICLE_STRUCT.d.ts.map +1 -1
  414. package/src/shade/renderer/water/surface/particles/WATER_PARTICLE_STRUCT.js +14 -4
  415. package/src/shade/renderer/water/surface/particles/chunk_water_particle.d.ts +10 -1
  416. package/src/shade/renderer/water/surface/particles/chunk_water_particle.d.ts.map +1 -1
  417. package/src/shade/renderer/water/surface/particles/chunk_water_particle.js +82 -40
  418. package/src/shade/renderer/water/surface/particles/chunk_water_particle_step.d.ts +4 -1
  419. package/src/shade/renderer/water/surface/particles/chunk_water_particle_step.d.ts.map +1 -1
  420. package/src/shade/renderer/water/surface/particles/chunk_water_particle_step.js +8 -4
  421. package/src/shade/renderer/water/surface/particles/shader_water_particles.d.ts +98 -86
  422. package/src/shade/renderer/water/surface/particles/shader_water_particles.d.ts.map +1 -1
  423. package/src/shade/renderer/water/surface/particles/shader_water_particles.js +440 -298
  424. package/src/view/common/HTMLElementCacheKey.d.ts.map +1 -1
  425. package/src/view/common/HTMLElementCacheKey.js +4 -3
  426. package/src/view/graphics/GraphicsContextFailureView.d.ts +1 -2
  427. package/src/view/graphics/GraphicsContextFailureView.d.ts.map +1 -1
  428. package/src/view/graphics/GraphicsContextFailureView.js +1 -2
  429. package/src/view/minimap/MinimapMarker.js +2 -2
  430. package/src/engine/network/transport/Channel.js +0 -336
  431. package/src/engine/network/transport/ReliableCommandPipeline.js +0 -335
  432. package/src/engine/network/transport/fragments/FragmentAssembler.js +0 -350
  433. package/src/engine/network/transport/fragments/FragmentRetention.js +0 -191
  434. package/src/engine/network/transport/fragments/fragment_send.js +0 -157
  435. package/src/engine/network/transport/fragments/packet_size.js +0 -107
@@ -9,7 +9,8 @@ import { NetworkIdentity } from "./ecs/components/NetworkIdentity.js";
9
9
  import { make_owner_authorization } from "./ecs/owner_authorization.js";
10
10
  import { NetworkSystem } from "./ecs/NetworkSystem.js";
11
11
  import { NetworkIdentitySerializationAdapter, } from "./ecs/serialization/NetworkIdentitySerializationAdapter.js";
12
- import { NetworkPeer, ResumeRejectReason, } from "./orchestrator/NetworkPeer.js";
12
+ import { InitialSyncPage, NetworkPeer, ResumeRejectReason, } from "./orchestrator/NetworkPeer.js";
13
+ import { HostJoin, JoinProgress } from "./orchestrator/HostJoin.js";
13
14
  import { ServerAuthoritativeClient } from "./orchestrator/ServerAuthoritativeClient.js";
14
15
  import { LateInput, ServerAuthoritativeServer } from "./orchestrator/ServerAuthoritativeServer.js";
15
16
  import { OwnerAwareScope } from "./replication/ScopeFilter.js";
@@ -19,7 +20,6 @@ import { Interpolated, INTERPOLATION_SOURCE_LOCAL } from "../interpolation/Inter
19
20
  import { InterpolationLog } from "../interpolation/InterpolationLog.js";
20
21
  import { InterpolationSystem } from "../interpolation/InterpolationSystem.js";
21
22
  import { SimAction } from "./sim/SimAction.js";
22
- import { snapshotter_emit } from "./sim/Snapshotter.js";
23
23
  import { PredictionHistory } from "./state/PredictionHistory.js";
24
24
  import { AdaptiveRenderDelay } from "./time/AdaptiveRenderDelay.js";
25
25
  import { RenderPlayout } from "./time/RenderPlayout.js";
@@ -143,13 +143,15 @@ const ReconnectState = Object.freeze({
143
143
  /**
144
144
  * Client-side: how the host has answered the transport binding in use. A new binding does not know
145
145
  * which world the client is in until the host says: the one it has been predicting on, which the
146
- * host resumes (RESUME_ACCEPT), or the host's own, sent as a snapshot (INITIAL_SYNC). The first
147
- * answer is the answer; the host's handshake arriving after it is stale.
146
+ * host resumes (RESUME_ACCEPT), or the host's own, sent as pages of a snapshot (INITIAL_SYNC). The
147
+ * first answer is the answer; the host's handshake arriving after it is stale. The first page is
148
+ * the answer (Syncing); the FINAL page completes it (Synced).
148
149
  * @readonly @enum {string}
149
150
  */
150
151
  const BindingAnswer = Object.freeze({
151
152
  Pending: 'pending',
152
153
  Resumed: 'resumed',
154
+ Syncing: 'syncing',
153
155
  Synced: 'synced',
154
156
  });
155
157
 
@@ -229,6 +231,18 @@ function assert_copies_itself(method, name, ComponentClass) {
229
231
  * @param {number} length
230
232
  * @returns {boolean}
231
233
  */
234
+ /**
235
+ * Close a transport the session is giving up on. Best effort: one already closed, or failing to
236
+ * close, changes nothing about what follows.
237
+ * @param {{disconnect?: function(): void}} transport
238
+ */
239
+ function close_transport(transport) {
240
+ try {
241
+ transport.disconnect();
242
+ } catch (_) { /* swallow */
243
+ }
244
+ }
245
+
232
246
  function bytes_equal(a, b, length) {
233
247
  for (let i = 0; i < length; i++) {
234
248
  if (a[i] !== b[i]) return false;
@@ -255,8 +269,17 @@ function bytes_equal_lengths(a, a_length, b, b_length) {
255
269
  *
256
270
  * Replication is component-driven: attaching a {@link NetworkIdentity}
257
271
  * to an entity registers it with the session via an `EntityObserver`.
258
- * Mutation is event-driven: callers fire
259
- * `dataset.sendEvent(entity, "net_mutate_component", payload)`.
272
+ *
273
+ * **What travels is what goes through the action executor**, and nothing
274
+ * else: actions (`send`, a client's input), and direct writes their writer
275
+ * reports — `dataset.sendEvent(entity, "net_mutate_component", payload)`.
276
+ * The session never looks for changes by itself. That is what keeps a tick's
277
+ * cost in what changed and what is owned, never in the size of the world;
278
+ * the stack is built for worlds of millions of networked objects. A
279
+ * replicated component written directly and not reported stays where it was
280
+ * written. Ownership is set when an identity attaches, or by the join, and
281
+ * `owner_peer_id` / `network_id` are not written afterwards. The README's
282
+ * "What the session assumes" has the whole contract and its measured costs.
260
283
  *
261
284
  * **The session runs on the engine's clock.** The `EntityManager`'s fixed
262
285
  * step is the network tick: each step opens a network frame before any
@@ -329,6 +352,11 @@ export class NetworkSession {
329
352
  #client = null;
330
353
  /** @private @type {NetworkPeer|null} */
331
354
  #peer = null;
355
+ /**
356
+ * The session's place in the engine loop, added by `start()` and removed by `stop()`.
357
+ * @private @type {NetworkSystem|null}
358
+ */
359
+ #network_system = null;
332
360
  /** @private @type {EntityObserver|null} */
333
361
  #identity_observer = null;
334
362
  /** @private @type {InterpolationLog|null} */
@@ -352,6 +380,22 @@ export class NetworkSession {
352
380
  */
353
381
  #remote_entities = new Set();
354
382
 
383
+ /**
384
+ * Networked entities by owner: peer id → its entities. What a tick does per owned entity —
385
+ * the host's AUTH_STATE to each peer, the client's record of what it predicted — walks this,
386
+ * never every networked entity: the cost of a tick is in what is owned and what changed, not
387
+ * in the size of the world. Kept exact by attach, detach and {@link #apply_ownership}, the
388
+ * only places ownership is set.
389
+ * @private @type {Map<number, Set<number>>}
390
+ */
391
+ #owned_by = new Map();
392
+
393
+ /**
394
+ * Owner each networked entity is filed under in {@link #owned_by}.
395
+ * @private @type {Map<number, number>}
396
+ */
397
+ #owner_of = new Map();
398
+
355
399
  // ---- Render-frame interpolation state ----
356
400
 
357
401
  /**
@@ -416,26 +460,11 @@ export class NetworkSession {
416
460
  */
417
461
  #inbound = new Map();
418
462
 
419
- // ---- Host: publishing what the simulation changed ----
420
-
421
- /** @private @type {boolean} */
422
- #publish_changes;
463
+ // ---- Host ----
423
464
 
424
465
  /** @private @type {string} */
425
466
  #late_input;
426
467
 
427
- /**
428
- * What the peers hold for each networked entity, per replicated component type id, as the
429
- * replication adapter's bytes — so "changed" means "changed on the wire". Updated by every
430
- * record the host publishes and by every action it executes (the peers apply the action and
431
- * arrive at the same value), and compared with the live state at the end of each step.
432
- * @private @type {Map<number, Array<{bytes: Uint8Array, length: number}|null>>}
433
- */
434
- #published = new Map();
435
-
436
- /** @private @type {BinaryBuffer} */
437
- #scratch_publish_buf = new BinaryBuffer();
438
-
439
468
  // ---- Client: reconciliation by comparison ----
440
469
 
441
470
  /** @private @type {string} */
@@ -506,19 +535,21 @@ export class NetworkSession {
506
535
  #connected_peers = new Set();
507
536
 
508
537
  /**
509
- * Host-only: peer_id → remaining-resends. Drained in
510
- * `#on_host_tick_complete` once `current_sim_frame >= 0`. Sent twice
511
- * for loss tolerance, and never queued again on the peer's say-so: each
512
- * send serializes the whole world, and a sync the peer could ask for is one
513
- * it could keep asking for. Filled only by {@link #queue_initial_sync}.
514
- * (A fragment of one that goes missing is re-sent on the receiver's
515
- * NACK_FRAGMENT. That repair is `NetworkPeer`'s: a NACK re-sends each
516
- * chunk of the sync at most once, for at most
517
- * `FRAGMENT_RETENTION_MAX_RETRIES` NACKs, so it adds at most that many
518
- * copies of each sync, however the peer asks.)
519
- * @private @type {Map<number, number>}
538
+ * Host-only: peer_id → its join, while one is going on. Advanced in
539
+ * `#on_host_tick_complete`; created only by {@link #queue_initial_sync}, so never on the
540
+ * peer's say-so: a join the peer could ask for is one it could keep asking for. The pages ride
541
+ * the connection's BULK lane, which re-sends what is lost; what a join costs beyond the world
542
+ * once is the entities that changed while it was going out, for at most
543
+ * {@link JOIN_MAX_ROUNDS} rounds.
544
+ * @private @type {Map<number, HostJoin>}
545
+ */
546
+ #joins = new Map();
547
+
548
+ /**
549
+ * Host-only: scratch every join builds its pages in.
550
+ * @private @type {BinaryBuffer}
520
551
  */
521
- #pending_initial_sync = new Map();
552
+ #page_buffer = new BinaryBuffer();
522
553
 
523
554
  /**
524
555
  * Host-only: peers whose current connection has been given its
@@ -541,6 +572,13 @@ export class NetworkSession {
541
572
  /** @private @type {boolean} */
542
573
  #started = false;
543
574
 
575
+ /**
576
+ * {@link stop} has run. A session is one match: what it knew of its peers outlives the stop, so
577
+ * it does not start again.
578
+ * @private @type {boolean}
579
+ */
580
+ #stopped = false;
581
+
544
582
  // ---- Host-side: per-peer session token + grace state ----
545
583
 
546
584
  /**
@@ -613,14 +651,36 @@ export class NetworkSession {
613
651
  /**
614
652
  * Client-side: how the host has answered the binding the last `connect()`
615
653
  * made. Once it has, what the binding carries about the handshake is news
616
- * no longer: the host sends each sync more than once against loss, and a
617
- * host that has finished with our old session takes the new transport for
618
- * a fresh peer and sends its sync before reading the RESUME_HELLO the
619
- * transport carried, so the RESUME_REJECT it answers that with arrives
620
- * after the resync it asks for.
654
+ * no longer: a host that has finished with our old session takes the new
655
+ * transport for a fresh peer and starts its join before reading the
656
+ * RESUME_HELLO the transport carried, so the RESUME_REJECT it answers that
657
+ * with arrives after the resync it asks for.
621
658
  * @private @type {BindingAnswer|string}
622
659
  */
623
660
  #binding_answer = BindingAnswer.Pending;
661
+
662
+ /**
663
+ * Client: the newest sim frame the host has named on this binding — every TIME_DILATION
664
+ * carries it — or -1. A join's pages can take a while to arrive, and the frame a page names
665
+ * is as old as its trip; this is the freshest word of where the host is.
666
+ * @private @type {number}
667
+ */
668
+ #host_frame_seen = -1;
669
+
670
+ /**
671
+ * Client: the peer whose held inbound — action stream and AUTH_STATE, held from `connect()`
672
+ * until the binding's answer is whole — is to be released as the step's inbound is done, or -1.
673
+ * Released there rather than inside the answer's own dispatch: the release applies what was
674
+ * held, through the same receive buffer the answer is still being read from.
675
+ * @private @type {number}
676
+ */
677
+ #inbound_release_peer = -1;
678
+
679
+ /**
680
+ * Client: whether that release completes a join, so {@link onSynced} follows it.
681
+ * @private @type {boolean}
682
+ */
683
+ #inbound_release_syncs = false;
624
684
  /**
625
685
  * Client-side: the first frame predicted on the world this client holds —
626
686
  * where the last INITIAL_SYNC it applied set the frame counter, 0 before
@@ -671,6 +731,14 @@ export class NetworkSession {
671
731
  */
672
732
  onReconnected = new Signal();
673
733
 
734
+ /**
735
+ * Client: a join has completed — the host's FINAL page has landed, every networked entity's
736
+ * state is here, and from now on this client's input is sent. No arguments. A world is sent
737
+ * as pages over as many ticks as its size takes; until this fires it is still arriving.
738
+ * @type {Signal}
739
+ */
740
+ onSynced = new Signal();
741
+
674
742
  /**
675
743
  * Client: the reconnect ladder is exhausted, or the host explicitly
676
744
  * kicked us with a DISCONNECT packet, or `reconnect.enabled` is
@@ -762,12 +830,6 @@ export class NetworkSession {
762
830
  * - `reconnect` (client role): policy for the reconnect ladder. See
763
831
  * {@link DEFAULT_RECONNECT_POLICY}. Set `enabled: false` to opt
764
832
  * out entirely.
765
- * - `publish_changes` (host role, default true): at the end of every
766
- * step, send each replicated component the simulation changed — physics,
767
- * controllers, anything that wrote it directly. Components an action
768
- * wrote are already on the wire and are not sent twice. Publishing never
769
- * writes back into the host's own state. Set false to publish by hand
770
- * (`net_mutate_component`).
771
833
  * - `reconcile` (client role): see {@link NetworkSessionReconcile}. Default
772
834
  * `'correct'`.
773
835
  * - `late_input` (host role): what to do with client input that arrives
@@ -791,7 +853,6 @@ export class NetworkSession {
791
853
  server_resume_grace_ms = DEFAULT_SERVER_RESUME_GRACE_MS,
792
854
  connection_timeout_ms = DEFAULT_CONNECTION_TIMEOUT_MS,
793
855
  reconnect = null,
794
- publish_changes = true,
795
856
  late_input = LateInput.Apply,
796
857
  reconcile = NetworkSessionReconcile.Correct,
797
858
  } = {}) {
@@ -904,7 +965,6 @@ export class NetworkSession {
904
965
  this.#connection_timeout_ms = connection_timeout_ms;
905
966
  this.#reconnect_policy = Object.freeze({ ...DEFAULT_RECONNECT_POLICY, ...(reconnect || {}) });
906
967
  this.#transport_factory = transport_factory;
907
- this.#publish_changes = publish_changes;
908
968
  this.#late_input = late_input;
909
969
  if (reconcile !== NetworkSessionReconcile.Correct && reconcile !== NetworkSessionReconcile.Replay) {
910
970
  throw new Error(`NetworkSession: reconcile must be one of ${Object.values(NetworkSessionReconcile).join(', ')}, got '${reconcile}'`);
@@ -1092,7 +1152,7 @@ export class NetworkSession {
1092
1152
  /**
1093
1153
  * Wire the session to the engine. After this, the EntityObserver
1094
1154
  * is active, the right orchestrator is constructed, and a
1095
- * {@link NetworkSystem} is attached. Subsequent `replicate_*`
1155
+ * {@link NetworkSystem} is attached until {@link stop}. Subsequent `replicate_*`
1096
1156
  * / {@link defineAction} / {@link defineInputSampler} calls throw.
1097
1157
  *
1098
1158
  * Pre-condition: every replicated component class must already have a
@@ -1101,6 +1161,7 @@ export class NetworkSession {
1101
1161
  */
1102
1162
  async start() {
1103
1163
  if (this.#started) throw new Error("NetworkSession.start: already started");
1164
+ if (this.#stopped) throw new Error("NetworkSession.start: a stopped session does not start again; the next match is a new NetworkSession");
1104
1165
 
1105
1166
  // Auto-register the NetworkIdentity adapter so initial-sync can
1106
1167
  // ship it across the wire. Allow caller-supplied registrations
@@ -1156,6 +1217,11 @@ export class NetworkSession {
1156
1217
  replicated_components,
1157
1218
  action_classes,
1158
1219
  frame_capacity: this.frame_capacity,
1220
+ // The session's one clock: render-paced, so the fixed steps one update runs all see
1221
+ // the same time, and a burst of catch-up steps cannot age packets whose
1222
+ // acknowledgements could not have arrived yet.
1223
+ clock: () => this.#clock_ms,
1224
+ tick_period_ms: this.tick_period_ms,
1159
1225
  };
1160
1226
 
1161
1227
  if (this.role === NetworkSessionRole.Host) {
@@ -1171,9 +1237,6 @@ export class NetworkSession {
1171
1237
  identity_class: NetworkIdentity,
1172
1238
  });
1173
1239
  this.#server.onTickComplete.add((sim_frame) => this.#on_host_tick_complete(sim_frame));
1174
- if (this.#publish_changes) {
1175
- this.#server.onBeforeTickClose.add(() => this.#publish_simulated_changes());
1176
- }
1177
1240
  } else {
1178
1241
  // Client role: covers both "playing" (input sampler registered)
1179
1242
  // and "spectator" (no sampler — sampler-driven onPredict
@@ -1204,13 +1267,6 @@ export class NetworkSession {
1204
1267
  // blended component on the first execute after a render pass.
1205
1268
  this.#peer.executor.before_execute = this.#bound_normalize_if_dirty;
1206
1269
 
1207
- if (this.role === NetworkSessionRole.Host && this.#publish_changes) {
1208
- // an action carries its own change to the peers: what it wrote is what they will hold
1209
- this.#peer.executor.on_component_written = (entity_id, component_class) => {
1210
- this.#note_published(entity_id, component_class);
1211
- };
1212
- }
1213
-
1214
1270
  // Secure-by-default inbound authorization on the host: a remote client
1215
1271
  // may only mutate entities it owns (NetworkIdentity.owner_peer_id ===
1216
1272
  // sender); the authoritative server (sender === local_peer_id) may
@@ -1234,11 +1290,12 @@ export class NetworkSession {
1234
1290
  //
1235
1291
  // It is also the session's place in the engine loop: each fixed step opens a network frame
1236
1292
  // and closes it, and every rendered frame runs the session's wall-paced work.
1237
- await this.entity_manager.addSystem(new NetworkSystem(this.#peer, {
1293
+ this.#network_system = new NetworkSystem(this.#peer, {
1238
1294
  open_step: () => this.#open_step(),
1239
1295
  close_step: () => this.#close_step(),
1240
1296
  frame: (dt) => this.#on_frame(dt),
1241
- }));
1297
+ });
1298
+ await this.entity_manager.addSystem(this.#network_system);
1242
1299
 
1243
1300
  // The network timeline: per-frame state of every remote entity, blended at render time on
1244
1301
  // the render-delayed playout by the engine's InterpolationSystem.
@@ -1267,65 +1324,32 @@ export class NetworkSession {
1267
1324
  // action-stream processing, and finalize any in-flight Tier-3
1268
1325
  // resync state.
1269
1326
  if (this.role === NetworkSessionRole.Client) {
1270
- this.#peer.onInitialSync.add((peer_id, session_token, frame_number, buf, payload_end) => {
1271
- if (this.#binding_answer === BindingAnswer.Synced && this.#reconnect_state !== ReconnectState.Resyncing) {
1272
- // A copy: the host sends every sync more than once, against loss, and this
1273
- // binding already has its world. The client has been predicting on it since;
1274
- // applied again, it would put what the client owns back behind input already
1275
- // sent, and seek the frame counter over frames already predicted.
1327
+ this.#peer.onInitialSync.add((peer_id, session_token, frame_number, flags, buf, payload_end) => {
1328
+ const first = (flags & InitialSyncPage.FIRST) !== 0;
1329
+ const final = (flags & InitialSyncPage.FINAL) !== 0;
1330
+
1331
+ if (first) {
1332
+ if (this.#binding_answer !== BindingAnswer.Pending) {
1333
+ // This binding has had its answer — a resume, or a join already under
1334
+ // way or done. The host starts one join per connection, so this is not
1335
+ // ours to act on: the client has been predicting on the world it holds.
1336
+ return;
1337
+ }
1338
+ this.#begin_sync(peer_id, session_token, frame_number);
1339
+ } else if (this.#binding_answer !== BindingAnswer.Syncing) {
1340
+ // a page of a join this binding is not in
1276
1341
  return;
1277
1342
  }
1278
1343
 
1279
- const token = new UUID();
1280
- token.data = session_token;
1281
- this.session_token = token;
1282
-
1283
- /*
1284
- Align to the host's clock. This argument has always been on the wire and in this
1285
- signature -- it was named `_frame_number` and dropped -- and without it a client
1286
- joining a match in progress tags its inputs from 0 against a host at frame 6,000,
1287
- which trims every one of them silently. GAP-042.
1288
-
1289
- The lead is the buffer depth the time dilation is already steering toward: an
1290
- input authored now arrives in the host's future, and this is how far ahead it has
1291
- to be tagged to land in the window rather than behind it. It only has to be in the
1292
- right neighbourhood -- `TimeDilation` converges on the real depth from the host's
1293
- feedback within a second or two -- but starting six thousand frames out is not a
1294
- neighbourhood, which is what made this total rather than approximate.
1295
- */
1296
- this.seek_to_frame(frame_number + 1 + this.#time_dilation.target_buffer_depth);
1297
-
1298
- // The world is about to be the host's. What this client predicted, sampled or was
1299
- // told about the one it had is about a world, and a frame numbering, that is gone.
1300
- this.#prediction_history.clear();
1301
- this.#pending_auth.clear();
1302
- this.#reconciled_frame.clear();
1303
- this.#sampled_actions_per_frame.clear();
1304
-
1305
1344
  this.#apply_initial_sync(buf, payload_end);
1306
- this.#binding_answer = BindingAnswer.Synced;
1307
1345
 
1308
- // Input from here on is predicted on the host's world. What was predicted before —
1309
- // held since this binding was made, or still in the log from before it — was
1310
- // predicted on the world just replaced, and is never sent.
1311
- this.#world_start_frame = this.#local_frame;
1312
- this.#peer.release_outbound(peer_id, this.#world_start_frame);
1313
-
1314
- // Reconnecting counts as well as Resyncing: a host that has
1315
- // finished with our old session readmits us as a fresh peer,
1316
- // so the snapshot arrives with no RESUME_ACCEPT ahead of it.
1317
- // That is the reconnection — staying in the ladder would burn
1318
- // attempts while holding the world the host just sent, and
1319
- // recovering later via RESUME_ACCEPT would report
1320
- // `state_resynced: false` over a world that was wholly
1321
- // replaced. `Lost` is excluded on purpose: the application has
1322
- // already been told that session is over.
1323
- if (this.#reconnect_state === ReconnectState.Resyncing
1324
- || this.#reconnect_state === ReconnectState.Reconnecting) {
1325
- this.#enter_connected();
1326
- this.onReconnected.send1({ state_resynced: true });
1346
+ if (final) {
1347
+ this.#finish_sync(peer_id, frame_number);
1327
1348
  }
1328
1349
  });
1350
+ this.#peer.onTimeDilationFeedback.add((_peer_id, _depth, frame_number) => {
1351
+ if (frame_number > this.#host_frame_seen) this.#host_frame_seen = frame_number;
1352
+ });
1329
1353
  // Host's response to our RESUME_HELLO during reconnect.
1330
1354
  this.#peer.onResumeAccept.add((peer_id) => {
1331
1355
  if (this.#binding_answer !== BindingAnswer.Pending) {
@@ -1336,6 +1360,9 @@ export class NetworkSession {
1336
1360
  // The host kept the world this client holds, so what the client predicted on it —
1337
1361
  // while cut off, and since — is owed to the host like any input, late as it is.
1338
1362
  this.#peer.release_outbound(peer_id, this.#world_start_frame);
1363
+ // and what the host sent on it meanwhile is about that world
1364
+ this.#inbound_release_peer = peer_id;
1365
+ this.#inbound_release_syncs = false;
1339
1366
  if (this.#reconnect_state === ReconnectState.Reconnecting
1340
1367
  || this.#reconnect_state === ReconnectState.Resyncing) {
1341
1368
  this.#enter_connected();
@@ -1370,6 +1397,7 @@ export class NetworkSession {
1370
1397
  this.#peer.onDisconnectPacket.add((_peer_id, reason_label) => {
1371
1398
  this.#permanently_lose_connection(reason_label || 'disconnected_by_host');
1372
1399
  });
1400
+ this.#peer.onProtocolError.add((_peer_id, reason) => this.#on_client_protocol_error(reason));
1373
1401
  } else {
1374
1402
  // Host: RESUME_HELLO validation + peer-initiated disconnects.
1375
1403
  this.#peer.onResumeHello.add((peer_id, local_peer_id, last_acked_frame, has_token, session_token) => {
@@ -1380,6 +1408,7 @@ export class NetworkSession {
1380
1408
  // and free state immediately.
1381
1409
  this.drop_peer(peer_id, reason_label || 'client_disconnect');
1382
1410
  });
1411
+ this.#peer.onProtocolError.add((peer_id, reason) => this.#on_peer_protocol_error(peer_id, reason));
1383
1412
  }
1384
1413
 
1385
1414
  // EntityObserver for NetworkIdentity: patches owner/network_id on
@@ -1478,7 +1507,13 @@ export class NetworkSession {
1478
1507
  // snapshot replaces must never reach the host. So what the client predicts meanwhile is
1479
1508
  // held: sent on an accept, dropped on a sync.
1480
1509
  this.#binding_answer = BindingAnswer.Pending;
1510
+ this.#host_frame_seen = -1;
1481
1511
  this.#peer.hold_outbound(remote_peer_id);
1512
+ // Inbound too, until the answer is whole: an action stream that arrives ahead of it —
1513
+ // a join's stream can, when the pages are lost on the way — is about the world the
1514
+ // answer settles, a resumed one or a new one, and is applied once it is settled.
1515
+ this.#peer.hold_inbound(remote_peer_id);
1516
+ this.#inbound_release_peer = -1;
1482
1517
 
1483
1518
  // Announce ourselves whenever this connect could be a claim on an
1484
1519
  // id the host still holds a record for: we have a token to prove
@@ -1499,6 +1534,9 @@ export class NetworkSession {
1499
1534
  remote_peer_id, this.local_peer_id, last_acked,
1500
1535
  token !== null, token !== null ? token.data : NO_SESSION_TOKEN,
1501
1536
  );
1537
+ // Out at once rather than with the next tick: a connect is an event, not part of
1538
+ // a frame, and the host can answer nothing until it has heard the claim.
1539
+ this.#peer.flush_peer(remote_peer_id);
1502
1540
  }
1503
1541
  this.#has_ever_connected = true;
1504
1542
  }
@@ -1542,6 +1580,7 @@ export class NetworkSession {
1542
1580
  // Best-effort notification — peer may have already dropped.
1543
1581
  try {
1544
1582
  this.#peer.send_disconnect(peer_id, reason);
1583
+ this.#peer.flush_peer(peer_id);
1545
1584
  } catch (_) { /* swallow */
1546
1585
  }
1547
1586
  this.#server.disconnect_peer(peer_id);
@@ -1551,10 +1590,23 @@ export class NetworkSession {
1551
1590
  this.#peer_inbound_mark.delete(peer_id);
1552
1591
  this.#peer_grace_state.delete(peer_id);
1553
1592
  this.#peer_session_tokens.delete(peer_id);
1554
- this.#pending_initial_sync.delete(peer_id);
1593
+ this.#joins.delete(peer_id);
1555
1594
  this.onPeerPermanentlyDropped.send2(peer_id, reason);
1556
1595
  }
1557
1596
 
1597
+ /**
1598
+ * Send what is queued for `peer_id` — a kick's RESUME_REJECT and DISCONNECT — before its
1599
+ * connection is torn down. One best-effort flush: a transport that refuses it (closed, say)
1600
+ * changes nothing about the teardown that follows.
1601
+ * @param {number} peer_id
1602
+ */
1603
+ #flush_last_word(peer_id) {
1604
+ try {
1605
+ this.#peer.flush_peer(peer_id);
1606
+ } catch (_) { /* swallow */
1607
+ }
1608
+ }
1609
+
1558
1610
  /**
1559
1611
  * Client-only: voluntarily disconnect from the server. Sends a
1560
1612
  * DISCONNECT packet so the host can free state immediately and
@@ -1570,6 +1622,7 @@ export class NetworkSession {
1570
1622
  const peer_id = this.#remote_peer_id;
1571
1623
  try {
1572
1624
  this.#peer.send_disconnect(peer_id, reason);
1625
+ this.#peer.flush_peer(peer_id);
1573
1626
  } catch (_) { /* swallow */
1574
1627
  }
1575
1628
  // Tear down the channel binding so a subsequent connect() with a
@@ -1598,9 +1651,19 @@ export class NetworkSession {
1598
1651
 
1599
1652
  /**
1600
1653
  * Tear down. Idempotent — safe to call from cleanup paths.
1654
+ *
1655
+ * Takes back from the engine what {@link start} put in it: the {@link NetworkSystem} is out of
1656
+ * the engine's loop when this returns, so another session can start on the same engine; this one
1657
+ * does not start again. The
1658
+ * networked entities stay in the world, and their `network_id`s are released — an id means
1659
+ * nothing without the session that gave it. The {@link InterpolationSystem} stays too, whoever
1660
+ * added it: it is the engine's, and blends whatever else is interpolated; the session only
1661
+ * withdraws its own source from it.
1662
+ *
1663
+ * @returns {Promise} resolves once the engine has shut the {@link NetworkSystem} down
1601
1664
  */
1602
1665
  stop() {
1603
- if (!this.#started) return;
1666
+ if (!this.#started) return Promise.resolve();
1604
1667
 
1605
1668
  this.entity_manager.fixedStepTimeScale = 1;
1606
1669
 
@@ -1634,6 +1697,8 @@ export class NetworkSession {
1634
1697
  }
1635
1698
  this.#event_listeners.clear();
1636
1699
  this.#remote_entities.clear();
1700
+ this.#owned_by.clear();
1701
+ this.#owner_of.clear();
1637
1702
  this.#sampled_actions_per_frame.clear();
1638
1703
  this.#peer_inbound_mark.clear();
1639
1704
  for (const transport of this.#inbound.values()) {
@@ -1641,6 +1706,13 @@ export class NetworkSession {
1641
1706
  }
1642
1707
  this.#inbound.clear();
1643
1708
  this.#started = false;
1709
+ this.#stopped = true;
1710
+
1711
+ // last: removing the system unlinks every networked entity, and nothing of the session's
1712
+ // may still be listening to that
1713
+ const network_system = this.#network_system;
1714
+ this.#network_system = null;
1715
+ return this.entity_manager.removeSystem(network_system);
1644
1716
  }
1645
1717
 
1646
1718
  // ----------------------------------------------------------------
@@ -1679,6 +1751,7 @@ export class NetworkSession {
1679
1751
  this.#interpolation.settle();
1680
1752
 
1681
1753
  this.#flush_inbound();
1754
+ this.#release_held_inbound();
1682
1755
 
1683
1756
  this.#apply_pending_interpolation();
1684
1757
 
@@ -1710,11 +1783,12 @@ export class NetworkSession {
1710
1783
 
1711
1784
  try {
1712
1785
  if (this.#server !== null) {
1713
- // runs the frame's local sim, then publishes (onBeforeTickClose), then closes it
1786
+ // runs the frame's local sim, then closes it
1714
1787
  this.#server.end_tick();
1715
1788
  } else {
1716
- this.#client.end_tick();
1789
+ // queued before end_tick, which sends: the marker leaves with the tick's stream
1717
1790
  this.#send_frame_marker(this.#local_frame);
1791
+ this.#client.end_tick();
1718
1792
  if (this.#reconcile === NetworkSessionReconcile.Correct) {
1719
1793
  this.#record_prediction(this.#local_frame);
1720
1794
  }
@@ -1769,102 +1843,6 @@ export class NetworkSession {
1769
1843
  return deferred;
1770
1844
  }
1771
1845
 
1772
- /**
1773
- * Host, end of step: record every replicated component whose wire image differs from what the
1774
- * peers hold, into the still-open frame, for the action stream to carry. Recorded, not
1775
- * executed — see {@link SimActionExecutor#record}. A component an action wrote this step and
1776
- * nothing changed since is not sent again: the peers apply the action.
1777
- */
1778
- #publish_simulated_changes() {
1779
- const world = this.world;
1780
- const executor = this.#peer.executor;
1781
- const scratch = this.#scratch_publish_buf;
1782
- const changed_entities = executor.changed_entities;
1783
-
1784
- for (const entity_id of this.#event_listeners.keys()) {
1785
- const identity = world.getComponent(entity_id, NetworkIdentity);
1786
- if (identity === undefined || identity.network_id < 0) continue;
1787
-
1788
- for (const [ComponentClass, desc] of this.#replicated) {
1789
- if (ComponentClass === NetworkIdentity) continue;
1790
-
1791
- const live = world.getComponent(entity_id, ComponentClass);
1792
- if (live === undefined) continue;
1793
-
1794
- const type_id = desc.interp_type_id;
1795
-
1796
- scratch.position = 0;
1797
- this.binary_registry.getAdapter(ComponentClass.typeName).serialize(scratch, live);
1798
- const length = scratch.position;
1799
-
1800
- if (!this.#remember_published(entity_id, type_id, scratch.raw_bytes, length)) continue;
1801
-
1802
- const action = this.#peer.action_registry.acquire(this.#ReplaceComponentAction);
1803
- try {
1804
- action.network_id = identity.network_id;
1805
- action.component_type_id = type_id;
1806
- action.payload_buf.position = 0;
1807
- action.payload_buf.writeBytes(scratch.raw_bytes, 0, length);
1808
- action.payload_length = length;
1809
- executor.record(action);
1810
- } finally {
1811
- this.#peer.action_registry.release(action);
1812
- }
1813
-
1814
- if (changed_entities !== null) changed_entities.add(identity.network_id);
1815
- }
1816
- }
1817
- }
1818
-
1819
- /**
1820
- * Host: an action wrote `ComponentClass` of `entity_id`; the peers will hold what it wrote.
1821
- * @param {number} entity_id
1822
- * @param {Function} ComponentClass
1823
- */
1824
- #note_published(entity_id, ComponentClass) {
1825
- const desc = this.#replicated.get(ComponentClass);
1826
- if (desc === undefined || ComponentClass === NetworkIdentity) return;
1827
-
1828
- const live = this.world.getComponent(entity_id, ComponentClass);
1829
- if (live === undefined) return;
1830
-
1831
- const scratch = this.#scratch_publish_buf;
1832
- scratch.position = 0;
1833
- this.binary_registry.getAdapter(ComponentClass.typeName).serialize(scratch, live);
1834
- this.#remember_published(entity_id, desc.interp_type_id, scratch.raw_bytes, scratch.position);
1835
- }
1836
-
1837
- /**
1838
- * Set what the peers hold for `(entity_id, type_id)`.
1839
- * @param {number} entity_id
1840
- * @param {number} type_id
1841
- * @param {Uint8Array} bytes
1842
- * @param {number} length
1843
- * @returns {boolean} whether it differs from what was held before
1844
- */
1845
- #remember_published(entity_id, type_id, bytes, length) {
1846
- let published = this.#published.get(entity_id);
1847
- if (published === undefined) {
1848
- published = [];
1849
- this.#published.set(entity_id, published);
1850
- }
1851
- while (published.length <= type_id) published.push(null);
1852
-
1853
- let held = published[type_id];
1854
- if (held !== null && held.length === length && bytes_equal(bytes, held.bytes, length)) {
1855
- return false;
1856
- }
1857
-
1858
- if (held === null || held.bytes.length < length) {
1859
- held = { bytes: new Uint8Array(Math.max(length, 32)), length: 0 };
1860
- published[type_id] = held;
1861
- }
1862
- held.bytes.set(bytes.subarray(0, length), 0);
1863
- held.length = length;
1864
-
1865
- return true;
1866
- }
1867
-
1868
1846
  /**
1869
1847
  * Once per rendered frame: the work paced by wall time rather than by the simulation.
1870
1848
  * @param {number} dt_seconds
@@ -2109,6 +2087,7 @@ export class NetworkSession {
2109
2087
  if (identity.owner_peer_id !== this.local_peer_id) {
2110
2088
  this.#remote_entities.add(entity_id);
2111
2089
  }
2090
+ this.#file_owner(entity_id, identity.owner_peer_id);
2112
2091
  this.#interpolation_pending.add(entity_id);
2113
2092
  // Per-entity event listener for net_mutate_component.
2114
2093
  const listener = (payload) => this.#on_net_mutate_component(entity_id, payload);
@@ -2123,23 +2102,26 @@ export class NetworkSession {
2123
2102
  this.#event_listeners.delete(entity_id);
2124
2103
  }
2125
2104
  this.#remote_entities.delete(entity_id);
2126
- this.#published.delete(entity_id);
2105
+ this.#file_owner(entity_id, -1);
2127
2106
  this.#prediction_history.forget(entity_id);
2128
2107
  // take back our Interpolated at the next step or frame, if the entity outlives its identity
2129
2108
  this.#interpolation_pending.add(entity_id);
2130
2109
  }
2131
2110
 
2132
2111
  /**
2133
- * Handler for `"net_mutate_component"` events. Translates the payload
2134
- * into a `ReplaceComponentAction` and dispatches via the executor.
2112
+ * Handler for `"net_mutate_component"` events: a replicated component changed outside an
2113
+ * action, and goes through the executor as a `ReplaceComponentAction`. The session finds no
2114
+ * change by itself (see the README, "What travels"); this is the door for a system that writes
2115
+ * replicated state directly — physics publishing its moving bodies, say — at a cost per
2116
+ * change, never per entity.
2135
2117
  *
2136
2118
  * Payload:
2137
- * - `{ component_type, new_state }` — recommended; engine captures
2138
- * current bytes as prior, then applies `new_state` via the
2139
- * replication adapter.
2140
- * - `{ component_type }` — caller has already mutated live; prior
2141
- * bytes equal post bytes (rewind for this mutation is a no-op).
2142
- *
2119
+ * - `{ component_type, new_state }` — the engine captures the current bytes as prior, then
2120
+ * applies `new_state` through the replication adapter.
2121
+ * - `{ component_type }` — the caller has already written live. On the host it is recorded,
2122
+ * not executed ({@link SimActionExecutor#record}): live is the truth, at full precision,
2123
+ * and executing would write the wire's quantised image back over it. On a client it is
2124
+ * executed, so prediction and rewind keep their prior state.
2143
2125
  */
2144
2126
  #on_net_mutate_component(entity_id, payload) {
2145
2127
  const { component_type, new_state } = payload || {};
@@ -2174,8 +2156,15 @@ export class NetworkSession {
2174
2156
  }
2175
2157
  action.payload_length = action.payload_buf.position;
2176
2158
 
2159
+ const executor = this.#peer.executor;
2177
2160
  try {
2178
- this.#peer.executor.execute(action, this.local_peer_id);
2161
+ if (this.#server !== null && (new_state === undefined || new_state === null)) {
2162
+ executor.record(action);
2163
+ // recorded writes are changes too: a join re-sends an entity changed after its page
2164
+ if (executor.changed_entities !== null) executor.changed_entities.add(network_id);
2165
+ } else {
2166
+ executor.execute(action, this.local_peer_id);
2167
+ }
2179
2168
  } finally {
2180
2169
  this.#peer.action_registry.release(action);
2181
2170
  }
@@ -2280,41 +2269,31 @@ export class NetworkSession {
2280
2269
  * @param {number} sim_frame
2281
2270
  */
2282
2271
  #on_host_tick_complete(sim_frame) {
2283
- // Initial-sync first, so receivers have a populated world before
2284
- // AUTH_STATE / action-stream packets land.
2285
- if (this.#pending_initial_sync.size > 0) {
2286
-
2287
- for (const [peer_id, sends_remaining] of this.#pending_initial_sync) {
2288
-
2289
- this.#send_initial_sync_to(peer_id, sim_frame);
2290
-
2291
- if (sends_remaining <= 1) {
2292
- this.#pending_initial_sync.delete(peer_id);
2293
- } else {
2294
- this.#pending_initial_sync.set(peer_id, sends_remaining - 1);
2272
+ // Joins first: a page is the state at this frame, which has just closed.
2273
+ if (this.#joins.size > 0) {
2274
+ for (const [peer_id, join] of this.#joins) {
2275
+ const progress = join.advance(sim_frame);
2276
+ if (progress === JoinProgress.Done) {
2277
+ this.#joins.delete(peer_id);
2278
+ } else if (progress === JoinProgress.DidNotConverge) {
2279
+ this.drop_peer(peer_id, 'join_did_not_converge');
2295
2280
  }
2296
-
2297
2281
  }
2298
-
2299
2282
  }
2300
2283
 
2301
2284
  // AUTH_STATE per owned entity, then time-dilation feedback, to
2302
- // every connected peer.
2285
+ // every connected peer. A peer still joining gets no AUTH_STATE: it
2286
+ // does not hold the world the state would correct yet.
2303
2287
  for (const peer_id of this.#connected_peers) {
2304
2288
 
2305
- for (const entity_id of this.#event_listeners.keys()) {
2306
-
2307
- const identity = this.world.getComponent(entity_id, NetworkIdentity);
2289
+ const owned = this.#joins.has(peer_id) ? undefined : this.#owned_by.get(peer_id);
2308
2290
 
2309
- if (identity === undefined) {
2310
- continue;
2311
- }
2312
- if (identity.owner_peer_id !== peer_id) {
2313
- continue;
2291
+ if (owned !== undefined) {
2292
+ for (const entity_id of owned) {
2293
+ const identity = this.world.getComponent(entity_id, NetworkIdentity);
2294
+ if (identity === undefined) continue;
2295
+ this.#send_auth_state_for_entity(peer_id, sim_frame, entity_id, identity);
2314
2296
  }
2315
-
2316
- this.#send_auth_state_for_entity(peer_id, sim_frame, entity_id, identity);
2317
-
2318
2297
  }
2319
2298
 
2320
2299
  this.#server.send_time_dilation_feedback(peer_id);
@@ -2323,49 +2302,160 @@ export class NetworkSession {
2323
2302
 
2324
2303
 
2325
2304
  /**
2326
- * Queue `peer_id`'s INITIAL_SYNC, unless its connection has been given
2327
- * one already — queued, or queued and sent. These are the host's only
2328
- * full-world sends, so how many a connection gets is the host's decision:
2329
- * one, when it takes the connection for a fresh peer — as `connect()`
2330
- * binds it, or after refusing its claim to resume. Nothing the peer sends
2331
- * afterwards earns it another.
2305
+ * Start `peer_id`'s join, unless its connection has been given one already — started, or
2306
+ * finished. These are the host's only whole-world sends, so how many a connection gets is the
2307
+ * host's decision: one, when it takes the connection for a fresh peer — as `connect()` binds
2308
+ * it, or after refusing its claim to resume. Nothing the peer sends afterwards earns it another.
2309
+ *
2310
+ * The peer's action stream is held from here until the join's FINAL page: what it would carry
2311
+ * is about a world the peer does not hold yet. The session token is issued here, and goes out
2312
+ * with every page; the receiver echoes it back via RESUME_HELLO to authenticate a later
2313
+ * reconnect.
2332
2314
  * @param {number} peer_id
2333
2315
  */
2334
2316
  #queue_initial_sync(peer_id) {
2335
2317
  if (this.#initial_sync_given.has(peer_id)) return;
2336
2318
  this.#initial_sync_given.add(peer_id);
2337
- this.#pending_initial_sync.set(peer_id, 2);
2319
+
2320
+ let token = this.#peer_session_tokens.get(peer_id);
2321
+ if (token === undefined) {
2322
+ token = UUID.v1();
2323
+ this.#peer_session_tokens.set(peer_id, token);
2324
+ }
2325
+
2326
+ this.#peer.hold_outbound(peer_id);
2327
+ this.#joins.set(peer_id, new HostJoin({
2328
+ peer: this.#peer,
2329
+ peer_id,
2330
+ session_token: token.data,
2331
+ world: this.world,
2332
+ for_each_entity: this.#for_each_networked_entity,
2333
+ page_buffer: this.#page_buffer,
2334
+ window_bytes: 128 * 1024,
2335
+ }));
2338
2336
  }
2339
2337
 
2340
2338
  /**
2341
- * Emit a Snapshotter-format payload to a freshly-connected peer.
2342
- * Generates and stashes a UUID v1 session token on first send,
2343
- * which the receiver echoes back via RESUME_HELLO to authenticate
2344
- * later reconnects.
2339
+ * Visits every networked entity: those whose NetworkIdentity the session is listening to.
2340
+ * @type {function(function(number): void): void}
2341
+ */
2342
+ #for_each_networked_entity = (visit) => {
2343
+ for (const entity_id of this.#event_listeners.keys()) {
2344
+ visit(entity_id);
2345
+ }
2346
+ };
2347
+
2348
+ /**
2349
+ * Client: the first page of a join. It is this binding's answer: the world is about to be the
2350
+ * host's, so what was predicted, sampled or told about the one the client had goes, and the
2351
+ * reconnect ladder stops — a join of a large world takes a while, and an attempt that tore the
2352
+ * binding down would restart it from the first page.
2353
+ *
2345
2354
  * @param {number} peer_id
2346
- * @param {number} sim_frame
2355
+ * @param {Uint8Array} session_token
2356
+ * @param {number} frame_number the frame the page's state is from
2357
+ */
2358
+ #begin_sync(peer_id, session_token, frame_number) {
2359
+ const token = new UUID();
2360
+ token.data = session_token;
2361
+ this.session_token = token;
2362
+
2363
+ this.#align_to_host(frame_number);
2364
+ this.#forget_predicted_world();
2365
+ this.#binding_answer = BindingAnswer.Syncing;
2366
+ // The host releases its action stream, and its AUTH_STATE, from the frame after the last
2367
+ // page; their datagrams can overtake the pages. The binding's inbound hold, taken at
2368
+ // `connect()`, keeps them until the world is whole.
2369
+
2370
+ // Reconnecting counts as well as Resyncing: a host that has
2371
+ // finished with our old session readmits us as a fresh peer,
2372
+ // so the snapshot arrives with no RESUME_ACCEPT ahead of it.
2373
+ // That is the reconnection — staying in the ladder would burn
2374
+ // attempts while holding the world the host is sending, and
2375
+ // recovering later via RESUME_ACCEPT would report
2376
+ // `state_resynced: false` over a world that was wholly
2377
+ // replaced. `Lost` is excluded on purpose: the application has
2378
+ // already been told that session is over.
2379
+ if (this.#reconnect_state === ReconnectState.Resyncing
2380
+ || this.#reconnect_state === ReconnectState.Reconnecting) {
2381
+ this.#enter_connected();
2382
+ this.onReconnected.send1({ state_resynced: true });
2383
+ }
2384
+ }
2385
+
2386
+ /**
2387
+ * Client: the FINAL page. Every entity's state at `frame_number` is here, and the host's action
2388
+ * stream resumes after it. The client predicted on a partial world while the pages came in, and
2389
+ * against the frame the first page named, which is as old as the join is long: both are put
2390
+ * right here, and only then does the client's input go out.
2391
+ *
2392
+ * @param {number} peer_id
2393
+ * @param {number} frame_number
2347
2394
  */
2348
- #send_initial_sync_to(peer_id, sim_frame) {
2349
- let token = this.#peer_session_tokens.get(peer_id);
2395
+ #finish_sync(peer_id, frame_number) {
2396
+ this.#align_to_host(frame_number);
2397
+ this.#forget_predicted_world();
2398
+ this.#binding_answer = BindingAnswer.Synced;
2350
2399
 
2351
- if (token === undefined) {
2352
- token = UUID.v1();
2353
- this.#peer_session_tokens.set(peer_id, token);
2400
+ // Input from here on is predicted on the host's world. What was predicted before —
2401
+ // held since this binding was made, or still in the log from before it — was
2402
+ // predicted on a world that is gone, or half-built, and is never sent.
2403
+ this.#world_start_frame = this.#local_frame;
2404
+ this.#peer.release_outbound(peer_id, this.#world_start_frame);
2405
+
2406
+ // what the host sent after the last page, applied as the step's inbound is done, now that
2407
+ // the world it is about is here; then onSynced
2408
+ this.#inbound_release_peer = peer_id;
2409
+ this.#inbound_release_syncs = true;
2410
+ }
2411
+
2412
+ /**
2413
+ * Client: apply what the binding held, once its answer is whole (see {@link #inbound_release_peer}).
2414
+ */
2415
+ #release_held_inbound() {
2416
+ const peer_id = this.#inbound_release_peer;
2417
+ if (peer_id < 0) return;
2418
+ this.#inbound_release_peer = -1;
2419
+ if (!this.#peer.is_connected(peer_id)) return;
2420
+ this.#peer.release_inbound(peer_id);
2421
+ if (this.#inbound_release_syncs) {
2422
+ this.#inbound_release_syncs = false;
2423
+ this.onSynced.send0();
2354
2424
  }
2425
+ }
2355
2426
 
2356
- this.#peer.send_initial_sync(peer_id, token.data, sim_frame, (buf) => {
2357
- snapshotter_emit({
2358
- buffer: buf,
2359
- world: this.world,
2360
- slot_table: this.#peer.slot_table,
2361
- component_registry: this.#peer.component_registry,
2362
- entity_iter: (cb) => {
2363
- for (const entity_id of this.#event_listeners.keys()) {
2364
- cb(entity_id);
2365
- }
2366
- },
2367
- });
2368
- });
2427
+ /**
2428
+ * Seek the frame counter to the host's `frame_number` — or the newer frame a TIME_DILATION
2429
+ * has named since — plus the lead input needs.
2430
+ *
2431
+ * The frame has always been on the wire — it was named `_frame_number` and dropped — and
2432
+ * without it a client joining a match in progress tags its inputs from 0 against a host at
2433
+ * frame 6,000, which trims every one of them silently. GAP-042.
2434
+ *
2435
+ * The lead is the buffer depth the time dilation is already steering toward: an input
2436
+ * authored now arrives in the host's future, and this is how far ahead it has to be tagged to
2437
+ * land in the window rather than behind it. It only has to be in the right neighbourhood —
2438
+ * `TimeDilation` converges on the real depth from the host's feedback within a second or two —
2439
+ * but starting six thousand frames out is not a neighbourhood, which is what made this total
2440
+ * rather than approximate.
2441
+ *
2442
+ * @param {number} frame_number
2443
+ */
2444
+ #align_to_host(frame_number) {
2445
+ const host_frame = Math.max(frame_number, this.#host_frame_seen);
2446
+ this.seek_to_frame(host_frame + 1 + this.#time_dilation.target_buffer_depth);
2447
+ }
2448
+
2449
+ /**
2450
+ * Drop what this client predicted, sampled or was told about the world it held: it is about a
2451
+ * world, and a frame numbering, that is being replaced.
2452
+ */
2453
+ #forget_predicted_world() {
2454
+ this.#prediction_history.clear();
2455
+ this.#pending_auth.clear();
2456
+ this.#reconciled_frame.clear();
2457
+ this.#sampled_actions_per_frame.clear();
2458
+ this.#client.forget_timeline();
2369
2459
  }
2370
2460
 
2371
2461
  /**
@@ -2382,7 +2472,7 @@ export class NetworkSession {
2382
2472
  // written over the truth, not over a render blend that a restore would then put back
2383
2473
  this.normalize_if_dirty();
2384
2474
 
2385
- const entity_count = buffer.readUintVar();
2475
+ const entity_count = buffer.readUint32();
2386
2476
  for (let i = 0; i < entity_count; i++) {
2387
2477
  const network_id = buffer.readUintVar();
2388
2478
  const component_count = buffer.readUint8();
@@ -2471,9 +2561,35 @@ export class NetworkSession {
2471
2561
  } else {
2472
2562
  this.#remote_entities.add(entity_id);
2473
2563
  }
2564
+ this.#file_owner(entity_id, owner_peer_id);
2474
2565
  this.#interpolation_pending.add(entity_id);
2475
2566
  }
2476
2567
 
2568
+ /**
2569
+ * File `entity_id` under `owner_peer_id` in {@link #owned_by}, out of wherever it was; -1 files
2570
+ * it nowhere.
2571
+ * @param {number} entity_id
2572
+ * @param {number} owner_peer_id
2573
+ */
2574
+ #file_owner(entity_id, owner_peer_id) {
2575
+ const previous = this.#owner_of.get(entity_id);
2576
+ if (previous === owner_peer_id) return;
2577
+ if (previous !== undefined) {
2578
+ const set = this.#owned_by.get(previous);
2579
+ set.delete(entity_id);
2580
+ if (set.size === 0) this.#owned_by.delete(previous);
2581
+ this.#owner_of.delete(entity_id);
2582
+ }
2583
+ if (owner_peer_id < 0) return;
2584
+ let set = this.#owned_by.get(owner_peer_id);
2585
+ if (set === undefined) {
2586
+ set = new Set();
2587
+ this.#owned_by.set(owner_peer_id, set);
2588
+ }
2589
+ set.add(entity_id);
2590
+ this.#owner_of.set(entity_id, owner_peer_id);
2591
+ }
2592
+
2477
2593
  /**
2478
2594
  *
2479
2595
  * @param {number} peer_id
@@ -2551,7 +2667,7 @@ export class NetworkSession {
2551
2667
  // session. Both calls are no-ops when there is nothing to release.
2552
2668
  this.#server.disconnect_peer(peer_id);
2553
2669
  this.#unwire_peer_transport_disconnect(peer_id);
2554
- this.#pending_initial_sync.delete(peer_id);
2670
+ this.#joins.delete(peer_id);
2555
2671
  return;
2556
2672
  }
2557
2673
  const last_acked = this.#peer.baseline.last_acked(peer_id);
@@ -2563,15 +2679,32 @@ export class NetworkSession {
2563
2679
  // from `#on_host_tick_complete` and throws — before AUTH_STATE and
2564
2680
  // time-dilation feedback go out to *any* peer, and without ever
2565
2681
  // draining the entry, so every later tick throws the same way.
2566
- this.#pending_initial_sync.delete(peer_id);
2682
+ // a peer that drops before its join finished holds half a world: it may not resume it
2683
+ const join_complete = !this.#joins.has(peer_id);
2684
+ this.#joins.delete(peer_id);
2567
2685
  this.#unwire_peer_transport_disconnect(peer_id);
2568
2686
  this.#peer_grace_state.set(peer_id, {
2569
2687
  deadline_ms: this.#clock_ms + this.#server_resume_grace_ms,
2570
2688
  last_acked,
2689
+ join_complete,
2571
2690
  });
2572
2691
  this.onPeerLost.send2(peer_id, reason);
2573
2692
  }
2574
2693
 
2694
+ /**
2695
+ * Host: `peer_id`'s connection is no longer whole (see Connection.onProtocolError) and can
2696
+ * carry nothing more. Handled as its transport dropping — the peer goes into the grace window
2697
+ * and may resume on a new connection — and the transport is closed, so the client hears of it
2698
+ * now rather than when silence reaps it.
2699
+ * @param {number} peer_id
2700
+ * @param {string} reason
2701
+ */
2702
+ #on_peer_protocol_error(peer_id, reason) {
2703
+ const entry = this.#peer_transport_listeners.get(peer_id);
2704
+ this.#on_peer_transport_disconnected(peer_id, `protocol_error: ${reason}`);
2705
+ if (entry !== undefined) close_transport(entry.transport);
2706
+ }
2707
+
2575
2708
  /**
2576
2709
  * Validate a RESUME_HELLO and either welcome the peer back (restore
2577
2710
  * baseline + send RESUME_ACCEPT) or reject with a reason code. A claim
@@ -2603,6 +2736,7 @@ export class NetworkSession {
2603
2736
  if (transport_peer_id !== claimed_peer_id) {
2604
2737
  this.#peer.send_resume_reject(transport_peer_id, ResumeRejectReason.PeerIdCollision);
2605
2738
  this.#peer.send_disconnect(transport_peer_id, 'peer_id_collision');
2739
+ this.#flush_last_word(transport_peer_id);
2606
2740
  this.#server.disconnect_peer(transport_peer_id);
2607
2741
  this.#unwire_peer_transport_disconnect(transport_peer_id);
2608
2742
  // The transport may have been added to the live set in connect()
@@ -2611,7 +2745,7 @@ export class NetworkSession {
2611
2745
  // serializes AUTH_STATE / time-dilation to a now-disposed conn and
2612
2746
  // throws "peer not connected", taking down the tick for all peers.
2613
2747
  this.#connected_peers.delete(transport_peer_id);
2614
- this.#pending_initial_sync.delete(transport_peer_id);
2748
+ this.#joins.delete(transport_peer_id);
2615
2749
  this.#peer_inbound_mark.delete(transport_peer_id);
2616
2750
  return;
2617
2751
  }
@@ -2639,6 +2773,7 @@ export class NetworkSession {
2639
2773
  // INITIAL_SYNC queue.
2640
2774
  this.#peer.send_resume_reject(transport_peer_id, ResumeRejectReason.NoSessionToken);
2641
2775
  this.#peer.send_disconnect(transport_peer_id, 'no_session_token');
2776
+ this.#flush_last_word(transport_peer_id);
2642
2777
  this.#server.disconnect_peer(transport_peer_id);
2643
2778
  this.#unwire_peer_transport_disconnect(transport_peer_id);
2644
2779
  return;
@@ -2679,16 +2814,28 @@ export class NetworkSession {
2679
2814
  // a new peer_id from the matchmaking layer.
2680
2815
  this.#peer.send_resume_reject(transport_peer_id, ResumeRejectReason.TokenMismatch);
2681
2816
  this.#peer.send_disconnect(transport_peer_id, 'token_mismatch');
2817
+ this.#flush_last_word(transport_peer_id);
2682
2818
  this.#server.disconnect_peer(transport_peer_id);
2683
2819
  this.#unwire_peer_transport_disconnect(transport_peer_id);
2684
2820
  // Same cleanup as the collision branch above: if this transport was
2685
2821
  // a fresh peer added to the live set, drop it from both iterated
2686
2822
  // maps so the next host tick doesn't address a disposed conn.
2687
2823
  this.#connected_peers.delete(transport_peer_id);
2688
- this.#pending_initial_sync.delete(transport_peer_id);
2824
+ this.#joins.delete(transport_peer_id);
2689
2825
  this.#peer_inbound_mark.delete(transport_peer_id);
2690
2826
  return;
2691
2827
  }
2828
+ // A join that never finished. The client holds whatever pages reached it — part of a
2829
+ // world — and resuming would keep it. Refused like a stale frame: the client falls through
2830
+ // to a full resync.
2831
+ if (grace.join_complete === false) {
2832
+ this.#peer.send_resume_reject(transport_peer_id, ResumeRejectReason.StaleFrame);
2833
+ this.#peer_grace_state.delete(claimed_peer_id);
2834
+ this.#queue_initial_sync(transport_peer_id);
2835
+ this.#connected_peers.add(transport_peer_id);
2836
+ return;
2837
+ }
2838
+
2692
2839
  // Ring-floor gate. The client asks to resume from `last_acked_frame + 1`,
2693
2840
  // but the action-log ring only still holds frames back to
2694
2841
  // `current_sim_frame - frame_capacity + 1`. If the requested frame
@@ -2733,7 +2880,7 @@ export class NetworkSession {
2733
2880
  // so release it here or the id stays unconnectable forever.
2734
2881
  this.#server.disconnect_peer(peer_id);
2735
2882
  this.#unwire_peer_transport_disconnect(peer_id);
2736
- this.#pending_initial_sync.delete(peer_id);
2883
+ this.#joins.delete(peer_id);
2737
2884
  this.onPeerPermanentlyDropped.send2(peer_id, 'grace_expired');
2738
2885
  }
2739
2886
  }
@@ -2813,6 +2960,25 @@ export class NetworkSession {
2813
2960
  this.#current_transport_listener = listener;
2814
2961
  }
2815
2962
 
2963
+ /**
2964
+ * Client: the connection to the host is no longer whole (see Connection.onProtocolError) and
2965
+ * can carry nothing more. Given up as a dropped transport is — the reconnect ladder resumes on
2966
+ * a new one — and closed, so the host hears of it now rather than when silence reaps it.
2967
+ * @param {string} reason
2968
+ */
2969
+ #on_client_protocol_error(reason) {
2970
+ const transport = this.#current_transport;
2971
+ // unhooked first: the close must not be taken for a second loss
2972
+ if (transport !== null && this.#current_transport_listener !== null) {
2973
+ try {
2974
+ transport.onDisconnect.remove(this.#current_transport_listener);
2975
+ } catch (_) { /* swallow */
2976
+ }
2977
+ }
2978
+ this.#on_client_transport_disconnected(`protocol_error: ${reason}`);
2979
+ if (transport !== null) close_transport(transport);
2980
+ }
2981
+
2816
2982
  #on_client_transport_disconnected(reason) {
2817
2983
  if (this.#reconnect_state === ReconnectState.Lost) return;
2818
2984
  // Tear down the dead peer/channel binding so a fresh transport
@@ -3001,9 +3167,10 @@ export class NetworkSession {
3001
3167
  const scratch = this.#scratch_send_buf;
3002
3168
  const world = this.world;
3003
3169
 
3004
- for (const entity_id of this.#event_listeners.keys()) {
3005
- if (this.#remote_entities.has(entity_id)) continue;
3170
+ const owned = this.#owned_by.get(this.local_peer_id);
3171
+ if (owned === undefined) return;
3006
3172
 
3173
+ for (const entity_id of owned) {
3007
3174
  const identity = world.getComponent(entity_id, NetworkIdentity);
3008
3175
  if (identity === undefined || identity.network_id < 0) continue;
3009
3176