@woosh/meep-engine 2.168.1 → 2.169.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 (287) hide show
  1. package/README.md +2 -2
  2. package/build/bundle-worker-image-decoder.js +1 -1
  3. package/package.json +1 -1
  4. package/src/REVIEW_2026_08_06/adv-core.md +511 -0
  5. package/src/REVIEW_2026_08_06/adv-gfx.md +480 -0
  6. package/src/REVIEW_2026_08_06/adv-view.md +446 -0
  7. package/src/REVIEW_2026_08_06/api-core.md +296 -0
  8. package/src/REVIEW_2026_08_06/api-engine.md +195 -0
  9. package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +507 -0
  10. package/src/REVIEW_2026_08_06/core-binary.md +348 -0
  11. package/src/REVIEW_2026_08_06/core-collection.md +376 -0
  12. package/src/REVIEW_2026_08_06/core-math.md +221 -0
  13. package/src/REVIEW_2026_08_06/core-process.md +266 -0
  14. package/src/REVIEW_2026_08_06/core-science.md +223 -0
  15. package/src/REVIEW_2026_08_06/engine-ai-nav.md +279 -0
  16. package/src/REVIEW_2026_08_06/engine-animation.md +167 -0
  17. package/src/REVIEW_2026_08_06/engine-asset.md +166 -0
  18. package/src/REVIEW_2026_08_06/engine-ecs.md +920 -0
  19. package/src/REVIEW_2026_08_06/engine-input.md +197 -0
  20. package/src/REVIEW_2026_08_06/engine-misc.md +248 -0
  21. package/src/REVIEW_2026_08_06/engine-terrain.md +52 -0
  22. package/src/REVIEW_2026_08_06/generation.md +176 -0
  23. package/src/REVIEW_2026_08_06/geom-2d.md +434 -0
  24. package/src/REVIEW_2026_08_06/geom-3d.md +117 -0
  25. package/src/REVIEW_2026_08_06/gfx-gi.md +143 -0
  26. package/src/REVIEW_2026_08_06/gfx-render.md +740 -0
  27. package/src/REVIEW_2026_08_06/gfx-texture.md +690 -0
  28. package/src/REVIEW_2026_08_06/prior-criticals.md +49 -0
  29. package/src/REVIEW_2026_08_06/prior-highs.md +138 -0
  30. package/src/REVIEW_2026_08_06/test-gaps.md +364 -0
  31. package/src/REVIEW_2026_08_06/view.md +667 -0
  32. package/src/REVIEW_2026_08_06.md +571 -0
  33. package/src/core/binary/BinaryBuffer.d.ts.map +1 -1
  34. package/src/core/binary/BinaryBuffer.js +12 -1
  35. package/src/core/binary/BitImage2.d.ts +5 -1
  36. package/src/core/binary/BitImage2.d.ts.map +1 -1
  37. package/src/core/binary/BitImage2.js +5 -1
  38. package/src/core/bvh8/build/NodeProxy.d.ts.map +1 -1
  39. package/src/core/bvh8/build/NodeProxy.js +314 -308
  40. package/src/core/collection/list/List.d.ts +15 -0
  41. package/src/core/collection/list/List.d.ts.map +1 -1
  42. package/src/core/collection/list/List.js +34 -0
  43. package/src/core/collection/list/SortedListProjection.d.ts.map +1 -1
  44. package/src/core/collection/list/SortedListProjection.js +4 -1
  45. package/src/core/collection/map/BiMap.d.ts +25 -2
  46. package/src/core/collection/map/BiMap.d.ts.map +1 -1
  47. package/src/core/collection/map/BiMap.js +57 -3
  48. package/src/core/debug/matchers/IsAnything.d.ts.map +1 -1
  49. package/src/core/debug/matchers/IsAnything.js +5 -1
  50. package/src/core/events/signal/Signal.d.ts.map +1 -1
  51. package/src/core/events/signal/Signal.js +3 -1
  52. package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +433 -0
  53. package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts +31 -0
  54. package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts.map +1 -0
  55. package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.js +84 -0
  56. package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts +24 -2
  57. package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts.map +1 -1
  58. package/src/core/geom/2d/polygon/polygon2_signed_area.js +52 -34
  59. package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts +28 -0
  60. package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts.map +1 -0
  61. package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.js +86 -0
  62. package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts +40 -0
  63. package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts.map +1 -0
  64. package/src/core/geom/2d/polygon/polygon2_triangulate.js +1280 -0
  65. package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts +28 -0
  66. package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts.map +1 -0
  67. package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.js +77 -0
  68. package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts +6 -3
  69. package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts.map +1 -1
  70. package/src/core/geom/2d/triangle/tri2_rasterize_conservative.js +26 -13
  71. package/src/core/geom/2d/triangle/tri2_signed_area.d.ts +17 -1
  72. package/src/core/geom/2d/triangle/tri2_signed_area.d.ts.map +1 -1
  73. package/src/core/geom/2d/triangle/tri2_signed_area.js +35 -23
  74. package/src/core/geom/2d/v2_morton_encode.d.ts +20 -0
  75. package/src/core/geom/2d/v2_morton_encode.d.ts.map +1 -0
  76. package/src/core/geom/2d/v2_morton_encode.js +23 -0
  77. package/src/core/geom/3d/atlas/segment/atlas_merge_charts.d.ts.map +1 -1
  78. package/src/core/geom/3d/atlas/segment/atlas_merge_charts.js +4 -1
  79. package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts +27 -0
  80. package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts.map +1 -0
  81. package/src/core/geom/3d/polygon/polygon3_compute_normal.js +60 -0
  82. package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts +34 -0
  83. package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts.map +1 -0
  84. package/src/core/geom/3d/polygon/polygon3_triangulate.js +94 -0
  85. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +8 -2
  86. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -1
  87. package/src/core/geom/3d/quaternion/quat3_multiply.js +8 -2
  88. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
  89. package/src/core/geom/3d/shape/ConvexHullShape3D.js +32 -2
  90. package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts +23 -6
  91. package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts.map +1 -1
  92. package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.js +268 -124
  93. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts +17 -0
  94. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts.map +1 -0
  95. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.js +44 -0
  96. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts +20 -0
  97. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts.map +1 -0
  98. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.js +40 -0
  99. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts +19 -0
  100. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts.map +1 -0
  101. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.js +34 -0
  102. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts +22 -0
  103. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts.map +1 -0
  104. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.js +37 -0
  105. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts +87 -0
  106. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts.map +1 -0
  107. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.js +599 -0
  108. package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts +32 -0
  109. package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts.map +1 -1
  110. package/src/core/geom/3d/triangle/tri3_compute_signed_volume.js +66 -7
  111. package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts +22 -1
  112. package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts.map +1 -1
  113. package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.js +83 -38
  114. package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +554 -0
  115. package/src/core/geom/vec3/v3_cotangent.d.ts +6 -2
  116. package/src/core/geom/vec3/v3_cotangent.d.ts.map +1 -1
  117. package/src/core/geom/vec3/v3_cotangent.js +6 -2
  118. package/src/core/geom/vec3/v3_cross.d.ts +6 -2
  119. package/src/core/geom/vec3/v3_cross.d.ts.map +1 -1
  120. package/src/core/geom/vec3/v3_cross.js +6 -2
  121. package/src/core/geom/vec3/v3_multiply.d.ts +6 -2
  122. package/src/core/geom/vec3/v3_multiply.d.ts.map +1 -1
  123. package/src/core/geom/vec3/v3_multiply.js +6 -2
  124. package/src/core/geom/vec3/v3_subtract.d.ts +6 -2
  125. package/src/core/geom/vec3/v3_subtract.d.ts.map +1 -1
  126. package/src/core/geom/vec3/v3_subtract.js +6 -2
  127. package/src/core/localization/Localization.js +1 -1
  128. package/src/core/math/random/randomSeed.d.ts +13 -0
  129. package/src/core/math/random/randomSeed.d.ts.map +1 -0
  130. package/src/core/math/random/randomSeed.js +16 -0
  131. package/src/core/math/spline/spline3_hermite_apply_transform.d.ts +50 -0
  132. package/src/core/math/spline/spline3_hermite_apply_transform.d.ts.map +1 -0
  133. package/src/core/math/spline/spline3_hermite_apply_transform.js +107 -0
  134. package/src/core/math/spline/spline3_hermite_reverse.d.ts +34 -0
  135. package/src/core/math/spline/spline3_hermite_reverse.d.ts.map +1 -0
  136. package/src/core/math/spline/spline3_hermite_reverse.js +49 -0
  137. package/src/core/math/statistics/hammersley_sequence.d.ts +17 -4
  138. package/src/core/math/statistics/hammersley_sequence.d.ts.map +1 -1
  139. package/src/core/math/statistics/hammersley_sequence.js +25 -16
  140. package/src/core/process/Future.d.ts.map +1 -1
  141. package/src/core/process/Future.js +12 -0
  142. package/src/core/process/PromiseWatcher.d.ts.map +1 -1
  143. package/src/core/process/PromiseWatcher.js +9 -2
  144. package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
  145. package/src/core/process/executor/ConcurrentExecutor.js +12 -2
  146. package/src/core/process/worker/WorkerProxy.d.ts.map +1 -1
  147. package/src/core/process/worker/WorkerProxy.js +5 -2
  148. package/src/core/process/worker/extractTransferables.d.ts.map +1 -1
  149. package/src/core/process/worker/extractTransferables.js +4 -1
  150. package/src/engine/Engine.d.ts +12 -12
  151. package/src/engine/Engine.d.ts.map +1 -1
  152. package/src/engine/Engine.js +149 -10
  153. package/src/engine/asset/loaders/ArrayBufferLoader.d.ts.map +1 -1
  154. package/src/engine/asset/loaders/ArrayBufferLoader.js +7 -0
  155. package/src/engine/ecs/speaker/lines/sets/LineSetDescription.d.ts.map +1 -1
  156. package/src/engine/ecs/speaker/lines/sets/LineSetDescription.js +3 -1
  157. package/src/engine/ecs/terrain/ecs/TerrainSystem.d.ts.map +1 -1
  158. package/src/engine/ecs/terrain/ecs/TerrainSystem.js +4 -1
  159. package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts +29 -0
  160. package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts.map +1 -1
  161. package/src/engine/ecs/terrain/tiles/TerrainTileManager.js +84 -23
  162. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +446 -0
  163. package/src/engine/graphics/GraphicsEngine.d.ts +56 -7
  164. package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
  165. package/src/engine/graphics/GraphicsEngine.js +196 -24
  166. package/src/engine/graphics/context/WebGLContextFailureReason.d.ts +9 -0
  167. package/src/engine/graphics/context/WebGLContextFailureReason.d.ts.map +1 -0
  168. package/src/engine/graphics/context/WebGLContextFailureReason.js +17 -0
  169. package/src/engine/graphics/context/WebGLContextMonitor.d.ts +106 -0
  170. package/src/engine/graphics/context/WebGLContextMonitor.d.ts.map +1 -0
  171. package/src/engine/graphics/context/WebGLContextMonitor.js +341 -0
  172. package/src/engine/graphics/context/WebGLContextState.d.ts +14 -0
  173. package/src/engine/graphics/context/WebGLContextState.d.ts.map +1 -0
  174. package/src/engine/graphics/context/WebGLContextState.js +24 -0
  175. package/src/engine/graphics/context/testWebGLContextLoss.d.ts +2 -0
  176. package/src/engine/graphics/context/testWebGLContextLoss.d.ts.map +1 -0
  177. package/src/engine/graphics/context/testWebGLContextLoss.js +696 -0
  178. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.d.ts.map +1 -1
  179. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.js +68 -32
  180. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts +14 -0
  181. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts.map +1 -0
  182. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.js +41 -0
  183. package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.js +1 -1
  184. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.d.ts.map +1 -1
  185. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js +5 -0
  186. package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts +1 -1
  187. package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts.map +1 -1
  188. package/src/engine/graphics/ecs/trail2d/Trail2DSystem.js +2 -1
  189. package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts +1 -1
  190. package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts.map +1 -1
  191. package/src/engine/graphics/ecs/trail3d/Trail3DSystem.js +3 -5
  192. package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.d.ts.map +1 -1
  193. package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.js +3 -2
  194. package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.d.ts.map +1 -1
  195. package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.js +3 -1
  196. package/src/engine/graphics/geometry/computeMeshSurfaceArea.js +1 -1
  197. package/src/engine/graphics/load_and_set_cubemap_v0.d.ts +5 -0
  198. package/src/engine/graphics/load_and_set_cubemap_v0.d.ts.map +1 -1
  199. package/src/engine/graphics/load_and_set_cubemap_v0.js +26 -21
  200. package/src/engine/graphics/particles/particular/group/ParticleGroup.d.ts.map +1 -1
  201. package/src/engine/graphics/particles/particular/group/ParticleGroup.js +19 -8
  202. package/src/engine/graphics/particles/particular/group/optimizeCommandQueue.js +1 -1
  203. package/src/engine/graphics/texture/atlas/AtlasLookupTexture.d.ts.map +1 -1
  204. package/src/engine/graphics/texture/atlas/AtlasLookupTexture.js +10 -3
  205. package/src/engine/graphics/texture/atlas/TextureAtlas.d.ts.map +1 -1
  206. package/src/engine/graphics/texture/atlas/TextureAtlas.js +4 -1
  207. package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.d.ts.map +1 -1
  208. package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.js +11 -0
  209. package/src/engine/graphics/texture/cubemap/load_environment_map.d.ts.map +1 -1
  210. package/src/engine/graphics/texture/cubemap/load_environment_map.js +5 -2
  211. package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.d.ts.map +1 -1
  212. package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.js +26 -18
  213. package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts +15 -2
  214. package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts.map +1 -1
  215. package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.js +29 -3
  216. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts +11 -1
  217. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts.map +1 -1
  218. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.js +158 -171
  219. package/src/engine/graphics/texture/sampler/sampler2d_paint.js +1 -1
  220. package/src/engine/graphics/texture/sampler/util/bitSet2Sampler2D.js +2 -2
  221. package/src/engine/input/ecs/util/TopDownCameraControllerHelper.d.ts.map +1 -1
  222. package/src/engine/input/ecs/util/TopDownCameraControllerHelper.js +10 -3
  223. package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts +14 -0
  224. package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts.map +1 -0
  225. package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.js +38 -0
  226. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
  227. package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +433 -425
  228. package/src/engine/physics/constraint/solve_constraints.d.ts +4 -1
  229. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
  230. package/src/engine/physics/constraint/solve_constraints.js +38 -13
  231. package/src/engine/physics/contact/ManifoldStore.d.ts +15 -3
  232. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  233. package/src/engine/physics/contact/ManifoldStore.js +15 -3
  234. package/src/engine/physics/ecs/Joint.d.ts +7 -4
  235. package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
  236. package/src/engine/physics/ecs/Joint.js +7 -4
  237. package/src/engine/physics/ecs/PhysicsSystem.d.ts +14 -0
  238. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  239. package/src/engine/physics/ecs/PhysicsSystem.js +16 -0
  240. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  241. package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
  242. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +15 -7
  243. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -1
  244. package/src/engine/physics/narrowphase/convex_convex_manifold.js +33 -10
  245. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +6 -2
  246. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
  247. package/src/engine/physics/narrowphase/refine_ray_concave.js +6 -2
  248. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +6 -2
  249. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -1
  250. package/src/engine/physics/narrowphase/refine_ray_hit.js +6 -2
  251. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  252. package/src/engine/physics/queries/raycast.js +11 -4
  253. package/src/engine/save/storage/IndexedDBStorage.d.ts.map +1 -1
  254. package/src/engine/save/storage/IndexedDBStorage.js +21 -3
  255. package/src/engine/simulation/Ticker.d.ts +12 -0
  256. package/src/engine/simulation/Ticker.d.ts.map +1 -1
  257. package/src/engine/simulation/Ticker.js +18 -0
  258. package/src/engine/sound/simulation/AcousticSimulator.d.ts.map +1 -1
  259. package/src/engine/sound/simulation/AcousticSimulator.js +9 -1
  260. package/src/engine/sound/simulation/core/VolumeField.d.ts +6 -2
  261. package/src/engine/sound/simulation/core/VolumeField.d.ts.map +1 -1
  262. package/src/engine/sound/simulation/core/VolumeField.js +6 -2
  263. package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts +6 -2
  264. package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts.map +1 -1
  265. package/src/engine/sound/simulation/probe/AcousticProbeField.js +6 -2
  266. package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts +6 -2
  267. package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts.map +1 -1
  268. package/src/engine/sound/simulation/probe/acoustic_probe_transfer.js +6 -2
  269. package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts +3 -1
  270. package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts.map +1 -1
  271. package/src/engine/sound/simulation/probe/bakeProbeReflectors.js +3 -1
  272. package/src/engine/ui/GUIEngine.d.ts.map +1 -1
  273. package/src/engine/ui/GUIEngine.js +8 -1
  274. package/src/generation/theme/TerrainTheme.d.ts.map +1 -1
  275. package/src/generation/theme/TerrainTheme.js +2 -1
  276. package/src/generation/theme/ThemeEngine.d.ts.map +1 -1
  277. package/src/generation/theme/ThemeEngine.js +6 -5
  278. package/src/view/ViewGroup.d.ts.map +1 -1
  279. package/src/view/ViewGroup.js +9 -7
  280. package/src/view/graphics/WebGLContextFailureView.d.ts +19 -0
  281. package/src/view/graphics/WebGLContextFailureView.d.ts.map +1 -0
  282. package/src/view/graphics/WebGLContextFailureView.js +76 -0
  283. package/src/view/minimap/Minimap.d.ts +8 -0
  284. package/src/view/minimap/Minimap.d.ts.map +1 -1
  285. package/src/view/minimap/Minimap.js +16 -3
  286. package/src/view/minimap/dom/MinimapMarkerView.d.ts.map +1 -1
  287. package/src/view/minimap/dom/MinimapMarkerView.js +3 -1
@@ -1,425 +1,433 @@
1
- import { assert } from "../../../core/assert.js";
2
- import Signal from "../../../core/events/signal/Signal.js";
3
- import { AlwaysRelevantScope } from "../replication/ScopeFilter.js";
4
- import { RewindEngine } from "../sim/RewindEngine.js";
5
- import { InputRing } from "../state/InputRing.js";
6
- import { TimeDilation } from "../time/TimeDilation.js";
7
- import { NetworkPeer } from "./NetworkPeer.js";
8
-
9
- /**
10
- * Client-side orchestrator for the predict-reconcile model.
11
- *
12
- * Encapsulates the algorithm previously inlined in
13
- * `app/src/prototype/network_prototype_prediction.js`:
14
- *
15
- * - Per tick: sample input, store it in the {@link InputRing}, fire
16
- * {@link onPredict} so the user's game code can execute SimAction(s)
17
- * representing the input. The action gets recorded into the local
18
- * action_log via the executor (capturing prior-state for later rewind),
19
- * and goes out over the action stream when `peer.end_tick` flushes it.
20
- *
21
- * - On AUTH_STATE arrival: rewind the world to end-of-(server_frame - 1)
22
- * via the {@link RewindEngine}, deserialize the server's authoritative
23
- * state bytes via {@link onApplyAuthState}, then replay forward by
24
- * firing {@link onReplay} for every frame in the InputRing between
25
- * `server_frame + 1` and the client's current frame. The user's
26
- * {@link onReplay} handler re-executes the same SimActions the original
27
- * predict path produced.
28
- *
29
- * - A no-op short-circuit is built in: before doing the rewind+replay, the
30
- * orchestrator gives the user a chance to compute what the replay WOULD
31
- * produce via {@link onComputeExpected}; if that matches the current
32
- * local state within a tolerance, the rewind is skipped entirely. This
33
- * keeps the steady-state reconciliation loop from oscillating.
34
- *
35
- * The orchestrator does NOT bake in input encoding — the user provides
36
- * serialize/deserialize/apply hooks. This keeps `MovePlayerAction` (and
37
- * any other input action shape) decoupled from the orchestrator.
38
- *
39
- * @author Alex Goldring
40
- * @copyright Company Named Limited (c) 2026
41
- */
42
- export class ServerAuthoritativeClient {
43
- #last_reconciled_frame;
44
- #current_frame;
45
- #expected_scratch;
46
- #measured_scratch;
47
-
48
-
49
- /**
50
- * @param {{
51
- * world: EntityComponentDataset,
52
- * binary_registry: BinarySerializationRegistry,
53
- * replicated_components: Function[],
54
- * action_classes: Function[],
55
- * scope_filter?: object,
56
- * frame_capacity?: number,
57
- * initial_buffer_size?: number,
58
- * input_ring_capacity?: number,
59
- * input_byte_size?: number,
60
- * reconcile_epsilon?: number,
61
- * time_dilation?: TimeDilation,
62
- * }} options
63
- *
64
- * `time_dilation`: optional pre-configured {@link TimeDilation}.
65
- * On TIME_DILATION feedback the client recomputes
66
- * {@link dilation_factor} for the caller to scale its tick cadence.
67
- * Defaults to a stock instance; `dilation_factor` is 1.0 until the
68
- * first feedback arrives.
69
- */
70
- constructor({
71
- world,
72
- binary_registry,
73
- replicated_components,
74
- action_classes,
75
- scope_filter = new AlwaysRelevantScope(),
76
- frame_capacity = 32,
77
- initial_buffer_size = 1024,
78
- input_ring_capacity = 256,
79
- input_byte_size = 4,
80
- reconcile_epsilon = 1e-4,
81
- time_dilation = new TimeDilation(),
82
- }) {
83
- /** @type {NetworkPeer} */
84
- this.peer = new NetworkPeer({
85
- world,
86
- binary_registry,
87
- replicated_components,
88
- action_classes,
89
- scope_filter,
90
- frame_capacity,
91
- initial_buffer_size,
92
- });
93
-
94
- /** @type {EntityComponentDataset} */
95
- this.world = world;
96
- /** @type {SimActionExecutor} */
97
- this.executor = this.peer.executor;
98
- /** @type {ReplicationSlotTable} */
99
- this.slot_table = this.peer.slot_table;
100
- /** @type {ActionLog} */
101
- this.action_log = this.peer.action_log;
102
- /** @type {SimActionRegistry} */
103
- this.action_registry = this.peer.action_registry;
104
-
105
- /**
106
- * Per-frame input ring. Sized to hold roughly 2× RTT_in_frames of
107
- * history so reconciliation can always find the inputs to replay.
108
- * @type {InputRing}
109
- */
110
- this.input_ring = new InputRing({
111
- frame_capacity: input_ring_capacity,
112
- initial_buffer_size: input_byte_size,
113
- });
114
-
115
- /** @type {RewindEngine} */
116
- this.rewind_engine = new RewindEngine({
117
- action_log: this.action_log,
118
- world: this.world,
119
- component_registry: this.peer.component_registry,
120
- });
121
-
122
- /**
123
- * Tolerance for the no-op reconciliation short-circuit. If the user's
124
- * {@link onComputeExpected} returns a state that matches the local
125
- * state to within this epsilon, the rewind+replay is skipped.
126
- * @type {number}
127
- */
128
- this.reconcile_epsilon = reconcile_epsilon;
129
-
130
- // State
131
- this.#last_reconciled_frame = -1;
132
- this.#current_frame = -1;
133
-
134
- // Counters
135
- /** @type {number} */
136
- this.reconcile_count = 0;
137
- /** @type {number} */
138
- this.replay_frame_count = 0;
139
-
140
- /**
141
- * Adaptive tick-rate controller. Server sends observed input-buffer
142
- * depth via the TIME_DILATION packet; this maps it to a tick-period
143
- * multiplier {@link dilation_factor}.
144
- * @type {TimeDilation}
145
- */
146
- this.time_dilation = time_dilation;
147
-
148
- /**
149
- * Most recent buffer depth reported by the server. -1 before any
150
- * feedback has arrived.
151
- * @type {number}
152
- */
153
- this.last_buffer_depth = -1;
154
-
155
- /**
156
- * Tick-period multiplier from the latest TIME_DILATION feedback.
157
- * `< 1.0` caller should run faster, `> 1.0` slower. 1.0 until
158
- * the first feedback arrives.
159
- *
160
- * scheduled_tick_period_ms = TICK_PERIOD_MS * client.dilation_factor;
161
- *
162
- * @type {number}
163
- */
164
- this.dilation_factor = 1.0;
165
-
166
- this.peer.onTimeDilationFeedback.add((_peer_id, buffer_depth) => {
167
- this.last_buffer_depth = buffer_depth;
168
- this.dilation_factor = this.time_dilation.compute(buffer_depth);
169
- });
170
-
171
- /**
172
- * Fires once per tick during prediction. Args: `(frame_number,
173
- * input_writer)` where `input_writer(write_fn)` records the input
174
- * bytes into the InputRing. The user's handler should:
175
- * 1. Sample input.
176
- * 2. Write input bytes via `input_writer(buf => buf.writeXxx(...))`.
177
- * 3. Execute SimAction(s) representing the input via
178
- * `client.executor.execute(action)`.
179
- * @type {Signal}
180
- */
181
- this.onPredict = new Signal();
182
-
183
- /**
184
- * Fires once per frame during reconciliation replay. Args:
185
- * `(frame_number, input_reader)` where `input_reader` is a
186
- * BinaryBuffer positioned at the start of that frame's stored
187
- * input bytes (or `null` if the frame's input wasn't recorded —
188
- * the handler must short-circuit in that case). The user's handler
189
- * should re-execute the SAME SimAction(s) that the predict handler
190
- * produced for the input.
191
- * @type {Signal}
192
- */
193
- this.onReplay = new Signal();
194
-
195
- /**
196
- * Fires when AUTH_STATE arrives, BEFORE the rewind+replay is
197
- * decided. Args: `(server_frame, network_id, buffer)`.
198
- *
199
- * The handler should peek at the server's bytes (the buffer is
200
- * positioned at the start of the auth payload; the handler may
201
- * advance and restore `buffer.position`) and compute what the
202
- * client's predicted state at the *current* frame WOULD be if a
203
- * full rewind+replay were performed, returning a 1D scalar or
204
- * `null` to skip the short-circuit. The orchestrator compares
205
- * the returned value to {@link onMeasureCurrent}'s return value
206
- * and short-circuits the rewind if they match within
207
- * {@link reconcile_epsilon}.
208
- *
209
- * Game code that doesn't want the optimization can simply not
210
- * subscribe to this; the rewind+replay then runs unconditionally.
211
- *
212
- * @type {Signal}
213
- */
214
- this.onComputeExpected = new Signal();
215
-
216
- /**
217
- * Fires alongside {@link onComputeExpected} to measure the current
218
- * local state for comparison. Args: `(network_id)`. Returns a
219
- * scalar.
220
- * @type {Signal}
221
- */
222
- this.onMeasureCurrent = new Signal();
223
-
224
- /**
225
- * Fires inside __handle_auth_state AFTER the no-op short-circuit
226
- * decided a rewind is needed but BEFORE the rewind starts.
227
- * Args: `(server_frame, network_id)`. Used by higher-level wiring
228
- * (e.g. {@link NetworkSession}) to bring the live world into
229
- * canonical form before `RewindEngine.rewind_to` reads it.
230
- * @type {Signal}
231
- */
232
- this.onBeforeReconcile = new Signal();
233
-
234
- /**
235
- * Fires on AUTH_STATE arrival, after the no-op check decided to
236
- * actually do the rewind+replay. Args: `(server_frame,
237
- * network_id, buffer)` where buffer is positioned at the auth
238
- * payload bytes. The handler should deserialize the bytes into
239
- * the world (the rewind has already happened; the buffer's bytes
240
- * ARE the new authoritative state at server_frame).
241
- * @type {Signal}
242
- */
243
- this.onApplyAuthState = new Signal();
244
-
245
- /**
246
- * Fires after each completed reconciliation (rewind + apply + replay).
247
- * Diagnostic. Args: `(server_frame, replay_count)`.
248
- * @type {Signal}
249
- */
250
- this.onReconcileComplete = new Signal();
251
-
252
- // Wire the AUTH_STATE handler to the algorithm above.
253
- this.peer.onAuthState.add((_peer_id, server_frame, network_id, buffer) => {
254
- this.#handle_auth_state(server_frame, network_id, buffer);
255
- });
256
- }
257
-
258
- /**
259
- * Connect to a server peer over a transport.
260
- * @param {number} peer_id
261
- * @param {object} transport
262
- */
263
- connect_to_server(peer_id, transport) {
264
- this.peer.connect_peer(peer_id, transport);
265
- }
266
-
267
- /**
268
- * Disconnect from a server peer.
269
- * @param {number} peer_id
270
- */
271
- disconnect_from_server(peer_id) {
272
- this.peer.disconnect_peer(peer_id);
273
- }
274
-
275
- /** Channel accessor for manual packet plumbing.
276
- * @param {number} peer_id */
277
- channel_for(peer_id) {
278
- return this.peer.channel_for(peer_id);
279
- }
280
-
281
- /**
282
- * Run one client-side prediction tick.
283
- *
284
- * 1. Open action_log frame.
285
- * 2. Fire {@link onPredict} so the user samples input, records it in the
286
- * InputRing, and executes SimAction(s).
287
- * 3. Close action_log frame (this flushes the action stream to all
288
- * connected peers via `peer.end_tick`).
289
- *
290
- * @param {number} current_frame
291
- */
292
- tick(current_frame) {
293
- assert.isNonNegativeInteger(current_frame, 'current_frame');
294
- this.#current_frame = current_frame;
295
-
296
- this.peer.begin_tick(current_frame);
297
- try {
298
- const input_writer = (cb) => this.input_ring.write(current_frame, cb);
299
- this.onPredict.send2(current_frame, input_writer);
300
- } finally {
301
- this.peer.end_tick();
302
- }
303
- }
304
-
305
- /**
306
- * Whether the InputRing has stored input bytes for the given frame.
307
- * Useful in {@link onReplay} handlers that need to short-circuit
308
- * frames with no recorded input.
309
- *
310
- * @param {number} frame
311
- * @returns {boolean}
312
- */
313
- has_input_for(frame) {
314
- return this.input_ring.has(frame);
315
- }
316
-
317
- /**
318
- * Read-only access to the InputRing buffer for a given frame. Position
319
- * is reset to 0 by the call.
320
- *
321
- * @param {number} frame
322
- * @returns {BinaryBuffer}
323
- */
324
- input_for(frame) {
325
- return this.input_ring.buffer_for(frame);
326
- }
327
-
328
- /** @private */
329
- #handle_auth_state(server_frame, network_id, buffer) {
330
- // Out-of-order safety: never reconcile to an older frame than the
331
- // last one we already reconciled to.
332
- if (server_frame <= this.#last_reconciled_frame) return;
333
- this.#last_reconciled_frame = server_frame;
334
-
335
- const local = this.slot_table.entity_for(network_id);
336
- if (local < 0) return;
337
-
338
- // No-op short-circuit. If the user provided both
339
- // onComputeExpected and onMeasureCurrent, compare. If the values
340
- // match within reconcile_epsilon, skip the rewind entirely.
341
- if (this.onComputeExpected.hasHandlers() && this.onMeasureCurrent.hasHandlers()) {
342
- // Capture the buffer position so onComputeExpected can peek
343
- // without disturbing the actual apply step below.
344
- const peek_pos = buffer.position;
345
- let expected = null;
346
- this.onComputeExpected.send3(server_frame, network_id, buffer);
347
- buffer.position = peek_pos;
348
- // The handler can stash the computed value on the buffer or via
349
- // a side channel; here we use a sentinel attached to the signal
350
- // result. Simpler protocol: handler stashes value in
351
- // this.#expected_scratch. Let's do that.
352
- expected = this.#expected_scratch;
353
- this.#expected_scratch = null;
354
-
355
- let measured = null;
356
- this.onMeasureCurrent.send1(network_id);
357
- measured = this.#measured_scratch;
358
- this.#measured_scratch = null;
359
-
360
- if (expected !== null && measured !== null
361
- && Math.abs(expected - measured) < this.reconcile_epsilon) {
362
- return; // Matches — no rewind needed.
363
- }
364
- }
365
-
366
- this.onBeforeReconcile.send2(server_frame, network_id);
367
-
368
- // Real divergence: rewind, apply server's bytes, replay forward.
369
- const rewind_target = server_frame - 1;
370
- if (this.#current_frame > rewind_target) {
371
- try {
372
- this.rewind_engine.rewind_to(this.#current_frame, rewind_target);
373
- } catch (e) {
374
- // The action_log probably rolled past server_frame. The
375
- // server would normally back-fill us via STATE_BURST; for now
376
- // skip this reconciliation.
377
- return;
378
- }
379
- }
380
-
381
- // Apply the server's authoritative state bytes. The handler does the
382
- // adapter-specific deserialize.
383
- this.onApplyAuthState.send3(server_frame, network_id, buffer);
384
-
385
- // Replay forward from (server_frame + 1) through current_frame.
386
- let replay_count = 0;
387
- for (let f = server_frame + 1; f <= this.#current_frame; f++) {
388
- this.action_log.begin_frame(f);
389
- try {
390
- let input_reader = null;
391
- if (this.input_ring.has(f)) {
392
- input_reader = this.input_ring.buffer_for(f);
393
- input_reader.position = 0;
394
- }
395
- this.onReplay.send2(f, input_reader);
396
- } finally {
397
- this.action_log.end_frame();
398
- }
399
- replay_count++;
400
- }
401
-
402
- this.reconcile_count++;
403
- this.replay_frame_count += replay_count;
404
- this.onReconcileComplete.send2(server_frame, replay_count);
405
- }
406
-
407
- /**
408
- * Helper for the {@link onComputeExpected} handler: stash the
409
- * computed scalar so the orchestrator can read it. Called inside the
410
- * handler.
411
- * @param {number} value
412
- */
413
- set_expected(value) {
414
- this.#expected_scratch = value;
415
- }
416
-
417
- /**
418
- * Helper for the {@link onMeasureCurrent} handler: stash the measured
419
- * scalar so the orchestrator can read it. Called inside the handler.
420
- * @param {number} value
421
- */
422
- set_measured(value) {
423
- this.#measured_scratch = value;
424
- }
425
- }
1
+ import { assert } from "../../../core/assert.js";
2
+ import { Uint32Map } from "../../../core/collection/Uint32Map.js";
3
+ import Signal from "../../../core/events/signal/Signal.js";
4
+ import { AlwaysRelevantScope } from "../replication/ScopeFilter.js";
5
+ import { RewindEngine } from "../sim/RewindEngine.js";
6
+ import { InputRing } from "../state/InputRing.js";
7
+ import { TimeDilation } from "../time/TimeDilation.js";
8
+ import { NetworkPeer } from "./NetworkPeer.js";
9
+
10
+ /**
11
+ * Client-side orchestrator for the predict-reconcile model.
12
+ *
13
+ * Encapsulates the algorithm previously inlined in
14
+ * `app/src/prototype/network_prototype_prediction.js`:
15
+ *
16
+ * - Per tick: sample input, store it in the {@link InputRing}, fire
17
+ * {@link onPredict} so the user's game code can execute SimAction(s)
18
+ * representing the input. The action gets recorded into the local
19
+ * action_log via the executor (capturing prior-state for later rewind),
20
+ * and goes out over the action stream when `peer.end_tick` flushes it.
21
+ *
22
+ * - On AUTH_STATE arrival: rewind the world to end-of-(server_frame - 1)
23
+ * via the {@link RewindEngine}, deserialize the server's authoritative
24
+ * state bytes via {@link onApplyAuthState}, then replay forward by
25
+ * firing {@link onReplay} for every frame in the InputRing between
26
+ * `server_frame + 1` and the client's current frame. The user's
27
+ * {@link onReplay} handler re-executes the same SimActions the original
28
+ * predict path produced.
29
+ *
30
+ * - A no-op short-circuit is built in: before doing the rewind+replay, the
31
+ * orchestrator gives the user a chance to compute what the replay WOULD
32
+ * produce via {@link onComputeExpected}; if that matches the current
33
+ * local state within a tolerance, the rewind is skipped entirely. This
34
+ * keeps the steady-state reconciliation loop from oscillating.
35
+ *
36
+ * The orchestrator does NOT bake in input encoding — the user provides
37
+ * serialize/deserialize/apply hooks. This keeps `MovePlayerAction` (and
38
+ * any other input action shape) decoupled from the orchestrator.
39
+ *
40
+ * @author Alex Goldring
41
+ * @copyright Company Named Limited (c) 2026
42
+ */
43
+ export class ServerAuthoritativeClient {
44
+ /**
45
+ * Newest server frame already reconciled, per network id. A single
46
+ * watermark for the whole client would let the first entity in a frame
47
+ * consume it and silently drop every other entity in that same frame.
48
+ * Uint32Map reports an absent key as -1, which is the correct starting
49
+ * watermark.
50
+ * @type {Uint32Map}
51
+ */
52
+ #last_reconciled_frame = new Uint32Map();
53
+ #current_frame;
54
+ #expected_scratch;
55
+ #measured_scratch;
56
+
57
+
58
+ /**
59
+ * @param {{
60
+ * world: EntityComponentDataset,
61
+ * binary_registry: BinarySerializationRegistry,
62
+ * replicated_components: Function[],
63
+ * action_classes: Function[],
64
+ * scope_filter?: object,
65
+ * frame_capacity?: number,
66
+ * initial_buffer_size?: number,
67
+ * input_ring_capacity?: number,
68
+ * input_byte_size?: number,
69
+ * reconcile_epsilon?: number,
70
+ * time_dilation?: TimeDilation,
71
+ * }} options
72
+ *
73
+ * `time_dilation`: optional pre-configured {@link TimeDilation}.
74
+ * On TIME_DILATION feedback the client recomputes
75
+ * {@link dilation_factor} for the caller to scale its tick cadence.
76
+ * Defaults to a stock instance; `dilation_factor` is 1.0 until the
77
+ * first feedback arrives.
78
+ */
79
+ constructor({
80
+ world,
81
+ binary_registry,
82
+ replicated_components,
83
+ action_classes,
84
+ scope_filter = new AlwaysRelevantScope(),
85
+ frame_capacity = 32,
86
+ initial_buffer_size = 1024,
87
+ input_ring_capacity = 256,
88
+ input_byte_size = 4,
89
+ reconcile_epsilon = 1e-4,
90
+ time_dilation = new TimeDilation(),
91
+ }) {
92
+ /** @type {NetworkPeer} */
93
+ this.peer = new NetworkPeer({
94
+ world,
95
+ binary_registry,
96
+ replicated_components,
97
+ action_classes,
98
+ scope_filter,
99
+ frame_capacity,
100
+ initial_buffer_size,
101
+ });
102
+
103
+ /** @type {EntityComponentDataset} */
104
+ this.world = world;
105
+ /** @type {SimActionExecutor} */
106
+ this.executor = this.peer.executor;
107
+ /** @type {ReplicationSlotTable} */
108
+ this.slot_table = this.peer.slot_table;
109
+ /** @type {ActionLog} */
110
+ this.action_log = this.peer.action_log;
111
+ /** @type {SimActionRegistry} */
112
+ this.action_registry = this.peer.action_registry;
113
+
114
+ /**
115
+ * Per-frame input ring. Sized to hold roughly 2× RTT_in_frames of
116
+ * history so reconciliation can always find the inputs to replay.
117
+ * @type {InputRing}
118
+ */
119
+ this.input_ring = new InputRing({
120
+ frame_capacity: input_ring_capacity,
121
+ initial_buffer_size: input_byte_size,
122
+ });
123
+
124
+ /** @type {RewindEngine} */
125
+ this.rewind_engine = new RewindEngine({
126
+ action_log: this.action_log,
127
+ world: this.world,
128
+ component_registry: this.peer.component_registry,
129
+ });
130
+
131
+ /**
132
+ * Tolerance for the no-op reconciliation short-circuit. If the user's
133
+ * {@link onComputeExpected} returns a state that matches the local
134
+ * state to within this epsilon, the rewind+replay is skipped.
135
+ * @type {number}
136
+ */
137
+ this.reconcile_epsilon = reconcile_epsilon;
138
+
139
+ // State
140
+ this.#current_frame = -1;
141
+
142
+ // Counters
143
+ /** @type {number} */
144
+ this.reconcile_count = 0;
145
+ /** @type {number} */
146
+ this.replay_frame_count = 0;
147
+
148
+ /**
149
+ * Adaptive tick-rate controller. Server sends observed input-buffer
150
+ * depth via the TIME_DILATION packet; this maps it to a tick-period
151
+ * multiplier {@link dilation_factor}.
152
+ * @type {TimeDilation}
153
+ */
154
+ this.time_dilation = time_dilation;
155
+
156
+ /**
157
+ * Most recent buffer depth reported by the server. -1 before any
158
+ * feedback has arrived.
159
+ * @type {number}
160
+ */
161
+ this.last_buffer_depth = -1;
162
+
163
+ /**
164
+ * Tick-period multiplier from the latest TIME_DILATION feedback.
165
+ * `< 1.0` ⇒ caller should run faster, `> 1.0` ⇒ slower. 1.0 until
166
+ * the first feedback arrives.
167
+ *
168
+ * scheduled_tick_period_ms = TICK_PERIOD_MS * client.dilation_factor;
169
+ *
170
+ * @type {number}
171
+ */
172
+ this.dilation_factor = 1.0;
173
+
174
+ this.peer.onTimeDilationFeedback.add((_peer_id, buffer_depth) => {
175
+ this.last_buffer_depth = buffer_depth;
176
+ this.dilation_factor = this.time_dilation.compute(buffer_depth);
177
+ });
178
+
179
+ /**
180
+ * Fires once per tick during prediction. Args: `(frame_number,
181
+ * input_writer)` where `input_writer(write_fn)` records the input
182
+ * bytes into the InputRing. The user's handler should:
183
+ * 1. Sample input.
184
+ * 2. Write input bytes via `input_writer(buf => buf.writeXxx(...))`.
185
+ * 3. Execute SimAction(s) representing the input via
186
+ * `client.executor.execute(action)`.
187
+ * @type {Signal}
188
+ */
189
+ this.onPredict = new Signal();
190
+
191
+ /**
192
+ * Fires once per frame during reconciliation replay. Args:
193
+ * `(frame_number, input_reader)` where `input_reader` is a
194
+ * BinaryBuffer positioned at the start of that frame's stored
195
+ * input bytes (or `null` if the frame's input wasn't recorded —
196
+ * the handler must short-circuit in that case). The user's handler
197
+ * should re-execute the SAME SimAction(s) that the predict handler
198
+ * produced for the input.
199
+ * @type {Signal}
200
+ */
201
+ this.onReplay = new Signal();
202
+
203
+ /**
204
+ * Fires when AUTH_STATE arrives, BEFORE the rewind+replay is
205
+ * decided. Args: `(server_frame, network_id, buffer)`.
206
+ *
207
+ * The handler should peek at the server's bytes (the buffer is
208
+ * positioned at the start of the auth payload; the handler may
209
+ * advance and restore `buffer.position`) and compute what the
210
+ * client's predicted state at the *current* frame WOULD be if a
211
+ * full rewind+replay were performed, returning a 1D scalar or
212
+ * `null` to skip the short-circuit. The orchestrator compares
213
+ * the returned value to {@link onMeasureCurrent}'s return value
214
+ * and short-circuits the rewind if they match within
215
+ * {@link reconcile_epsilon}.
216
+ *
217
+ * Game code that doesn't want the optimization can simply not
218
+ * subscribe to this; the rewind+replay then runs unconditionally.
219
+ *
220
+ * @type {Signal}
221
+ */
222
+ this.onComputeExpected = new Signal();
223
+
224
+ /**
225
+ * Fires alongside {@link onComputeExpected} to measure the current
226
+ * local state for comparison. Args: `(network_id)`. Returns a
227
+ * scalar.
228
+ * @type {Signal}
229
+ */
230
+ this.onMeasureCurrent = new Signal();
231
+
232
+ /**
233
+ * Fires inside __handle_auth_state AFTER the no-op short-circuit
234
+ * decided a rewind is needed but BEFORE the rewind starts.
235
+ * Args: `(server_frame, network_id)`. Used by higher-level wiring
236
+ * (e.g. {@link NetworkSession}) to bring the live world into
237
+ * canonical form before `RewindEngine.rewind_to` reads it.
238
+ * @type {Signal}
239
+ */
240
+ this.onBeforeReconcile = new Signal();
241
+
242
+ /**
243
+ * Fires on AUTH_STATE arrival, after the no-op check decided to
244
+ * actually do the rewind+replay. Args: `(server_frame,
245
+ * network_id, buffer)` where buffer is positioned at the auth
246
+ * payload bytes. The handler should deserialize the bytes into
247
+ * the world (the rewind has already happened; the buffer's bytes
248
+ * ARE the new authoritative state at server_frame).
249
+ * @type {Signal}
250
+ */
251
+ this.onApplyAuthState = new Signal();
252
+
253
+ /**
254
+ * Fires after each completed reconciliation (rewind + apply + replay).
255
+ * Diagnostic. Args: `(server_frame, replay_count)`.
256
+ * @type {Signal}
257
+ */
258
+ this.onReconcileComplete = new Signal();
259
+
260
+ // Wire the AUTH_STATE handler to the algorithm above.
261
+ this.peer.onAuthState.add((_peer_id, server_frame, network_id, buffer) => {
262
+ this.#handle_auth_state(server_frame, network_id, buffer);
263
+ });
264
+ }
265
+
266
+ /**
267
+ * Connect to a server peer over a transport.
268
+ * @param {number} peer_id
269
+ * @param {object} transport
270
+ */
271
+ connect_to_server(peer_id, transport) {
272
+ this.peer.connect_peer(peer_id, transport);
273
+ }
274
+
275
+ /**
276
+ * Disconnect from a server peer.
277
+ * @param {number} peer_id
278
+ */
279
+ disconnect_from_server(peer_id) {
280
+ this.peer.disconnect_peer(peer_id);
281
+ }
282
+
283
+ /** Channel accessor for manual packet plumbing.
284
+ * @param {number} peer_id */
285
+ channel_for(peer_id) {
286
+ return this.peer.channel_for(peer_id);
287
+ }
288
+
289
+ /**
290
+ * Run one client-side prediction tick.
291
+ *
292
+ * 1. Open action_log frame.
293
+ * 2. Fire {@link onPredict} so the user samples input, records it in the
294
+ * InputRing, and executes SimAction(s).
295
+ * 3. Close action_log frame (this flushes the action stream to all
296
+ * connected peers via `peer.end_tick`).
297
+ *
298
+ * @param {number} current_frame
299
+ */
300
+ tick(current_frame) {
301
+ assert.isNonNegativeInteger(current_frame, 'current_frame');
302
+ this.#current_frame = current_frame;
303
+
304
+ this.peer.begin_tick(current_frame);
305
+ try {
306
+ const input_writer = (cb) => this.input_ring.write(current_frame, cb);
307
+ this.onPredict.send2(current_frame, input_writer);
308
+ } finally {
309
+ this.peer.end_tick();
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Whether the InputRing has stored input bytes for the given frame.
315
+ * Useful in {@link onReplay} handlers that need to short-circuit
316
+ * frames with no recorded input.
317
+ *
318
+ * @param {number} frame
319
+ * @returns {boolean}
320
+ */
321
+ has_input_for(frame) {
322
+ return this.input_ring.has(frame);
323
+ }
324
+
325
+ /**
326
+ * Read-only access to the InputRing buffer for a given frame. Position
327
+ * is reset to 0 by the call.
328
+ *
329
+ * @param {number} frame
330
+ * @returns {BinaryBuffer}
331
+ */
332
+ input_for(frame) {
333
+ return this.input_ring.buffer_for(frame);
334
+ }
335
+
336
+ /** @private */
337
+ #handle_auth_state(server_frame, network_id, buffer) {
338
+ // Out-of-order safety: never reconcile to an older frame than the
339
+ // last one we already reconciled to.
340
+ if (server_frame <= this.#last_reconciled_frame.get(network_id)) return;
341
+ this.#last_reconciled_frame.set(network_id, server_frame);
342
+
343
+ const local = this.slot_table.entity_for(network_id);
344
+ if (local < 0) return;
345
+
346
+ // No-op short-circuit. If the user provided both
347
+ // onComputeExpected and onMeasureCurrent, compare. If the values
348
+ // match within reconcile_epsilon, skip the rewind entirely.
349
+ if (this.onComputeExpected.hasHandlers() && this.onMeasureCurrent.hasHandlers()) {
350
+ // Capture the buffer position so onComputeExpected can peek
351
+ // without disturbing the actual apply step below.
352
+ const peek_pos = buffer.position;
353
+ let expected = null;
354
+ this.onComputeExpected.send3(server_frame, network_id, buffer);
355
+ buffer.position = peek_pos;
356
+ // The handler can stash the computed value on the buffer or via
357
+ // a side channel; here we use a sentinel attached to the signal
358
+ // result. Simpler protocol: handler stashes value in
359
+ // this.#expected_scratch. Let's do that.
360
+ expected = this.#expected_scratch;
361
+ this.#expected_scratch = null;
362
+
363
+ let measured = null;
364
+ this.onMeasureCurrent.send1(network_id);
365
+ measured = this.#measured_scratch;
366
+ this.#measured_scratch = null;
367
+
368
+ if (expected !== null && measured !== null
369
+ && Math.abs(expected - measured) < this.reconcile_epsilon) {
370
+ return; // Matches no rewind needed.
371
+ }
372
+ }
373
+
374
+ this.onBeforeReconcile.send2(server_frame, network_id);
375
+
376
+ // Real divergence: rewind, apply server's bytes, replay forward.
377
+ const rewind_target = server_frame - 1;
378
+ if (this.#current_frame > rewind_target) {
379
+ try {
380
+ this.rewind_engine.rewind_to(this.#current_frame, rewind_target);
381
+ } catch (e) {
382
+ // The action_log probably rolled past server_frame. The
383
+ // server would normally back-fill us via STATE_BURST; for now
384
+ // skip this reconciliation.
385
+ return;
386
+ }
387
+ }
388
+
389
+ // Apply the server's authoritative state bytes. The handler does the
390
+ // adapter-specific deserialize.
391
+ this.onApplyAuthState.send3(server_frame, network_id, buffer);
392
+
393
+ // Replay forward from (server_frame + 1) through current_frame.
394
+ let replay_count = 0;
395
+ for (let f = server_frame + 1; f <= this.#current_frame; f++) {
396
+ this.action_log.begin_frame(f);
397
+ try {
398
+ let input_reader = null;
399
+ if (this.input_ring.has(f)) {
400
+ input_reader = this.input_ring.buffer_for(f);
401
+ input_reader.position = 0;
402
+ }
403
+ this.onReplay.send2(f, input_reader);
404
+ } finally {
405
+ this.action_log.end_frame();
406
+ }
407
+ replay_count++;
408
+ }
409
+
410
+ this.reconcile_count++;
411
+ this.replay_frame_count += replay_count;
412
+ this.onReconcileComplete.send2(server_frame, replay_count);
413
+ }
414
+
415
+ /**
416
+ * Helper for the {@link onComputeExpected} handler: stash the
417
+ * computed scalar so the orchestrator can read it. Called inside the
418
+ * handler.
419
+ * @param {number} value
420
+ */
421
+ set_expected(value) {
422
+ this.#expected_scratch = value;
423
+ }
424
+
425
+ /**
426
+ * Helper for the {@link onMeasureCurrent} handler: stash the measured
427
+ * scalar so the orchestrator can read it. Called inside the handler.
428
+ * @param {number} value
429
+ */
430
+ set_measured(value) {
431
+ this.#measured_scratch = value;
432
+ }
433
+ }