@woosh/meep-engine 2.168.0 → 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.
- package/README.md +2 -2
- package/build/bundle-worker-image-decoder.js +1 -1
- package/package.json +1 -1
- package/src/REVIEW_2026_08_06/adv-core.md +511 -0
- package/src/REVIEW_2026_08_06/adv-gfx.md +480 -0
- package/src/REVIEW_2026_08_06/adv-view.md +446 -0
- package/src/REVIEW_2026_08_06/api-core.md +296 -0
- package/src/REVIEW_2026_08_06/api-engine.md +195 -0
- package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +507 -0
- package/src/REVIEW_2026_08_06/core-binary.md +348 -0
- package/src/REVIEW_2026_08_06/core-collection.md +376 -0
- package/src/REVIEW_2026_08_06/core-math.md +221 -0
- package/src/REVIEW_2026_08_06/core-process.md +266 -0
- package/src/REVIEW_2026_08_06/core-science.md +223 -0
- package/src/REVIEW_2026_08_06/engine-ai-nav.md +279 -0
- package/src/REVIEW_2026_08_06/engine-animation.md +167 -0
- package/src/REVIEW_2026_08_06/engine-asset.md +166 -0
- package/src/REVIEW_2026_08_06/engine-ecs.md +920 -0
- package/src/REVIEW_2026_08_06/engine-input.md +197 -0
- package/src/REVIEW_2026_08_06/engine-misc.md +248 -0
- package/src/REVIEW_2026_08_06/engine-terrain.md +52 -0
- package/src/REVIEW_2026_08_06/generation.md +176 -0
- package/src/REVIEW_2026_08_06/geom-2d.md +434 -0
- package/src/REVIEW_2026_08_06/geom-3d.md +117 -0
- package/src/REVIEW_2026_08_06/gfx-gi.md +143 -0
- package/src/REVIEW_2026_08_06/gfx-render.md +740 -0
- package/src/REVIEW_2026_08_06/gfx-texture.md +690 -0
- package/src/REVIEW_2026_08_06/prior-criticals.md +49 -0
- package/src/REVIEW_2026_08_06/prior-highs.md +138 -0
- package/src/REVIEW_2026_08_06/test-gaps.md +364 -0
- package/src/REVIEW_2026_08_06/view.md +667 -0
- package/src/REVIEW_2026_08_06.md +571 -0
- package/src/core/binary/BinaryBuffer.d.ts.map +1 -1
- package/src/core/binary/BinaryBuffer.js +12 -1
- package/src/core/binary/BitImage2.d.ts +5 -1
- package/src/core/binary/BitImage2.d.ts.map +1 -1
- package/src/core/binary/BitImage2.js +5 -1
- package/src/core/bvh8/build/NodeProxy.d.ts.map +1 -1
- package/src/core/bvh8/build/NodeProxy.js +314 -308
- package/src/core/collection/list/List.d.ts +15 -0
- package/src/core/collection/list/List.d.ts.map +1 -1
- package/src/core/collection/list/List.js +34 -0
- package/src/core/collection/list/SortedListProjection.d.ts.map +1 -1
- package/src/core/collection/list/SortedListProjection.js +4 -1
- package/src/core/collection/map/BiMap.d.ts +25 -2
- package/src/core/collection/map/BiMap.d.ts.map +1 -1
- package/src/core/collection/map/BiMap.js +57 -3
- package/src/core/debug/matchers/IsAnything.d.ts.map +1 -1
- package/src/core/debug/matchers/IsAnything.js +5 -1
- package/src/core/events/signal/Signal.d.ts.map +1 -1
- package/src/core/events/signal/Signal.js +3 -1
- package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +433 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts +31 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.js +84 -0
- package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts +24 -2
- package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts.map +1 -1
- package/src/core/geom/2d/polygon/polygon2_signed_area.js +52 -34
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts +28 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.js +86 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts +40 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.js +1280 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts +28 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.js +77 -0
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts +6 -3
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts.map +1 -1
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.js +26 -13
- package/src/core/geom/2d/triangle/tri2_signed_area.d.ts +17 -1
- package/src/core/geom/2d/triangle/tri2_signed_area.d.ts.map +1 -1
- package/src/core/geom/2d/triangle/tri2_signed_area.js +35 -23
- package/src/core/geom/2d/v2_morton_encode.d.ts +20 -0
- package/src/core/geom/2d/v2_morton_encode.d.ts.map +1 -0
- package/src/core/geom/2d/v2_morton_encode.js +23 -0
- package/src/core/geom/3d/atlas/segment/atlas_merge_charts.d.ts.map +1 -1
- package/src/core/geom/3d/atlas/segment/atlas_merge_charts.js +4 -1
- package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts +27 -0
- package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts.map +1 -0
- package/src/core/geom/3d/polygon/polygon3_compute_normal.js +60 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts +34 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts.map +1 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.js +94 -0
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +8 -2
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -1
- package/src/core/geom/3d/quaternion/quat3_multiply.js +8 -2
- package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/ConvexHullShape3D.js +32 -2
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts +23 -6
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.js +268 -124
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts +17 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.js +44 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts +20 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.js +40 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts +19 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.js +34 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts +22 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.js +37 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts +87 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.js +599 -0
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts +32 -0
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts.map +1 -1
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.js +66 -7
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts +22 -1
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts.map +1 -1
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.js +83 -38
- package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +554 -0
- package/src/core/geom/vec3/v3_cotangent.d.ts +6 -2
- package/src/core/geom/vec3/v3_cotangent.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_cotangent.js +6 -2
- package/src/core/geom/vec3/v3_cross.d.ts +6 -2
- package/src/core/geom/vec3/v3_cross.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_cross.js +6 -2
- package/src/core/geom/vec3/v3_multiply.d.ts +6 -2
- package/src/core/geom/vec3/v3_multiply.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_multiply.js +6 -2
- package/src/core/geom/vec3/v3_subtract.d.ts +6 -2
- package/src/core/geom/vec3/v3_subtract.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_subtract.js +6 -2
- package/src/core/localization/Localization.js +1 -1
- package/src/core/math/random/randomSeed.d.ts +13 -0
- package/src/core/math/random/randomSeed.d.ts.map +1 -0
- package/src/core/math/random/randomSeed.js +16 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.d.ts +50 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.js +107 -0
- package/src/core/math/spline/spline3_hermite_reverse.d.ts +34 -0
- package/src/core/math/spline/spline3_hermite_reverse.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_reverse.js +49 -0
- package/src/core/math/statistics/hammersley_sequence.d.ts +17 -4
- package/src/core/math/statistics/hammersley_sequence.d.ts.map +1 -1
- package/src/core/math/statistics/hammersley_sequence.js +25 -16
- package/src/core/process/Future.d.ts.map +1 -1
- package/src/core/process/Future.js +12 -0
- package/src/core/process/PromiseWatcher.d.ts.map +1 -1
- package/src/core/process/PromiseWatcher.js +9 -2
- package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
- package/src/core/process/executor/ConcurrentExecutor.js +12 -2
- package/src/core/process/worker/WorkerProxy.d.ts.map +1 -1
- package/src/core/process/worker/WorkerProxy.js +5 -2
- package/src/core/process/worker/extractTransferables.d.ts.map +1 -1
- package/src/core/process/worker/extractTransferables.js +4 -1
- package/src/engine/Engine.d.ts +12 -12
- package/src/engine/Engine.d.ts.map +1 -1
- package/src/engine/Engine.js +149 -10
- package/src/engine/EngineHarness.d.ts.map +1 -1
- package/src/engine/EngineHarness.js +4 -1
- package/src/engine/asset/loaders/ArrayBufferLoader.d.ts.map +1 -1
- package/src/engine/asset/loaders/ArrayBufferLoader.js +7 -0
- package/src/engine/ecs/speaker/lines/sets/LineSetDescription.d.ts.map +1 -1
- package/src/engine/ecs/speaker/lines/sets/LineSetDescription.js +3 -1
- package/src/engine/ecs/terrain/ecs/TerrainSystem.d.ts.map +1 -1
- package/src/engine/ecs/terrain/ecs/TerrainSystem.js +4 -1
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts +29 -0
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts.map +1 -1
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.js +84 -23
- package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +446 -0
- package/src/engine/graphics/GraphicsEngine.d.ts +56 -7
- package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
- package/src/engine/graphics/GraphicsEngine.js +196 -24
- package/src/engine/graphics/context/WebGLContextFailureReason.d.ts +9 -0
- package/src/engine/graphics/context/WebGLContextFailureReason.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextFailureReason.js +17 -0
- package/src/engine/graphics/context/WebGLContextMonitor.d.ts +106 -0
- package/src/engine/graphics/context/WebGLContextMonitor.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextMonitor.js +341 -0
- package/src/engine/graphics/context/WebGLContextState.d.ts +14 -0
- package/src/engine/graphics/context/WebGLContextState.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextState.js +24 -0
- package/src/engine/graphics/context/testWebGLContextLoss.d.ts +2 -0
- package/src/engine/graphics/context/testWebGLContextLoss.d.ts.map +1 -0
- package/src/engine/graphics/context/testWebGLContextLoss.js +696 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.d.ts.map +1 -1
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.js +68 -32
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts +14 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts.map +1 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.js +41 -0
- package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.js +1 -1
- package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js +5 -0
- package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts +5 -1
- package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2D.js +15 -0
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.js +2 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts +4 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.js +488 -476
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.js +280 -253
- package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.d.ts.map +1 -1
- package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.js +3 -2
- package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.d.ts.map +1 -1
- package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.js +3 -1
- package/src/engine/graphics/geometry/computeMeshSurfaceArea.js +1 -1
- package/src/engine/graphics/load_and_set_cubemap_v0.d.ts +5 -0
- package/src/engine/graphics/load_and_set_cubemap_v0.d.ts.map +1 -1
- package/src/engine/graphics/load_and_set_cubemap_v0.js +26 -21
- package/src/engine/graphics/particles/particular/group/ParticleGroup.d.ts.map +1 -1
- package/src/engine/graphics/particles/particular/group/ParticleGroup.js +19 -8
- package/src/engine/graphics/particles/particular/group/optimizeCommandQueue.js +1 -1
- package/src/engine/graphics/texture/atlas/AtlasLookupTexture.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/AtlasLookupTexture.js +10 -3
- package/src/engine/graphics/texture/atlas/TextureAtlas.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/TextureAtlas.js +4 -1
- package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.js +11 -0
- package/src/engine/graphics/texture/cubemap/load_environment_map.d.ts.map +1 -1
- package/src/engine/graphics/texture/cubemap/load_environment_map.js +5 -2
- package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.js +26 -18
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts +15 -2
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.js +29 -3
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts +11 -1
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.js +158 -171
- package/src/engine/graphics/texture/sampler/sampler2d_paint.js +1 -1
- package/src/engine/graphics/texture/sampler/util/bitSet2Sampler2D.js +2 -2
- package/src/engine/input/ecs/util/TopDownCameraControllerHelper.d.ts.map +1 -1
- package/src/engine/input/ecs/util/TopDownCameraControllerHelper.js +10 -3
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts +14 -0
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts.map +1 -0
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.js +38 -0
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +433 -425
- package/src/engine/physics/constraint/solve_constraints.d.ts +4 -1
- package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
- package/src/engine/physics/constraint/solve_constraints.js +38 -13
- package/src/engine/physics/contact/ManifoldStore.d.ts +15 -3
- package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
- package/src/engine/physics/contact/ManifoldStore.js +15 -3
- package/src/engine/physics/ecs/Joint.d.ts +7 -4
- package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
- package/src/engine/physics/ecs/Joint.js +7 -4
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +14 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +16 -0
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
- package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +15 -7
- package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/convex_convex_manifold.js +33 -10
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +6 -2
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/refine_ray_concave.js +6 -2
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +6 -2
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/refine_ray_hit.js +6 -2
- package/src/engine/physics/queries/raycast.d.ts.map +1 -1
- package/src/engine/physics/queries/raycast.js +11 -4
- package/src/engine/save/storage/IndexedDBStorage.d.ts.map +1 -1
- package/src/engine/save/storage/IndexedDBStorage.js +21 -3
- package/src/engine/simulation/Ticker.d.ts +12 -0
- package/src/engine/simulation/Ticker.d.ts.map +1 -1
- package/src/engine/simulation/Ticker.js +18 -0
- package/src/engine/sound/simulation/AcousticSimulator.d.ts.map +1 -1
- package/src/engine/sound/simulation/AcousticSimulator.js +9 -1
- package/src/engine/sound/simulation/core/VolumeField.d.ts +6 -2
- package/src/engine/sound/simulation/core/VolumeField.d.ts.map +1 -1
- package/src/engine/sound/simulation/core/VolumeField.js +6 -2
- package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts +6 -2
- package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/AcousticProbeField.js +6 -2
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts +6 -2
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.js +6 -2
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts +3 -1
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.js +3 -1
- package/src/engine/ui/GUIEngine.d.ts.map +1 -1
- package/src/engine/ui/GUIEngine.js +8 -1
- package/src/generation/theme/TerrainTheme.d.ts.map +1 -1
- package/src/generation/theme/TerrainTheme.js +2 -1
- package/src/generation/theme/ThemeEngine.d.ts.map +1 -1
- package/src/generation/theme/ThemeEngine.js +6 -5
- package/src/view/ViewGroup.d.ts.map +1 -1
- package/src/view/ViewGroup.js +9 -7
- package/src/view/graphics/WebGLContextFailureView.d.ts +19 -0
- package/src/view/graphics/WebGLContextFailureView.d.ts.map +1 -0
- package/src/view/graphics/WebGLContextFailureView.js +76 -0
- package/src/view/minimap/Minimap.d.ts +8 -0
- package/src/view/minimap/Minimap.d.ts.map +1 -1
- package/src/view/minimap/Minimap.js +16 -3
- package/src/view/minimap/dom/MinimapMarkerView.d.ts.map +1 -1
- package/src/view/minimap/dom/MinimapMarkerView.js +3 -1
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
# Correctness review — `engine/ecs`
|
|
2
|
+
|
|
3
|
+
Scope: every non-spec source file under `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/`
|
|
4
|
+
(208 source files). Reviewed 2026-08-06.
|
|
5
|
+
|
|
6
|
+
Findings marked **Confirmed** were either reproduced by executing the code or read directly
|
|
7
|
+
off the source with an unambiguous trace. `assert.*` is treated as absent (stripped in prod).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Core (Entity / EntityComponentDataset / EntityManager / System)
|
|
12
|
+
|
|
13
|
+
### ECS-1 `Entity.readFromDataset` never watches destruction, so a stale `Built` flag corrupts recycled entity ids
|
|
14
|
+
- **Severity**: CRITICAL
|
|
15
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/Entity.js:508-525`
|
|
16
|
+
- **Defect**: `build()` registers `#handleEntityDestroyed` on `EventType.EntityRemoved` when the
|
|
17
|
+
`WatchDestruction` flag is set (line 487-489), which is on by default. `readFromDataset` sets
|
|
18
|
+
`EntityFlags.Built` and binds the reference but registers no such listener, so the returned
|
|
19
|
+
`Entity` keeps reporting `isBuilt === true` forever after the underlying entity is removed.
|
|
20
|
+
Since `EntityComponentDataset.createEntity` recycles ids (`entityOccupancy.nextClearBit(0)`),
|
|
21
|
+
every subsequent operation on that `Entity` lands on a *different* entity.
|
|
22
|
+
- **Failure scenario** (reproduced):
|
|
23
|
+
```js
|
|
24
|
+
const ds = new EntityComponentDataset();
|
|
25
|
+
ds.setComponentTypeMap([A, B]);
|
|
26
|
+
const id = ds.createEntity(); // id = 0
|
|
27
|
+
ds.addComponentToEntity(id, new A());
|
|
28
|
+
const e = Entity.readFromDataset(id, ds);
|
|
29
|
+
ds.removeEntity(id);
|
|
30
|
+
const recycled = ds.createEntity(); // id = 0 again, generation 1
|
|
31
|
+
e.isBuilt // => true (should be false)
|
|
32
|
+
e.add(new B()); // => writes B onto `recycled`, an unrelated entity
|
|
33
|
+
e.destroy(); // => destroys `recycled`
|
|
34
|
+
```
|
|
35
|
+
Output observed: `recycled entity now has B: true`, `after e.destroy(), recycled entity exists: false`.
|
|
36
|
+
The `EntityReference` already carries the generation needed to detect this (`e.generation === 0`
|
|
37
|
+
vs `ds.getEntityGeneration(recycled) === 1`) but nothing consults it.
|
|
38
|
+
- **Fix**: in `readFromDataset`, mirror `build()` — after setting the `Built` flag, do
|
|
39
|
+
`if (r.getFlag(EntityFlags.WatchDestruction)) dataset.addEntityEventListener(entity, EventType.EntityRemoved, r.#handleEntityDestroyed, r);`
|
|
40
|
+
(needs a small private accessor since `#handleEntityDestroyed` is instance-private).
|
|
41
|
+
- **Confidence**: Confirmed
|
|
42
|
+
|
|
43
|
+
### ECS-2 `setComponentTypeMap` leaves the dataset permanently corrupted when observer rebuild throws
|
|
44
|
+
- **Severity**: MAJOR
|
|
45
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/EntityComponentDataset.js:674-731`
|
|
46
|
+
- **Defect**: `setComponentTypeMap` mutates state in four steps — `updateComponentOccupancy` (716),
|
|
47
|
+
`updateComponentStores` (718), `updateObservers` (720), then `componentTypeMap` / `componentTypeCount`
|
|
48
|
+
(722, 730). `updateObservers` calls `observer.build(map)` for every observer sitting in a *surviving*
|
|
49
|
+
component's store; `EntityObserver.build` **throws** if any of that observer's component types is
|
|
50
|
+
absent from the new map (`EntityObserver.js:102-104`). Because occupancy and the component stores
|
|
51
|
+
have already been re-strided at that point, the throw leaves the dataset with the new stride but the
|
|
52
|
+
old `componentTypeMap` / `componentTypeCount` / `__type_to_index_map`.
|
|
53
|
+
- **Failure scenario** (reproduced):
|
|
54
|
+
```js
|
|
55
|
+
ds.setComponentTypeMap([A, B]);
|
|
56
|
+
ds.addObserver(new EntityObserver([A, B], onComplete, onBroken), false);
|
|
57
|
+
ds.unregisterComponentType(A); // A has no live instances, so the removal check passes
|
|
58
|
+
```
|
|
59
|
+
Observed: throws `Component type[0] was not found in the supplied map`, then
|
|
60
|
+
`componentTypeMap = ['A','B']`, `componentTypeCount = 2`, but `components.length = 1`.
|
|
61
|
+
The next `ds.addComponentToEntity(e, new B())` throws
|
|
62
|
+
`TypeError: Cannot read properties of undefined (reading '0')` — `components[1]` no longer exists.
|
|
63
|
+
The dataset cannot be used again.
|
|
64
|
+
- **Fix**: validate first, mutate second. Before touching occupancy/stores, iterate every registered
|
|
65
|
+
observer and check `map` contains all of `observer.componentTypes`; throw there (dataset untouched).
|
|
66
|
+
Observers that watch only removed types should be disconnected explicitly (`observer.dataset = null`)
|
|
67
|
+
rather than silently dropped from the stores.
|
|
68
|
+
- **Confidence**: Confirmed
|
|
69
|
+
|
|
70
|
+
### ECS-3 `traverseEntitiesCompactedFiltered` uses `-1` as a component index for unregistered classes
|
|
71
|
+
- **Severity**: MAJOR
|
|
72
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/EntityComponentDataset.js:1990-2020`
|
|
73
|
+
- **Defect**: `componentIndices[i] = this.computeComponentTypeIndex(klass)` stores `-1` for a class
|
|
74
|
+
that is not registered in this dataset, and nothing filters it out. The loop then does
|
|
75
|
+
`componentOccupancy.get(entityOccupancyAddress + (-1))` — reading the *last* component bit of the
|
|
76
|
+
*previous* entity — and, when that bit happens to be set, `this.components[-1][i]`.
|
|
77
|
+
`this.components[-1]` is `undefined`. (In production `BitSet.get`'s `assert.isNonNegativeInteger`
|
|
78
|
+
is stripped, so it silently returns the neighbouring entity's bit instead of throwing.)
|
|
79
|
+
- **Failure scenario** (reproduced): dataset registered with `[A, B]`; entity 0 has both A and B;
|
|
80
|
+
entity 1 has A. `ds.traverseEntitiesCompactedFiltered([C, A], visitor)` where `C` was never
|
|
81
|
+
registered. With asserts on it throws `bitIndex must be >= 0, instead was -1`. With asserts stripped,
|
|
82
|
+
entity 1 reads bit `2 - 1 = 1` (entity 0's B bit, which is set) and then dereferences
|
|
83
|
+
`this.components[-1][1]` → `TypeError: Cannot read properties of undefined`.
|
|
84
|
+
- **Fix**: skip unregistered classes when building `componentIndices` (build a compacted array of
|
|
85
|
+
valid indices only), the same way `traverseEntities` returns early on `-1`.
|
|
86
|
+
- **Confidence**: Confirmed
|
|
87
|
+
|
|
88
|
+
### ECS-4 Bare `.sort()` on component indices is lexicographic
|
|
89
|
+
- **Severity**: MINOR
|
|
90
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/EntityComponentDataset.js:1996`
|
|
91
|
+
- **Defect**: `componentIndices.sort()` with no comparator sorts numbers as strings. The comment
|
|
92
|
+
says "sort component indices array for faster traversal"; with 10 or more registered component
|
|
93
|
+
types the array is not sorted at all, and the `components` array handed to the visitor comes out
|
|
94
|
+
in a scrambled, size-dependent order.
|
|
95
|
+
- **Failure scenario** (reproduced): 12 registered component types; an entity holds instances of
|
|
96
|
+
type index 2 and type index 10. The visitor receives `[K10, K2]` instead of `[K2, K10]`. Any
|
|
97
|
+
consumer of this "main utility … to facilitate serialization" method that depends on a stable
|
|
98
|
+
component ordering (e.g. a byte-for-byte save comparison or a diff) sees the order flip as soon
|
|
99
|
+
as the dataset crosses 10 component types.
|
|
100
|
+
- **Fix**: `componentIndices.sort((a, b) => a - b);`
|
|
101
|
+
- **Confidence**: Confirmed
|
|
102
|
+
|
|
103
|
+
### ECS-5 `Entity.getComponentSafe` throws `ReferenceError` — its helper is neither imported nor exported
|
|
104
|
+
- **Severity**: MINOR
|
|
105
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/Entity.js:256`
|
|
106
|
+
- **Defect**: The error path calls `computeComponentClassName(klass)`, but `Entity.js` imports only
|
|
107
|
+
`assert`, `Signal`, `EntityFlags`, `EntityReference`, `EventType`. Worse,
|
|
108
|
+
`computeComponentClassName.js` declares the function without any `export` statement at all, so
|
|
109
|
+
there is nothing to import.
|
|
110
|
+
- **Failure scenario** (reproduced): `new Entity().getComponentSafe(SomeClass)` throws
|
|
111
|
+
`ReferenceError: computeComponentClassName is not defined` instead of the documented
|
|
112
|
+
`Error: Component of given class 'SomeClass' not found`. The diagnostic naming the missing
|
|
113
|
+
component — the entire point of the method over `getComponent` — is lost.
|
|
114
|
+
- **Fix**: add `export` to `computeComponentClassName.js` and import it in `Entity.js`.
|
|
115
|
+
- **Confidence**: Confirmed
|
|
116
|
+
|
|
117
|
+
### ECS-6 Re-`build()`ing an `Entity` whose components drifted orphans the old entity and mis-aims its destruction watcher
|
|
118
|
+
- **Severity**: MINOR
|
|
119
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/Entity.js:442-493`
|
|
120
|
+
- **Defect**: `build()` returns early only when `checkExistingComponents` passes. Otherwise it falls
|
|
121
|
+
through and calls `dataset.createEntity()` **without** removing the entity it was previously bound
|
|
122
|
+
to and without unregistering the `EntityRemoved` listener registered on the old id at line 488.
|
|
123
|
+
- **Failure scenario** (reproduced):
|
|
124
|
+
```js
|
|
125
|
+
const e = new Entity(); e.add(new A());
|
|
126
|
+
const id1 = e.build(ds); // 0
|
|
127
|
+
ds.removeComponentFromEntity(id1, A); // a system strips the component directly
|
|
128
|
+
const id2 = e.build(ds); // 1 -- entity 0 is now orphaned, entityCount = 2
|
|
129
|
+
ds.removeEntity(id1); // fires the stale watcher
|
|
130
|
+
e.isBuilt // => false, although entity 1 is alive
|
|
131
|
+
e.destroy() // => false; entity 1 leaks permanently
|
|
132
|
+
```
|
|
133
|
+
- **Fix**: when the early-return check fails but `EntityFlags.Built` is set, call `this.destroy()`
|
|
134
|
+
first so the previous entity and its listener are released before rebinding.
|
|
135
|
+
- **Confidence**: Confirmed
|
|
136
|
+
|
|
137
|
+
### ECS-7 `EntityManager.removeSystem` never clears `system.entityManager`
|
|
138
|
+
- **Severity**: MINOR
|
|
139
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/EntityManager.js:590-631`
|
|
140
|
+
- **Defect**: `startSystem` refuses to start a system whose `entityManager` is set to a different
|
|
141
|
+
manager (`EntityManager.js:739-741`). Neither `removeSystem` nor `stopSystem` resets the field.
|
|
142
|
+
- **Failure scenario**: `em1.removeSystem(s)` then `em2.addSystem(s); em2.startup()` →
|
|
143
|
+
`startSystem` throws `System is bound to another EntityManager`, caught at line 763, so
|
|
144
|
+
`errorCallback` fires and `em2.state` becomes `EntityManagerState.Failed`; the whole manager
|
|
145
|
+
never reaches `Running`. Reusing a system instance across manager instances (level reload, test
|
|
146
|
+
fixtures) is impossible.
|
|
147
|
+
- **Fix**: set `system.entityManager = null` in `removeSystem` after `stopSystem` resolves.
|
|
148
|
+
- **Confidence**: Confirmed
|
|
149
|
+
|
|
150
|
+
### ECS-8 `system_validate_class` duplicate-component backtrace is off by one
|
|
151
|
+
- **Severity**: MINOR
|
|
152
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/system_validate_class.js:49`
|
|
153
|
+
- **Defect**: `for (let j = 0; j < i - 1; j++)` never compares `components_used[i]` against
|
|
154
|
+
`components_used[i - 1]`, so adjacent duplicate resource specifications slip through.
|
|
155
|
+
- **Failure scenario**: `components_used = [Spec(Transform, Read), Spec(Transform, Write)]` passes
|
|
156
|
+
validation. `System.getAccessForComponent` (`System.js:110-117`) `break`s at the first match, so
|
|
157
|
+
the `Write` access is silently discarded, and `EntityManager.updateExecutionOrder`'s `scoreSystem`
|
|
158
|
+
scores the system as a reader (`attached_edge_count` instead of `× 2`) — the system is scheduled
|
|
159
|
+
after the systems that should run after it.
|
|
160
|
+
- **Fix**: `for (let j = 0; j < i; j++)`.
|
|
161
|
+
- **Confidence**: Confirmed
|
|
162
|
+
|
|
163
|
+
### ECS-48 `EntityManager.startup` reports `Running` while a system added during startup is still `STARTING`
|
|
164
|
+
- **Severity**: MINOR
|
|
165
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/EntityManager.js:795,814,567-574`
|
|
166
|
+
- **Defect**: `startup()` captures `expectedReadyCount = systems.length` before the start loop, then
|
|
167
|
+
`finalizeStartup()` fires when `readyCount === expectedReadyCount`. A system's own `startup()` may
|
|
168
|
+
call `entityManager.addSystem(...)`, which pushes onto the same `systems` array the `for…of` loop is
|
|
169
|
+
walking, so the loop does start the newcomer — but the target count is stale by one. Separately,
|
|
170
|
+
`addSystem` only auto-starts when `this.state === EntityManagerState.Running`; during
|
|
171
|
+
`EntityManagerState.Starting` it returns `Promise.resolve()` and never calls `startSystem`, so a
|
|
172
|
+
system added after the loop has drained is never started at all.
|
|
173
|
+
- **Failure scenario**: `SGMeshSystem.startup(em)` does
|
|
174
|
+
`if (!em.hasSystem(TransformAttachmentSystem)) await em.addSystem(new TransformAttachmentSystem());`
|
|
175
|
+
(`engine/graphics/ecs/mesh-v2/aggregate/SGMeshSystem.js:94-96`). With N systems registered,
|
|
176
|
+
`expectedReadyCount = N`; once the original N have reported ready, `finalizeStartup()` sets
|
|
177
|
+
`state = Running` and invokes `readyCallback()` while `TransformAttachmentSystem` is still in
|
|
178
|
+
`STARTING`. The caller begins `simulate()`; `updateExecutionOrder` skips non-`RUNNING` systems and its
|
|
179
|
+
observer has not been added to the dataset, so attachments silently do not update for those frames.
|
|
180
|
+
If the same `addSystem` were made from after an `await` (once the start loop has drained) the system
|
|
181
|
+
would never start at all, while its promise still resolves.
|
|
182
|
+
- **Fix**: read `systems.length` live at each `systemReady` (`readyCount === systems.length`) instead of
|
|
183
|
+
a captured count, and treat `EntityManagerState.Starting` like `Running` in `addSystem`'s auto-start
|
|
184
|
+
branch.
|
|
185
|
+
- **Confidence**: Confirmed
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Fog of war
|
|
190
|
+
|
|
191
|
+
### ECS-9 `FogOfWarSystem.shutdown` removes none of the three render-loop subscriptions it made in `startup`
|
|
192
|
+
- **Severity**: MAJOR
|
|
193
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/fow/FogOfWarSystem.js:162-219`
|
|
194
|
+
- **Defect**: `startup` registers three anonymous closures on `graphics.on.preRender`,
|
|
195
|
+
`graphics.on.buffersRendered` and `graphics.on.postRender`. `shutdown` only calls
|
|
196
|
+
`this.graphics.layerComposer.remove(this.compositLayer)`. Because the handlers are inline
|
|
197
|
+
functions with no retained reference, they can never be removed even by a caller.
|
|
198
|
+
- **Failure scenario**: `entityManager.shutdown()` (or `removeSystem(fogOfWarSystem)`) removes the
|
|
199
|
+
composit layer, but on the very next frame the `preRender` handler still runs
|
|
200
|
+
`compositerLayer.enable()` on the removed layer and `depthFrameBuffer.referenceCount++`, and
|
|
201
|
+
`buffersRendered` still runs `fogOfWarRenderer.render(renderer, camera, scene, compositerLayer.renderTarget)`
|
|
202
|
+
into a render target that is no longer composited — burning a full-screen pass every frame for the
|
|
203
|
+
lifetime of the `GraphicsEngine`. The closures also pin `entityManager`, `graphics` and the
|
|
204
|
+
`FogOfWarRenderer` (its `ShaderMaterial`, `Mesh` and `Scene` are never disposed). Starting a second
|
|
205
|
+
`FogOfWarSystem` — e.g. a level reload on a shared `GraphicsEngine` — stacks a second set of handlers,
|
|
206
|
+
so each frame renders the fog twice.
|
|
207
|
+
- **Fix**: hoist the three handlers into named fields (or wrap them in `SignalBinding`s) in `startup`,
|
|
208
|
+
`remove` them in `shutdown`, and dispose `fogOfWarRenderer.material` / `fogOfWarRenderer.quad`.
|
|
209
|
+
- **Confidence**: Confirmed
|
|
210
|
+
|
|
211
|
+
### ECS-10 A revealer on the *wrong* team still reveals fog whenever its radius changes
|
|
212
|
+
- **Severity**: MAJOR
|
|
213
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/fow/FogOfWarRevealerSystem.js:52-97`
|
|
214
|
+
- **Defect**: The team check lives only in `attemptToWatch`, which gates the *position* watcher.
|
|
215
|
+
`update()` itself contains no team test, and `radiusWatcher` (line 93-97) is linked
|
|
216
|
+
unconditionally for every linked entity regardless of team.
|
|
217
|
+
- **Failure scenario**: `new FogOfWarRevealerSystem(/* team */ 1)`. An enemy unit with
|
|
218
|
+
`Team = 2`, `Transform`, `FogOfWarRevealer` links: `attemptToWatch(2)` correctly leaves
|
|
219
|
+
`positionWatcher` unlinked, but `radiusWatcher.link()` runs anyway. The enemy then receives a
|
|
220
|
+
vision buff (`revealer.radius.set(8)`) — `revealer.radius.onChanged` fires `update()`, which maps
|
|
221
|
+
the *enemy's* position to the grid and calls `fow.reveal(...)` on the player's fog. The player's
|
|
222
|
+
map is permanently uncovered at the enemy's location.
|
|
223
|
+
- **Fix**: put the team check inside `update()` (`if (team.getValue() !== visibilityTeam) return;`),
|
|
224
|
+
or link/unlink `radiusWatcher` from `attemptToWatch` alongside `positionWatcher`.
|
|
225
|
+
- **Confidence**: Confirmed
|
|
226
|
+
|
|
227
|
+
### ECS-11 `computeRevealedGridBoundingRectangle` returns an inverted AABB when nothing is revealed
|
|
228
|
+
- **Severity**: MINOR
|
|
229
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/fow/FogOfWar.js:532-563`
|
|
230
|
+
- **Defect**: `x0`/`y0` seed to `width - 1` / `height - 1` and `x1`/`y1` to `0`. If no cell differs
|
|
231
|
+
from 255 the loop body never runs and those seeds are written out unchanged, producing
|
|
232
|
+
`x0 > x1` and `y0 > y1`.
|
|
233
|
+
- **Failure scenario**: a freshly resized 64×64 fog (sampler 66×66, all 255) →
|
|
234
|
+
`result.set(65, 65, 0, 0)`. `MinimapFogOfWar.updateFocusArea`
|
|
235
|
+
(`view/minimap/gl/MinimapFogOfWar.js:87`) then does `bounds.grow(2)`, `bounds.move(-0.5, 0.5)`
|
|
236
|
+
and multiplies by the scale, feeding a negative-extent rectangle into `enforceAspectRatio` — the
|
|
237
|
+
minimap focus area is garbage on the first frame of any map before the first reveal.
|
|
238
|
+
- **Fix**: track a `found` flag and, when nothing is revealed, emit an explicitly empty rectangle
|
|
239
|
+
(`result.set(0, 0, 0, 0)`) or leave `result` untouched and return a boolean.
|
|
240
|
+
- **Confidence**: Confirmed
|
|
241
|
+
|
|
242
|
+
### ECS-12 `getWorldClearance` reads the distance sampler unclamped
|
|
243
|
+
- **Severity**: MINOR
|
|
244
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/fow/FogOfWar.js:677-684`
|
|
245
|
+
- **Defect**: Its sibling `computeAABBVisibility` clamps the sample position before
|
|
246
|
+
`distanceSampler.readChannel` (line 628-629); `getWorldClearance` does not.
|
|
247
|
+
`Sampler2D.readChannel` computes `(y * width + x) * itemSize + channel` with no bounds check
|
|
248
|
+
(the `assert`s are stripped), so an x outside `[0, width)` silently indexes into an adjacent row.
|
|
249
|
+
- **Failure scenario**: `FogOfWarVisibilityPredicate.test(x, y, z)` (line 41) is called with an
|
|
250
|
+
object's world position. For a 64×64 fog with `scale = 1`, an object one cell east of the map at
|
|
251
|
+
grid row 20 produces `sampleX = 65`, `sampleY = 21`, giving index `21 * 66 + 65 = 1451` — which is
|
|
252
|
+
cell `(1, 22)`, the *west* border of the row below. The object's visibility is decided by an
|
|
253
|
+
unrelated cell; where that cell is revealed, an off-map object that should be hidden renders.
|
|
254
|
+
- **Fix**: clamp exactly as `computeAABBVisibility` does, using the sampler's own dimensions.
|
|
255
|
+
- **Confidence**: Confirmed
|
|
256
|
+
|
|
257
|
+
### ECS-13 `computeAABBVisibility` clamps to `size - 1` but the distance sampler is `size + 2` wide
|
|
258
|
+
- **Severity**: MINOR
|
|
259
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/fow/FogOfWar.js:628-629`
|
|
260
|
+
- **Defect**: `worldToSamplePosition` emits sampler-space coordinates with the +1 border offset
|
|
261
|
+
baked in, so the real world area occupies sampler columns `[1, size.x]`. The clamp bound is
|
|
262
|
+
`this.size.x - 1`, two cells short of the valid maximum.
|
|
263
|
+
- **Failure scenario**: 64×64 fog, `scale = 1`. `worldToSamplePosition` maps the world extent onto
|
|
264
|
+
sample coordinates `[1, 64]`, but the clamp caps at `63`. An object at the far eastern edge maps to
|
|
265
|
+
`sampleX = 64` and is read at column `63` instead — the outermost column/row of the map takes its
|
|
266
|
+
clearance from the cell one step inward. An unexplored strip along the east/south map edge therefore
|
|
267
|
+
reports the clearance of the explored cell beside it, and objects standing there are drawn through
|
|
268
|
+
the fog.
|
|
269
|
+
- **Fix**: clamp against the sampler, not the logical size: `clamp(sampleX, 0, this.sampler.width - 1)`.
|
|
270
|
+
- **Confidence**: Confirmed
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Terrain
|
|
275
|
+
|
|
276
|
+
### ECS-14 `getTileByWorldPosition2D` reads the vertical axis as the grid row
|
|
277
|
+
- **Severity**: CRITICAL
|
|
278
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/terrain/tiles/TerrainTileManager.js:461-472`
|
|
279
|
+
- **Defect**: The point is packed as `Float32Array.of(x, 0, y)` — so the top-down Y lands at index
|
|
280
|
+
**2** — inverse-transformed, and then passed as `getRawTileByPosition(v3[0], v3[1])`. `v3[1]` is
|
|
281
|
+
the vertical component. Separately, `getRawTileByPosition` divides by `tileSize`, which is in grid
|
|
282
|
+
units, while `v3` is in object units, so `this.scale` must be divided out first.
|
|
283
|
+
- **Failure scenario**: 64×64 terrain, `gridScale = 1`, tile size 32 → a 2×2 tile grid, identity
|
|
284
|
+
transform. `getTileByWorldPosition2D(40, 40)` → `getRawTileByPosition(40, 0)` →
|
|
285
|
+
`tileX = 1, tileY = floor(0 / 32) = 0` → tile **(1,0)** instead of **(1,1)**. With an identity
|
|
286
|
+
transform the row is *always* 0, so every lookup collapses onto the top row of tiles.
|
|
287
|
+
`obtainTileAtWorldPosition2D` then builds the wrong tile.
|
|
288
|
+
- **Fix**: `return this.getRawTileByPosition(v3[0] / this.scale.x, v3[2] / this.scale.y);`
|
|
289
|
+
- **Confidence**: Confirmed
|
|
290
|
+
|
|
291
|
+
### ECS-15 `traverseVisibleTiles` sums per-frustum hit counts but always writes at offset 0
|
|
292
|
+
- **Severity**: MAJOR
|
|
293
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/terrain/ecs/TerrainSystem.js:265-290`
|
|
294
|
+
- **Defect**: `tile_count` is initialised to 0 and passed as the `result_offset` to
|
|
295
|
+
`bvh_query_user_data_intersects_frustum` on every iteration but never advanced, so each frustum
|
|
296
|
+
overwrites the previous frustum's results from index 0. `found_tiles` meanwhile accumulates the
|
|
297
|
+
per-frustum counts, and the consumer loop iterates to that sum.
|
|
298
|
+
- **Failure scenario**: two active `Camera` components (`CameraSystem.getActiveFrustums` yields one
|
|
299
|
+
frustum per active camera). Frustum A returns 6 tiles, frustum B returns 4 → `tiles` holds 6
|
|
300
|
+
entries but `found_tiles = 10`. Indices 6..9 are `undefined` →
|
|
301
|
+
`tileManager.tiles[undefined] === undefined` → the callback dereferences `tile.isBuilt` /
|
|
302
|
+
`tile.gridPosition` → `TypeError` out of `TerrainSystem.update` every frame.
|
|
303
|
+
- **Fix**: `tile_count += bvh_query_user_data_intersects_frustum(tiles, tile_count, ...)` and iterate
|
|
304
|
+
to `tile_count`.
|
|
305
|
+
- **Confidence**: Confirmed
|
|
306
|
+
|
|
307
|
+
### ECS-16 `TerrainOverlay.paintSampler` passes the overlay instead of its sampler
|
|
308
|
+
- **Severity**: MAJOR
|
|
309
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/terrain/overlay/TerrainOverlay.js:321`
|
|
310
|
+
- **Defect**: `sampler2d_paint(this, scaled_source, ...)` supplies the `TerrainOverlay` as the
|
|
311
|
+
destination `Sampler2D`. The pixel data lives on `this.sampler`.
|
|
312
|
+
- **Failure scenario**: `sampler2d_paint` computes
|
|
313
|
+
`_w = Math.min(width, source.width - 0, undefined - dx)` → `NaN`, so `for (x = 0; x < NaN; ...)`
|
|
314
|
+
never executes. Nothing is written and no error is raised — `paintImage`/`paintSampler` are silent
|
|
315
|
+
no-ops, while `texture.needsUpdate = true` re-uploads unchanged pixels every call.
|
|
316
|
+
- **Fix**: `sampler2d_paint(this.sampler, scaled_source, 0, 0, dx, dy, dWidth, dHeight);`
|
|
317
|
+
- **Confidence**: Confirmed
|
|
318
|
+
|
|
319
|
+
### ECS-17 Tile rebuild leaks the previous `BufferGeometry`
|
|
320
|
+
- **Severity**: MAJOR
|
|
321
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/terrain/tiles/TerrainTile.js:551,586`
|
|
322
|
+
- **Defect**: `build()` assigns `this.geometry = g` / `mesh.geometry = g` without disposing the
|
|
323
|
+
geometry it replaces. The only disposal path, `dispose()`, early-returns on `if (!this.isBuilt) return;`
|
|
324
|
+
— and `TerrainTileManager.rebuildTilesByUV` (`tiles/TerrainTileManager.js:300`) clears `isBuilt`
|
|
325
|
+
while the geometry is still attached, so the old geometry can never be released.
|
|
326
|
+
- **Failure scenario**: terrain height editing calls `rebuildTilesByUV`, clearing `isBuilt` on every
|
|
327
|
+
overlapping tile; `TerrainSystem.update` then re-invokes `tile.build(tileData)`, installing a fresh
|
|
328
|
+
geometry and orphaning the old VBO/IBO. For a 4-tile region at `resolution = 4` with 32×32 tiles
|
|
329
|
+
(16 384 verts, ~590 KB of attributes each) that is ~2.4 MB of VRAM per edit, never reclaimed.
|
|
330
|
+
`destroyTiles()` hits the same early return.
|
|
331
|
+
- **Fix**: dispose the previous geometry at the top of `build()`, and replace the `!isBuilt` early
|
|
332
|
+
return in `dispose()` with a `geometry !== null` check.
|
|
333
|
+
- **Confidence**: Confirmed
|
|
334
|
+
|
|
335
|
+
### ECS-18 `TerrainTile.stitching` flags are never reset, so re-stitched seams copy instead of average
|
|
336
|
+
- **Severity**: MAJOR
|
|
337
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/terrain/tiles/TerrainTile.js:99-110` (consumed at `:279`, `:333`)
|
|
338
|
+
- **Defect**: Neither `build()`, `dispose()`, nor `TerrainTileManager.rebuildTilesByUV` clears the
|
|
339
|
+
per-neighbour `stitching` state. `stitchHorizontal`/`stitchVertical` branch on those flags: when one
|
|
340
|
+
side is already marked stitched, the other side's normals are *copied* wholesale rather than averaged.
|
|
341
|
+
- **Failure scenario**: tiles A(0,0) and B(1,0) built and stitched, so `A.stitching.right === true`.
|
|
342
|
+
A height edit clears `isBuilt` on both but leaves the flags. A rebuilds first — `stitchTile` sees B
|
|
343
|
+
unbuilt and skips. B rebuilds — `stitchHorizontal(A, B)` finds the stale
|
|
344
|
+
`left.stitching.right === true` and takes the copy branch, writing A's one-sided edge normals into B.
|
|
345
|
+
The shared edge now carries normals computed from A's triangles only: a visible lighting crease down
|
|
346
|
+
every rebuilt tile boundary after any terrain edit.
|
|
347
|
+
- **Fix**: reset all eight `stitching` fields in `TerrainTile.build()` and `dispose()`, and clear the
|
|
348
|
+
neighbours' matching flags in `rebuildTilesByUV`.
|
|
349
|
+
- **Confidence**: Confirmed
|
|
350
|
+
|
|
351
|
+
### ECS-19 `ClingToTerrain.fromJSON` defaults `rotationSpeed` to `Infinity`; the field initialiser is `3`
|
|
352
|
+
- **Severity**: MAJOR
|
|
353
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/terrain/ecs/cling/ClingToTerrain.js:54`
|
|
354
|
+
- **Defect**: The class field is `rotationSpeed = 3`, and `ClingToTerrainSerializationUpgrader_0_1.js:22`
|
|
355
|
+
explicitly writes `3`, calling it "the default value from the ClingToTerrain class". `fromJSON`
|
|
356
|
+
destructures with `Number.POSITIVE_INFINITY` as the default.
|
|
357
|
+
- **Failure scenario**: a blueprint carrying `{"ClingToTerrain": {"normalAlign": true}}` with no
|
|
358
|
+
`rotationSpeed` → `rotationSpeed = Infinity`. In `ClingToTerrainSystem.processRaycastHit:272`
|
|
359
|
+
`Number.isFinite` is false → `angularLimit = Infinity` → `alignToVector` computes
|
|
360
|
+
`t = clamp01(Infinity / angle) = 1` → the entity snaps to the terrain normal instantly every frame
|
|
361
|
+
instead of easing at 3 rad/s. The same entity constructed in code, or loaded from binary v0 through
|
|
362
|
+
the upgrader, eases correctly — the behaviour differs by load path.
|
|
363
|
+
- **Fix**: use `3` as the `fromJSON` destructuring default.
|
|
364
|
+
- **Confidence**: Confirmed
|
|
365
|
+
|
|
366
|
+
### ECS-20 `Terrain` JSON and binary paths persist disjoint field sets, and disagree on a default
|
|
367
|
+
- **Severity**: MAJOR
|
|
368
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/terrain/ecs/Terrain.js:930,981`;
|
|
369
|
+
`H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/terrain/serialization/TerrainSerializationAdapter.js:29,144`
|
|
370
|
+
- **Defect**: Three fields are handled by exactly one of the two paths.
|
|
371
|
+
`gridTransformKind` and `lightMapURL` are written into `extra` by `serialize`/`deserialize` but are
|
|
372
|
+
absent from `toJSON`/`fromJSON`. `overlay.baseTileImage` is written by `toJSON` (as
|
|
373
|
+
`overlayTileImage`) and restored by `fromJSON`, but is absent from the binary adapter. The two paths
|
|
374
|
+
also disagree on the `gridTransformKind` default: `deserialize` falls back to
|
|
375
|
+
`GridTransformKind.Legacy` (`:144`) while the class field initialiser is `GridTransformKind.Direct`
|
|
376
|
+
(`Terrain.js:76`), which `fromJSON` leaves in place.
|
|
377
|
+
- **Failure scenario**: a terrain with `gridTransformKind = Legacy`, `size = (64,64)`, `gridScale = 1`,
|
|
378
|
+
saved via `toJSON` and reloaded. `fromJSON` leaves it at `Direct`, so `buildGridTransform` produces
|
|
379
|
+
`scale = 1, offset = 0.5` instead of `scale = 64/63 ≈ 1.0159, offset = 0`. `mapPointGrid2World(63, 63)`
|
|
380
|
+
returns `(63.5, ·, 63.5)` instead of `(64, ·, 64)` — every grid-anchored entity, marker and combat
|
|
381
|
+
cell is displaced, growing to more than half a cell at the far edge. The same round-trip drops
|
|
382
|
+
`lightMapURL`, so `build()` skips the `aoMap` load and the terrain renders unshadowed. Symmetrically,
|
|
383
|
+
a custom `overlay.baseTileImage` saved to binary reloads as the default `WHITE_PIXEL_DATA_URL`.
|
|
384
|
+
- **Fix**: add `gridTransformKind` and `lightMapURL` to `toJSON`/`fromJSON` (with the same default the
|
|
385
|
+
binary path uses), and write `overlay.baseTileImage` into the binary adapter's `extra` blob.
|
|
386
|
+
- **Confidence**: Confirmed
|
|
387
|
+
|
|
388
|
+
### ECS-21 `convertHeightMap2NormalMap` returns normals tilted uphill — both gradients unnegated
|
|
389
|
+
- **Severity**: MAJOR
|
|
390
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/grid/HeightSampler2NormalSamplerSoft.js:54-56`
|
|
391
|
+
- **Defect**: For a height field `h(x, y)` the surface normal is `normalize(-∂h/∂x, -∂h/∂y, k)`. The
|
|
392
|
+
Sobel gradients `dX`/`dY` are computed with the correct sign but written out unnegated
|
|
393
|
+
(`_x = dX / magnitude`, `_y = dY / magnitude`).
|
|
394
|
+
- **Failure scenario**: a ramp `h(x, y) = x`. At an interior texel `dX = 8`, `dY = 0`,
|
|
395
|
+
`magnitude = sqrt(64.25) ≈ 8.0156` → the function returns `(+0.998, 0, +0.0624)`. The true normal for
|
|
396
|
+
a surface rising in `+x` leans toward `−x`: `(−0.998, 0, +0.0624)`. Every slope gets a normal pointing
|
|
397
|
+
up-slope, so any lighting or slope test built on this map is exactly reversed on both horizontal
|
|
398
|
+
axes. Flat regions are unaffected, which is why it is not obvious.
|
|
399
|
+
- **Fix**: `const _x = -dX / magnitude; const _y = -dY / magnitude;`
|
|
400
|
+
- **Confidence**: Likely (no in-repo consumer — public library surface; the maths is unambiguous)
|
|
401
|
+
|
|
402
|
+
### ECS-22 `Terrain.heightTexture` receives the `Sampler2D` object as its `height` argument
|
|
403
|
+
- **Severity**: MINOR
|
|
404
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/terrain/ecs/Terrain.js:145`
|
|
405
|
+
- **Defect**: `new DataTexture(this.samplerHeight.data, this.samplerHeight.width, this.samplerHeight, RedFormat, FloatType)`
|
|
406
|
+
— the third parameter of `DataTexture(data, width, height, …)` gets the sampler instance rather than
|
|
407
|
+
`this.samplerHeight.height`. Compare `graphics/ecs/water/Water.js:138`.
|
|
408
|
+
- **Failure scenario**: immediately after `new Terrain()`, `terrain.heightTexture.image` is
|
|
409
|
+
`{data: Float32Array(1), width: 1, height: <Sampler2D>}`. Any consumer that binds `heightTexture`
|
|
410
|
+
before `build()` runs `updateHeightTexture()` (which repairs `image.width`/`image.height` at
|
|
411
|
+
`:721-722`) uploads with a non-numeric height — `gl.texImage2D` raises `GL_INVALID_VALUE`.
|
|
412
|
+
- **Fix**: pass `this.samplerHeight.height`.
|
|
413
|
+
- **Confidence**: Confirmed
|
|
414
|
+
|
|
415
|
+
### ECS-23 `heightMap2AOMap` leaks the intermediate denoise source texture
|
|
416
|
+
- **Severity**: MINOR
|
|
417
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/grid/HeightMap2AOMap.js:77`
|
|
418
|
+
- **Defect**: `filter_result_to_texture(raw_ao.array, width, height)` creates a `DataTexture` that is
|
|
419
|
+
uploaded by the denoise `ImageFilter` pass and then dropped without `.dispose()`. The sibling
|
|
420
|
+
`pyramid_texture` *is* disposed at `:91`, so the omission is unintentional.
|
|
421
|
+
- **Failure scenario**: `terrain.buildLightMap()` runs per generated map. For a 256×256 terrain at the
|
|
422
|
+
default `quality = 4` the AO resolution is 1024×1024, so each bake orphans a 1024×1024 RGBA8 GPU
|
|
423
|
+
texture (~4 MB VRAM) for the lifetime of the WebGL context. Each new run accumulates another.
|
|
424
|
+
- **Fix**: hold the texture in a local and `.dispose()` it after the `smooth_ao` `ImageFilter` returns.
|
|
425
|
+
- **Confidence**: Confirmed
|
|
426
|
+
|
|
427
|
+
### ECS-24 `loadVisibleTerrainTiles` does not reset its counters on retry, and can report success early
|
|
428
|
+
- **Severity**: MINOR
|
|
429
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/terrain/util/loadVisibleTerrainTiles.js:70-84`
|
|
430
|
+
- **Defect**: `handleFailedTile` bumps `attempt` and re-runs `init()`, which recomputes `totalTiles` and
|
|
431
|
+
restarts `start_time`, but `loadedTiles` and `failedTiles` carry over from the aborted attempt. The
|
|
432
|
+
`attemptId !== attempt` guard only suppresses *stale* callbacks; it does not roll back what they
|
|
433
|
+
already counted.
|
|
434
|
+
- **Failure scenario**: 20 visible tiles; 12 resolve, then one rejects
|
|
435
|
+
(`'Original tile was destroyed during build process'`). `init()` re-runs with `totalTiles = 20` and
|
|
436
|
+
`loadedTiles = 12`. The 12 already-built tiles resolve on the next microtask → `loadedTiles = 24 ≥ 20`
|
|
437
|
+
→ `cycleFunction` returns `TaskSignal.EndSuccess` while 8 tiles are still mid-build. The loading
|
|
438
|
+
screen closes and the scene shows 8 tiles with no geometry.
|
|
439
|
+
- **Fix**: reset `loadedTiles = 0` and `failedTiles = 0` at the top of `init()`.
|
|
440
|
+
- **Confidence**: Confirmed
|
|
441
|
+
|
|
442
|
+
### ECS-25 `TerrainSystem.mapPointGrid2World` hardcodes the Legacy transform and drops the offset
|
|
443
|
+
- **Severity**: MINOR
|
|
444
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/terrain/ecs/TerrainSystem.js:81-87,186-187`
|
|
445
|
+
- **Defect**: `link()` computes `gridScaleX = g_w * gridScale / (g_w - 1)` unconditionally — the
|
|
446
|
+
`GridTransformKind.Legacy` formula — ignoring `component.gridTransformKind`, and
|
|
447
|
+
`mapPointGrid2World` applies no offset. `Terrain.mapPointGrid2World` (`Terrain.js:555`) uses
|
|
448
|
+
`gridTransform`, whose `Direct` branch (the class default) is `scale = gridScale, offset = gridScale / 2`.
|
|
449
|
+
- **Failure scenario**: 64×64 terrain, `gridScale = 1`, default `Direct` kind.
|
|
450
|
+
`terrain.mapPointGrid2World(0, 0, v3)` → `(0.5, ·, 0.5)`; `terrainSystem.mapPointGrid2World(0, 0, v3)`
|
|
451
|
+
→ `(0, ·, 0)`. At `x = 63`: `63.5` vs `64.0`. Two public methods documented to perform the same
|
|
452
|
+
mapping differ by half a cell at the origin and diverge with distance. `gridScaleX/Y` are also
|
|
453
|
+
system-global, so with two `Terrain` entities they hold whichever linked last.
|
|
454
|
+
- **Fix**: delete `TerrainSystem.mapPointGrid2World`/`gridScaleX`/`gridScaleY` and route callers to
|
|
455
|
+
`Terrain.mapPointGrid2World`, or read `terrain.gridTransform` directly.
|
|
456
|
+
- **Confidence**: Confirmed
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Animation / dynamic actions / evaluation
|
|
461
|
+
|
|
462
|
+
### ECS-26 `AnimationOptimizer` writes its results to itself — optimization is a silent no-op
|
|
463
|
+
- **Severity**: MAJOR
|
|
464
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/animation/AnimationOptimizer.js:109-114`
|
|
465
|
+
- **Defect**: `optimizeTrack(track)` is a port of three.js `KeyframeTrack.prototype.optimize()`, where
|
|
466
|
+
`this` *is* the track. Here `this` is the `AnimationOptimizer`, so the compacted arrays are assigned
|
|
467
|
+
to `this.times` / `this.values` on the optimizer; the `track` (whose arrays were only *copied* at
|
|
468
|
+
lines 108-109) is never modified, and the method returns `this`.
|
|
469
|
+
- **Failure scenario**: `GLTFAssetLoader.js:426` calls `animationOptimizer.optimize(animation)` on
|
|
470
|
+
every animation of every loaded GLTF, using the module-level singleton. Load a GLTF whose track has
|
|
471
|
+
1000 keyframes of which 900 are redundant: the track still has 1000 keyframes afterwards. The
|
|
472
|
+
singleton also permanently retains `this.times`/`this.values` — full copies of the last-optimized
|
|
473
|
+
track's typed arrays, megabytes for a skeletal clip — for the module's lifetime.
|
|
474
|
+
- **Fix**: write back to the track (`track.times = ...; track.values = ...;`) and return `track`.
|
|
475
|
+
- **Confidence**: Confirmed
|
|
476
|
+
|
|
477
|
+
### ECS-27 `AnimationOptimizer`: `time[0]` should be `times[0]`
|
|
478
|
+
- **Severity**: MINOR
|
|
479
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/animation/AnimationOptimizer.js:36`
|
|
480
|
+
- **Defect**: `if (time !== timeNext && (i !== 1 || time !== time[0]))` — `time` is a number, so
|
|
481
|
+
`time[0]` is `undefined` and the sub-condition is always true. three.js has `times[0]`.
|
|
482
|
+
- **Failure scenario**: a track with `times = [0, 0, 1, 2]` and distinct values at index 1. three.js
|
|
483
|
+
drops keyframe 1 as a duplicate of the keyframe at t=0; this code keeps it, leaving two keyframes
|
|
484
|
+
scheduled at t=0 in the "optimized" output.
|
|
485
|
+
- **Fix**: `time !== times[0]`.
|
|
486
|
+
- **Confidence**: Confirmed
|
|
487
|
+
|
|
488
|
+
### ECS-28 `DynamicRuleDescription.equals` calls `.equals` on a primitive string
|
|
489
|
+
- **Severity**: MAJOR
|
|
490
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/dynamic_actions/rules/DynamicRuleDescription.js:55`
|
|
491
|
+
- **Defect**: `id` is a primitive string (`UUID.string()` returns `UUID.v4().toString()`, and
|
|
492
|
+
`fromJSON` assigns the JSON string directly), but `equals` does `return this.id.equals(other.id)`.
|
|
493
|
+
`String.prototype.equals` does not exist.
|
|
494
|
+
- **Failure scenario**:
|
|
495
|
+
`DynamicRuleDescription.fromJSON({id:"combat.long-idle.ai", ...}).equals(otherRule)` →
|
|
496
|
+
`TypeError: this.id.equals is not a function`. Any container relying on the `equals` protocol
|
|
497
|
+
(a default-configured `HashMap`/`HashSet` keyed by rules) throws on the first lookup.
|
|
498
|
+
`DynamicActorSystem` only escapes because `__global_last_used_times` supplies a custom
|
|
499
|
+
`keyEqualityFunction`.
|
|
500
|
+
- **Fix**: `return this.id === other.id;`
|
|
501
|
+
- **Confidence**: Confirmed
|
|
502
|
+
|
|
503
|
+
### ECS-29 Pooled `DynamicActorSystem` context keeps the previous entity's `RuleExecution`
|
|
504
|
+
- **Severity**: MAJOR
|
|
505
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/dynamic_actions/DynamicActorSystem.js:43,74-90`
|
|
506
|
+
- **Defect**: `Context` instances are pooled by `AbstractContextSystem`, and the pool's reset callback
|
|
507
|
+
is an empty `// TODO` (`system/AbstractContextSystem.js:37-42`). `Context.link()` resets
|
|
508
|
+
`next_idle_event_time` but never resets `execution`, so a recycled context arrives carrying the
|
|
509
|
+
previous entity's `execution.rule` and `execution.executor`. `Context.unlink()` also never terminates
|
|
510
|
+
the still-running executor.
|
|
511
|
+
- **Failure scenario**: unit A executes a `SpeakLine` rule, whose executor tree is
|
|
512
|
+
`ParallelBehavior([WaitForEventBehavior(FinishedSpeakingLine on A), SendEventBehavior])`. A is killed
|
|
513
|
+
and removed; `removeEntity` deletes A's event listeners, so `FinishedSpeakingLine` can never arrive,
|
|
514
|
+
the parallel never completes, `DieBehavior` never runs, and the executor `Entity` keeps
|
|
515
|
+
`EntityFlags.Built` forever. A's context is released to the pool still pointing at that immortal
|
|
516
|
+
executor. Unit B spawns and pops the same context. On B's first event, `attemptRuleExecution`
|
|
517
|
+
(lines 193-199) sees `execution.executor.getFlag(Built) === true` and tests
|
|
518
|
+
`rule.priority <= execution.rule.priority`. Every rule in
|
|
519
|
+
`app/data/database/dynamic-actions/data.json` uses the default `priority = 0`, so `0 <= 0` →
|
|
520
|
+
`return false`: **B never executes any rule for the rest of the session.** If a higher-priority rule
|
|
521
|
+
ever does get through, `terminateActiveExecution` destroys entity A's executor from B's context.
|
|
522
|
+
- **Fix**: reset execution state at the context boundary — in `Context.link()` set
|
|
523
|
+
`this.execution.rule = null; this.execution.executor = null;`, and in `Context.unlink()` destroy a
|
|
524
|
+
still-built executor before clearing.
|
|
525
|
+
- **Confidence**: Confirmed
|
|
526
|
+
|
|
527
|
+
### ECS-30 `match()` hands actions a live view of a shared scope stack that it unwinds before the action runs
|
|
528
|
+
- **Severity**: MAJOR
|
|
529
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/dynamic_actions/DynamicActorSystem.js:536-551`;
|
|
530
|
+
`.../actions/definition/WriteToBlackboardActionDescription.js:24-35`
|
|
531
|
+
- **Defect**: `match()` passes `this.scope.proxy` — a proxy over the system-wide, mutable `DataScope` —
|
|
532
|
+
as the action `context`, then calls `this.scope.unwind(top)` on line 551, dropping every frame it
|
|
533
|
+
just pushed (blackboards, `{now, entity}`, the event frame, the entity proxy scope).
|
|
534
|
+
`WriteToBlackboardActionDescription` captures that proxy in an `ActionBehavior` closure and only
|
|
535
|
+
calls `expression.evaluate(context)` when the behavior ticks. `BehaviorSystem.link()` only calls
|
|
536
|
+
`behavior.initialize()`; behaviors are first ticked in `BehaviorSystem.update()`, i.e. no earlier
|
|
537
|
+
than the next frame.
|
|
538
|
+
- **Failure scenario**: a rule with `"action": {"type":"Write", "name":"last_bark_time", "value":"now"}`.
|
|
539
|
+
`match()` pushes `{now: 12.5, entity}`, matches, builds the executor, then unwinds. Next frame the
|
|
540
|
+
`ActionBehavior` ticks and evaluates `now` against an empty scope → `undefined`, so
|
|
541
|
+
`blackboard.acquire("last_bark_time", …).set(undefined)` instead of `12.5`. With two actors handling
|
|
542
|
+
events in the same frame the deferred read can also resolve against the *other* actor's frames if
|
|
543
|
+
those are still on the stack.
|
|
544
|
+
- **Fix**: snapshot the scope before handing it to an action — materialise the proxy into a plain
|
|
545
|
+
object in `executeRule` (as `SendRequestActionDescription` already does eagerly for
|
|
546
|
+
`action_context`), or have `WriteToBlackboardActionDescription.execute` evaluate the expression
|
|
547
|
+
eagerly and defer only the blackboard write.
|
|
548
|
+
- **Confidence**: Confirmed
|
|
549
|
+
|
|
550
|
+
### ECS-31 `PointFitnessGaussianBlur` serializes a `quality` field that is never stored
|
|
551
|
+
- **Severity**: MAJOR
|
|
552
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/evaluation/json/serializeToJSON_PointFitnessFunction.js:61`
|
|
553
|
+
- **Defect**: The serializer writes `quality: value.quality`, but `PointFitnessGaussianBlur` never
|
|
554
|
+
stores it — `from()` consumes it only to compute `samples_x/y/z = size * quality` and discards it.
|
|
555
|
+
The deserializer (`deserializeFromJSON_PointFitnessFunction.js:45,51`) destructures `quality` with no
|
|
556
|
+
default and forwards it to `from()`, whose own default `quality = 5` then applies.
|
|
557
|
+
- **Failure scenario**: `PointFitnessGaussianBlur.from(src, 4, 4, 4, 1)` → `samples = 4,4,4`
|
|
558
|
+
(64 source evaluations per point). Serialize → `{size_x:4, size_y:4, size_z:4, quality: undefined}`
|
|
559
|
+
(and `undefined` is dropped entirely by `JSON.stringify`). Deserialize → `quality = 5` →
|
|
560
|
+
`samples = 20,20,20`, i.e. 8000 source evaluations per point and a numerically different blur.
|
|
561
|
+
- **Fix**: store the value in `from()` (`r.quality = quality;`) with a matching class field, or derive
|
|
562
|
+
it on serialize as `value.samples_x / value.size_x`.
|
|
563
|
+
- **Confidence**: Confirmed
|
|
564
|
+
|
|
565
|
+
### ECS-32 `PointFitnessGaussianBlur` kernel is offset by half a sample on every axis
|
|
566
|
+
- **Severity**: MINOR
|
|
567
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/evaluation/complex/PointFitnessGaussianBlur.js:67-69,80,89,99`
|
|
568
|
+
- **Defect**: `half_samples_x = samplesX / 2` with `local_x = ix - half_samples_x` gives taps at
|
|
569
|
+
`-n/2 … n/2 - 1`, which is not centred on the sample point. A centred kernel needs `(samplesX - 1) / 2`.
|
|
570
|
+
- **Failure scenario**: `samples_x = 5, size_x = 10`. Taps land at
|
|
571
|
+
`local_x ∈ {-2.5,-1.5,-0.5,0.5,1.5}` → spatial offsets `{-5,-3,-1,1,3}` instead of
|
|
572
|
+
`{-5,-2.5,0,2.5,5}`. Both the gaussian weights and the sample positions are biased by half a tap, so
|
|
573
|
+
blurring a step edge shifts it by 1 world unit in −x on each axis, and the point being blurred is
|
|
574
|
+
never itself sampled.
|
|
575
|
+
- **Fix**: `const half_samples_x = (samplesX - 1) / 2;` (and likewise for y, z).
|
|
576
|
+
- **Confidence**: Confirmed
|
|
577
|
+
|
|
578
|
+
### ECS-33 `Animation.flags` is in neither serialization path, so a paused animation resumes after load
|
|
579
|
+
- **Severity**: MINOR
|
|
580
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/animation/Animation.js:40,153-158`;
|
|
581
|
+
`.../animation/AnimationSerializationAdapter.js:15-28`
|
|
582
|
+
- **Defect**: `flags` (carrying `Playing` and `MeshSizeCulling`) is written by neither `toJSON` nor
|
|
583
|
+
`serialize`, and the field initialiser unconditionally sets `Playing | MeshSizeCulling`.
|
|
584
|
+
`equals()`/`hash()` also ignore `flags` while including `debtTime`.
|
|
585
|
+
- **Failure scenario**: `animation.isPlaying = false` (a supported setter, covered by
|
|
586
|
+
`Animation.spec.js:58-62`), save via `BinaryBufferSerializer`, reload → `flags` is back to the
|
|
587
|
+
constructor default and the animation plays again. Two `Animation` components differing only in
|
|
588
|
+
`isPlaying` also report `equals() === true` and identical `hash()`.
|
|
589
|
+
- **Fix**: add `flags` to the binary adapter (bumping `version`) and to `toJSON`/`fromJSON`
|
|
590
|
+
(defaulting to `MeshSizeCulling | Playing`), and include it in `equals`/`hash`.
|
|
591
|
+
- **Confidence**: Confirmed
|
|
592
|
+
|
|
593
|
+
### ECS-34 `AnimationSystem` re-subscribes to clip signals on every mesh change and never unsubscribes
|
|
594
|
+
- **Severity**: MINOR
|
|
595
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/systems/AnimationSystem.js:116-117,150,221-230`
|
|
596
|
+
- **Defect**: `updateAnimationState` unconditionally does `clips.on.added.add(initAnimationClip)` and
|
|
597
|
+
`clips.on.removed.add(...)` with fresh closures each time. It runs from `registerAnimation` (line 150),
|
|
598
|
+
which runs both from `link()` and from every `MeshEvents.DataSet` event. `unlink()` removes only the
|
|
599
|
+
`MeshEvents.DataSet` entity listener; the clip-signal handlers are never removed, and `Signal.add`
|
|
600
|
+
does not deduplicate.
|
|
601
|
+
- **Failure scenario**: `MeshSystem.process` sets a placeholder mesh (DataSet #1) then the real asset
|
|
602
|
+
(DataSet #2) for every async-loaded mesh with `__use_placeholder_mesh` on — together with `link()`
|
|
603
|
+
that is 3 registrations. Every later runtime mesh swap adds another. After N mesh sets, a single
|
|
604
|
+
`animation.clips.add(clip)` invokes `startClip` N times, and the handler list grows without bound for
|
|
605
|
+
the component's life; unlink/relink cycles accumulate across cycles too.
|
|
606
|
+
- **Fix**: keep references to the two handlers on the component, remove any previous pair at the top of
|
|
607
|
+
`updateAnimationState`, and remove them in `unlink()`.
|
|
608
|
+
- **Confidence**: Confirmed
|
|
609
|
+
|
|
610
|
+
### ECS-35 `SerializationMetadata` binary adapter truncates a documented 32-bit flag field to 8 bits
|
|
611
|
+
- **Severity**: MINOR
|
|
612
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/components/SerializationMetadataSerializationAdapter.js:15,24`
|
|
613
|
+
- **Defect**: `flags` is documented as a "32 bit bitfield" (`SerializationMetadata.js:19-22`) and
|
|
614
|
+
`toJSON` writes the full number, but the binary adapter uses `writeUint8`/`readUint8`.
|
|
615
|
+
- **Failure scenario**: a consumer defines a ninth flag `Foo = 1 << 8`. `metadata.setFlag(Foo)` →
|
|
616
|
+
`flags === 256`. `toJSON()` preserves `256`; the binary path writes `256 & 0xFF === 0` and reads back
|
|
617
|
+
`flags === 0`, silently losing the flag on save/load.
|
|
618
|
+
- **Fix**: `writeUint32`/`readUint32`, bumping the adapter `version` and adding an upgrader.
|
|
619
|
+
- **Confidence**: Confirmed
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
## Storage / serialization
|
|
624
|
+
|
|
625
|
+
### ECS-36 Schema back-reference is looked up by address in the schema-*object* map, then dereferenced
|
|
626
|
+
- **Severity**: MAJOR
|
|
627
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/storage/binary/object/BinaryObjectSerializationAdapter2.js:416`
|
|
628
|
+
- **Defect**: `#deserializeObject` resolves a `HEADER_TAG_REF_SCHEMA` record with
|
|
629
|
+
`this.#schema_table.getRecord(schema_address)`. `getRecord(schema)` looks up `#schema_map`, which is
|
|
630
|
+
keyed by the **schema object** (`#schema_map.set(record.schema, record)`); the address-keyed map is
|
|
631
|
+
`#address_map`, reachable only via `getSchemaAt(address)`. Passing a number can never hit, so
|
|
632
|
+
`record` is `undefined` and line 419 (`record.typeName`) throws. The whole `HEADER_TAG_REF_SCHEMA`
|
|
633
|
+
decode branch can only crash.
|
|
634
|
+
- **Failure scenario**: the `schema` parameter of `serialize(buffer, object, schema)` is a documented
|
|
635
|
+
optional argument. `const S = {v: undefined}; a2.serialize(buf, {v:1}, S); a2.serialize(buf, {v:2}, S);`
|
|
636
|
+
— the first object writes `HEADER_TAG_INLINE_SCHEMA` and interns `S`; the second takes the `else`
|
|
637
|
+
branch at line 375 and writes `HEADER_TAG_REF_SCHEMA` + address. On read, the first decodes fine, the
|
|
638
|
+
second throws `TypeError: Cannot read properties of undefined (reading 'typeName')`. Same for a single
|
|
639
|
+
object whose explicit schema reuses one sub-schema object for two fields.
|
|
640
|
+
- **Fix**: `record = this.#schema_table.getSchemaAt(schema_address);`. Also reset `#schema_table` when
|
|
641
|
+
switching an instance between writing and reading — write-side records are registered at write-buffer
|
|
642
|
+
positions and never cleared, so they can shadow read-side records at the same address.
|
|
643
|
+
- **Confidence**: Confirmed
|
|
644
|
+
|
|
645
|
+
### ECS-37 Per-component transient flag is ignored whenever the dataset has no `SerializationMetadata` type
|
|
646
|
+
- **Severity**: MAJOR
|
|
647
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/storage/BinaryBufferSerializer.js:26-34`
|
|
648
|
+
- **Defect**: `isEntityTransient` returns `false` on `smComponentIndex === -1` **before** checking
|
|
649
|
+
`componentInstance[COMPONENT_SERIALIZATION_TRANSIENT_FIELD]`. The two mechanisms are independent —
|
|
650
|
+
the magic field is a per-instance opt-out that does not require a `SerializationMetadata` component
|
|
651
|
+
to exist — but it is gated behind one.
|
|
652
|
+
- **Failure scenario**: `EntityComponentDataset.computeComponentTypeIndex` returns `-1` for an
|
|
653
|
+
unregistered type, and a type is only registered once some entity actually receives one. In a dataset
|
|
654
|
+
where nothing ever adds a `SerializationMetadata`, `smComponentIndex === -1`. Mark any component
|
|
655
|
+
transient the way the game does (`guiElement[COMPONENT_SERIALIZATION_TRANSIENT_FIELD] = true` in
|
|
656
|
+
`setupStrategyHeadsUpDisplays.js:98`, `sockets[...] = true` in `CombatUnitEntityDecorator.js:660`,
|
|
657
|
+
`this.__transform2GridPosition[...] = true` in `CharacterMoveAsynchronousAction.js:236`) and it is
|
|
658
|
+
written to the save anyway, then resurrected on load as a duplicate of a runtime-only component.
|
|
659
|
+
- **Fix**: move the `COMPONENT_SERIALIZATION_TRANSIENT_FIELD` check above the `smComponentIndex === -1`
|
|
660
|
+
early return.
|
|
661
|
+
- **Confidence**: Confirmed
|
|
662
|
+
|
|
663
|
+
### ECS-38 Object adapter resolves class identity through *inherited* `typeName`
|
|
664
|
+
- **Severity**: MAJOR
|
|
665
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/storage/binary/object/BinaryObjectSerializationAdapter.js:59-61`
|
|
666
|
+
- **Defect**: `const typeName = Klass.typeName;` is a normal static lookup, so an ES class that does not
|
|
667
|
+
declare its own `typeName` silently resolves to its ancestor's. The object-v2 writer explicitly
|
|
668
|
+
guards this with `Object.prototype.hasOwnProperty.call(ctor, "typeName")`
|
|
669
|
+
(`BinaryObjectSerializer.js:270`) and has a dedicated regression test, so the v1 adapter is the
|
|
670
|
+
un-fixed half.
|
|
671
|
+
- **Failure scenario**: `BehaviorComponentSerializationAdapter.serialize`
|
|
672
|
+
(`engine/intelligence/behavior/ecs/BehaviorComponentSerializationAdapter.js:38`) routes every behavior
|
|
673
|
+
through `this.objectAdapter.serialize(buffer, behavior)`. `Behavior.js:78` sets
|
|
674
|
+
`Behavior.typeName = "Behavior"`, while `SelectorBehavior`, `WeightedRandomBehavior`, `BranchBehavior`
|
|
675
|
+
and `ConditionalBehavior` declare none. Saving an entity whose behavior tree contains a
|
|
676
|
+
`SelectorBehavior` therefore looks up adapter `"Behavior"` — not registered — and the save aborts with
|
|
677
|
+
`No adapter found for class 'Behavior'`, naming a class that is not in the tree. If any ancestor *does*
|
|
678
|
+
have a registered adapter the failure is worse: the subclass is written with the ancestor's payload
|
|
679
|
+
and read back as the ancestor, dropping all subclass state.
|
|
680
|
+
- **Fix**: require an own `typeName` —
|
|
681
|
+
`Object.prototype.hasOwnProperty.call(Klass, "typeName") ? Klass.typeName : undefined` — and throw
|
|
682
|
+
naming the actual constructor when absent, mirroring `BinaryObjectSerializer.writeValue`.
|
|
683
|
+
- **Confidence**: Confirmed
|
|
684
|
+
|
|
685
|
+
### ECS-39 The last component type in a stream is never finalized on load
|
|
686
|
+
- **Severity**: MINOR
|
|
687
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/storage/BinaryBufferDeSerializer.js:117-127`
|
|
688
|
+
- **Defect**: `cycleFunction` finalizes the *previous* type at the top of the next loop iteration
|
|
689
|
+
(`if (typesDecoded > 0) collectionDeSerializer.finalize();`), but the `typesLeft === 0` exit at
|
|
690
|
+
line 117 returns `TaskSignal.EndSuccess` first. So `BinaryCollectionDeSerializer.finalize()` — and
|
|
691
|
+
hence `adapter.finalize()` — never runs for the final type. The write path
|
|
692
|
+
(`BinaryBufferSerializer.js:194`) finalizes every type correctly.
|
|
693
|
+
- **Failure scenario**: load a buffer containing types `[Name, CombatUnit]`. `CombatUnit` elements are
|
|
694
|
+
read to exhaustion, `typesLeft` is `0`, the task ends, and
|
|
695
|
+
`CombatUnitSerializationAdapter.finalize()`
|
|
696
|
+
(`model/game/ecs/component/unit/serialization/CombatUnitSerializationAdapter.js:51`) is never invoked,
|
|
697
|
+
so its module-static `serialization_array` is left holding stale entries instead of being spliced
|
|
698
|
+
empty. The deserializer is also left in `State.Ready`, so a subsequent `initialize()` on the same
|
|
699
|
+
instance throws `IllegalStateException`. Which adapter is affected depends on which component type
|
|
700
|
+
happens to be last, so the leak is non-deterministic across saves.
|
|
701
|
+
- **Fix**: finalize the in-flight collection before returning `EndSuccess`, guarding against
|
|
702
|
+
double-finalize on a re-entered cycle.
|
|
703
|
+
- **Confidence**: Confirmed
|
|
704
|
+
|
|
705
|
+
---
|
|
706
|
+
|
|
707
|
+
## GUI / HUD / speaker
|
|
708
|
+
|
|
709
|
+
### ECS-40 `LineSetDescription.collect` walks the root's children instead of the popped node's — infinite loop
|
|
710
|
+
- **Severity**: CRITICAL
|
|
711
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/speaker/lines/sets/LineSetDescription.js:56`
|
|
712
|
+
- **Defect**: Inside the traversal loop the popped node is `set`, but the child-set expansion reads
|
|
713
|
+
`const sets = this.sets;` — the *root's* children — on every iteration. The root's children are
|
|
714
|
+
therefore re-pushed forever and `pointer` never drains.
|
|
715
|
+
- **Failure scenario** (reproduced): a `voice-line-sets` entry with a non-empty `sets` array
|
|
716
|
+
(schema-supported: `LineSetDescriptionTable.linkOne` reads `sets` from JSON, and `validateOne` exists
|
|
717
|
+
purely to cycle-check that graph). Root `R{lines:[l-root], sets:[C]}`, `C{lines:[l-child]}` →
|
|
718
|
+
`collect` writes `l-root, l-child, l-child, l-child, …` without bound; `destination` grows until OOM.
|
|
719
|
+
`VoiceSystem.sayLineFromSet` calls `set.collect(temp_lines, 0)`, so the tab hangs on the first nested
|
|
720
|
+
bark set anyone authors.
|
|
721
|
+
- **Fix**: `const sets = set.sets;`
|
|
722
|
+
- **Confidence**: Confirmed
|
|
723
|
+
|
|
724
|
+
### ECS-41 `GUIElementSystem.unlink` calls `Signal.remove` without the context it subscribed with
|
|
725
|
+
- **Severity**: MAJOR
|
|
726
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/gui/GUIElementSystem.js:261` (subscribe at `:248`)
|
|
727
|
+
- **Defect**: `link` does `component.visible.onChanged.add(handleComponentVisibilityChange, {el, system, entity})`;
|
|
728
|
+
`unlink` does `component.visible.onChanged.remove(handleComponentVisibilityChange)` with no `thisArg`.
|
|
729
|
+
`Signal.remove` matches on `handle === h && context === thisArg`, so it matches nothing and returns
|
|
730
|
+
`false`.
|
|
731
|
+
- **Failure scenario** (reproduced against the real `Signal`): `add(handler, ctx)` then `remove(handler)`
|
|
732
|
+
→ returns `false`, `hasHandlers()` stays `true`, the next `send1` still runs the handler. Concretely,
|
|
733
|
+
`ecd.removeComponentFromEntity(entity, GUIElement)` (real caller:
|
|
734
|
+
`model/game/scenes/combat/UnitStateManager.js:437`) or `Entity.destroy()` → `Entity.build()` leaves
|
|
735
|
+
the old subscription in place, and the next `link` adds a second. With two handlers,
|
|
736
|
+
`component.visible.set(true)` runs `attachComponent` twice → `parent.addChild(component.view)` twice
|
|
737
|
+
→ `View.addChild` (`view/View.js:365`) pushes the same child into `parent.children` twice (the
|
|
738
|
+
`isLinked` assert that would catch it is stripped in production). `removeChild` then drops only the
|
|
739
|
+
first entry, so `parent.children` keeps a stale entry whose `el` is no longer in the DOM; a later
|
|
740
|
+
`removeAllChildren` calls `this.el.removeChild(child.el)` on a non-child and throws `NotFoundError`.
|
|
741
|
+
- **Fix**: store the context object (as `ViewportPositionSystem` does in `this.data`) and pass it to
|
|
742
|
+
`remove`.
|
|
743
|
+
- **Confidence**: Confirmed
|
|
744
|
+
|
|
745
|
+
### ECS-42 `ViewportPositionSystem.unlink` never unsubscribes from `el.view.size.onChanged`
|
|
746
|
+
- **Severity**: MAJOR
|
|
747
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/gui/position/ViewportPositionSystem.js:154` (unlink at `:167-183`)
|
|
748
|
+
- **Defect**: `link` makes five `onChanged.add(updatePosition, eventContext)` subscriptions —
|
|
749
|
+
`vp.position`, `vp.offset`, `vp.anchor`, `vp.enabled` and `el.view.size`. `unlink` removes only the
|
|
750
|
+
first four.
|
|
751
|
+
- **Failure scenario**: `GUIEngine.createModal` (`meep/src/engine/ui/GUIEngine.js:215`) does
|
|
752
|
+
`window.removeComponent(ViewportPosition)` immediately after `openWindow` built the entity,
|
|
753
|
+
deliberately freeing the modal from viewport positioning while the `GUIElement` and its
|
|
754
|
+
`ui-modal-window-container` view stay live on screen. `unlink` runs, but `updatePosition` stays bound
|
|
755
|
+
to that container's `size.onChanged` with a context holding the now-detached `vp`. Any later size
|
|
756
|
+
change re-invokes `positionComponent(el, vp)`, overwriting `view.position` from the stale
|
|
757
|
+
`vp.position`, overwriting `view.transformOrigin` with the stale `vp.anchor` (default `0,0`,
|
|
758
|
+
discarding the `0.5,0.5` set at `GUIEngine.js:213`), and calling `el.visible.set(...)` — if the stale
|
|
759
|
+
position is out of bounds and `stickToScreenEdge` is false (the default), `el.visible.set(false)`
|
|
760
|
+
makes `GUIElementSystem` detach the modal from the DOM and the open modal vanishes.
|
|
761
|
+
- **Fix**: add `el.view.size.onChanged.remove(updatePosition, eventContext);` inside the
|
|
762
|
+
`eventContext !== undefined` block in `unlink`.
|
|
763
|
+
- **Confidence**: Confirmed
|
|
764
|
+
|
|
765
|
+
### ECS-43 `HeadsUpDisplay.toJSON` writes `transformPerspectiveRotation`, `fromJSON` reads `perspectiveRotation`
|
|
766
|
+
- **Severity**: MAJOR
|
|
767
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/gui/hud/HeadsUpDisplay.js:86` vs `:90`
|
|
768
|
+
- **Defect**: The JSON key emitted for the `PerspectiveRotation` flag does not match the key the reader
|
|
769
|
+
destructures, so the reader always falls back to its `false` default.
|
|
770
|
+
- **Failure scenario** (reproduced): a HUD with `PerspectiveRotation` set — exactly what
|
|
771
|
+
`makeEnemyGroupHud` in `model/game/scenes/strategy/gui/setupStrategyHeadsUpDisplays.js` configures.
|
|
772
|
+
`toJSON()` yields `{"worldOffset":…,"transformWorldOffset":true,"transformPerspectiveRotation":true}`;
|
|
773
|
+
`HeadsUpDisplay.fromJSON(that)` yields `flags = 1` instead of `3`,
|
|
774
|
+
`getFlag(PerspectiveRotation) === false`, and `a.equals(b) === false`. The screen-space billboard
|
|
775
|
+
rotation is silently dropped on any JSON round-trip. The binary adapter round-trips the flag
|
|
776
|
+
correctly, so the two persistence paths disagree.
|
|
777
|
+
- **Fix**: emit `perspectiveRotation` in `toJSON`, or rename the `fromJSON` destructure to
|
|
778
|
+
`transformPerspectiveRotation`. Pick one and keep both sides identical.
|
|
779
|
+
- **Confidence**: Confirmed
|
|
780
|
+
|
|
781
|
+
### ECS-44 `GUIElementSystem.link` logs "does not have a view" and then dereferences the missing view
|
|
782
|
+
- **Severity**: MINOR
|
|
783
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/gui/GUIElementSystem.js:212`
|
|
784
|
+
- **Defect**: The un-managed-with-no-view branch does `console.error(...)` with no `return`. Execution
|
|
785
|
+
falls through to `if (component.visible.getValue()) this.attachComponent(component, entity)`
|
|
786
|
+
(`visible` defaults to `true`), and `attachComponent` reaches `parent.addChild(component.view)` with
|
|
787
|
+
`component.view === null`.
|
|
788
|
+
- **Failure scenario**: `const c = new GUIElement(); c.clearFlag(GUIElementFlag.Managed);` added to an
|
|
789
|
+
entity — precisely the state the error message describes. `View.addChild(null)` executes
|
|
790
|
+
`child.parent = this;` (its guarding asserts are stripped in production) →
|
|
791
|
+
`TypeError: Cannot set properties of null (setting 'parent')` propagates out of `link`, aborting the
|
|
792
|
+
component-added observer dispatch for that entity so later systems never link.
|
|
793
|
+
- **Fix**: `return;` after the `console.error` at `:212`.
|
|
794
|
+
- **Confidence**: Confirmed
|
|
795
|
+
|
|
796
|
+
### ECS-45 `ButtonViewEntity.finalize` destroys the structural children created in its constructor
|
|
797
|
+
- **Severity**: MINOR
|
|
798
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/gui/view/ButtonViewEntity.js:104-107` (children created at `:17-23`)
|
|
799
|
+
- **Defect**: The constructor adds the `foreground` and `background` `EmptyView`s the button's CSS is
|
|
800
|
+
built on. `finalize()` calls `removeAllChildren()`, which removes them too — but `ViewEntity.finalize`
|
|
801
|
+
is documented as bringing the view "to initial state ready to be destroyed **or re-used**", and
|
|
802
|
+
`GUIElementSystem` implements exactly that re-use contract (`unlink` calls `view.finalize()` and
|
|
803
|
+
clears `Initialized`; `attachComponent` re-calls `view.initialize(...)` when it sees
|
|
804
|
+
`Managed && !Initialized`).
|
|
805
|
+
- **Failure scenario**: an entity carrying a `view.Button` `GUIElement` (registered at
|
|
806
|
+
`model/game/GameClassRegistry.js:89`) goes through link → unlink → link. On the second `initialize`
|
|
807
|
+
the label is re-added but `.foreground`/`.background` are gone, so the button renders with no
|
|
808
|
+
background fill and dead hover/press styling. `LocalizedLabelViewEntity` is correct precisely because
|
|
809
|
+
its constructor adds no children.
|
|
810
|
+
- **Fix**: create the foreground/background children in `initialize()`, or have `finalize()` remove only
|
|
811
|
+
the label child.
|
|
812
|
+
- **Confidence**: Confirmed
|
|
813
|
+
|
|
814
|
+
### ECS-46 `LineSetDescriptionTable.validateOne` reports a false circular dependency for a shared child set
|
|
815
|
+
- **Severity**: MINOR
|
|
816
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/speaker/lines/sets/LineSetDescriptionTable.js:79`
|
|
817
|
+
- **Defect**: The check treats *any* second visit to a node as a cycle
|
|
818
|
+
(`if (visited.hasOwnProperty(set.id))`) rather than tracking the current traversal path, so a DAG with
|
|
819
|
+
a shared child is rejected.
|
|
820
|
+
- **Failure scenario**: `A{sets:[B,C]}`, `B{sets:[D]}`, `C{sets:[D]}` — no cycle. Traversal pops `A`,
|
|
821
|
+
pushes `B,C`; pops `C`, pushes `D`; pops `D`, marks visited; pops `B`, pushes `D`; pops `D` again →
|
|
822
|
+
`visited` hit → `errorConsumer("Circular dependency detected: D -> C -> A")` and `return false`. Valid
|
|
823
|
+
voice-line-set data fails validation at load, and the reported path names the wrong parent.
|
|
824
|
+
- **Fix**: track the recursion stack (mark on entry, unmark on exit) or colour nodes white/grey/black,
|
|
825
|
+
and only report a cycle on re-entering a grey node.
|
|
826
|
+
- **Confidence**: Confirmed
|
|
827
|
+
|
|
828
|
+
### ECS-47 `VoiceSystem.sayLineFromSet` dereferences `undefined` when the set yields no lines
|
|
829
|
+
- **Severity**: MINOR
|
|
830
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/speaker/VoiceSystem.js:313-315`
|
|
831
|
+
- **Defect**: `weightedRandomFromArray` returns `undefined` when `length === 0`
|
|
832
|
+
(`core/collection/array/weightedRandomFromArray.js:26-28`), and the result is used unguarded as
|
|
833
|
+
`selected_line.id`. The neighbouring "set not found" case at `:303` warns and returns, so graceful
|
|
834
|
+
handling of bad data is the intended contract here.
|
|
835
|
+
- **Failure scenario**: a `voice-line-sets` entry with an empty or omitted `lines` array (`linkOne`
|
|
836
|
+
defaults `lines = []`) — a placeholder or not-yet-authored bark set.
|
|
837
|
+
`ecd.sendEvent(entity, VoiceEvents.SpeakSetLine, {id: thatSet})` → `collect` returns `0` →
|
|
838
|
+
`TypeError: Cannot read properties of undefined (reading 'id')` thrown out of the entity-event dispatch.
|
|
839
|
+
- **Fix**: guard on `selected_line === undefined` after the `weightedRandomFromArray` call.
|
|
840
|
+
- **Confidence**: Confirmed
|
|
841
|
+
|
|
842
|
+
---
|
|
843
|
+
|
|
844
|
+
## Attachment / parent / transform / IK
|
|
845
|
+
|
|
846
|
+
### ECS-49 `EntityNode`'s `parent` setter strips `TransformAttachment` and never re-adds it
|
|
847
|
+
- **Severity**: MAJOR
|
|
848
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/parent/EntityNode.js:240-270`
|
|
849
|
+
- **Defect**: The setter removes both components up front
|
|
850
|
+
(`let parent_entity = this_entity.removeComponent(ParentEntity); let attachment = this_entity.removeComponent(TransformAttachment);`)
|
|
851
|
+
and, in the `node !== null` branch, explicitly re-adds `ParentEntity` via `this_entity.add(parent_entity)`
|
|
852
|
+
— but the attachment is only put back through `this.__safe_get_attachment()`, which runs **only when
|
|
853
|
+
`attachment === null`**. When the node already had a `TransformAttachment`, the removed instance is
|
|
854
|
+
mutated (`attachment.parent = parent_entity_id`) and dropped on the floor.
|
|
855
|
+
- **Failure scenario** (reproduced): three built `EntityNode`s A, B, C each with a `Transform`.
|
|
856
|
+
```js
|
|
857
|
+
A.addChild(C); // C -> ParentEntity{0}, TransformAttachment{parent:0}
|
|
858
|
+
C.parent = B; // public setter, the documented re-parent entry point
|
|
859
|
+
```
|
|
860
|
+
Observed: `ParentEntity.entity` correctly becomes `1`, but
|
|
861
|
+
`C.entity.getComponent(TransformAttachment)` is `null` and
|
|
862
|
+
`ds.getComponent(C.entity.id, TransformAttachment)` is `undefined`. `TransformAttachmentSystem` has
|
|
863
|
+
unlinked C, so C's world transform is no longer driven by any parent — the node visually detaches
|
|
864
|
+
while still being logically parented (and still gets destroyed when B is destroyed).
|
|
865
|
+
`addChild` escapes this only because it routes through `removeChild` → `node.parent = null` first,
|
|
866
|
+
which nulls both.
|
|
867
|
+
- **Fix**: after `attachment.parent = parent_entity_id`, put the component back —
|
|
868
|
+
`if (this_entity.getComponent(TransformAttachment) === null) this_entity.add(attachment);` — or
|
|
869
|
+
simply do not remove `TransformAttachment` at the top when `node !== null`.
|
|
870
|
+
- **Confidence**: Confirmed
|
|
871
|
+
|
|
872
|
+
### ECS-50 `InverseKinematicsSystem` declares read-only access to `Mesh` because of an undefined enum member
|
|
873
|
+
- **Severity**: MINOR
|
|
874
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/ik/InverseKinematicsSystem.js:18`
|
|
875
|
+
- **Defect**: `ResourceAccessSpecification.from(Mesh, ResourceAccessKind.Read | ResourceAccessSpecification.Write)`
|
|
876
|
+
— `Write` is a member of `ResourceAccessKind`, not of `ResourceAccessSpecification`. The latter has no
|
|
877
|
+
such static, so the expression is `1 | undefined === 1`: the system declares `Read` only, while
|
|
878
|
+
`TwoBoneInverseKinematicsSolver.solve` actually mutates the mesh's bone quaternions
|
|
879
|
+
(`boneA.setRotationFromQuaternion(...)`, `TwoBoneInverseKinematicsSolver.js:173-174`).
|
|
880
|
+
- **Failure scenario**: `EntityManager.updateExecutionOrder`'s `scoreSystem` weights a `Write` component
|
|
881
|
+
at `attached_edge_count * 2` and a `Read` at `* 1`, and `computeSystemComponentDependencyGraph` only
|
|
882
|
+
emits edges out of a system that declares `Write`/`Create`. With the typo, `InverseKinematicsSystem`
|
|
883
|
+
contributes no dependency edges and is scored as a pure `Mesh` reader, so it can be ordered *after*
|
|
884
|
+
the systems that consume mesh/bone state in the same `simulate()` call — IK corrections (feet planted
|
|
885
|
+
on terrain) are applied one frame late, producing visible foot sliding / ground penetration on the
|
|
886
|
+
frame a character's pose changes.
|
|
887
|
+
- **Fix**: `ResourceAccessKind.Read | ResourceAccessKind.Write`.
|
|
888
|
+
- **Confidence**: Confirmed
|
|
889
|
+
|
|
890
|
+
### ECS-51 `OneBoneSurfaceAlignmentSolver` recovers the alignment angle with `asin`, which cannot exceed 90°
|
|
891
|
+
- **Severity**: MINOR
|
|
892
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/ecs/ik/OneBoneSurfaceAlignmentSolver.js:170-175`
|
|
893
|
+
- **Defect**: `axis.crossVectors(v3, Vector3.forward); const betaSine = axis.length(); const beta = Math.asin(clamp(betaSine, -1, 1));`
|
|
894
|
+
— `|a × b| = sin θ` is ambiguous between `θ` and `π − θ`, and `asin` always returns the branch in
|
|
895
|
+
`[0, π/2]`. The angle between the contact normal and the bone axis is not recoverable this way once it
|
|
896
|
+
exceeds 90°.
|
|
897
|
+
- **Failure scenario**: bone axis `(0,0,1)` in bone space, contact normal in bone space
|
|
898
|
+
`(0, 0.9, −0.436)` — 116° apart. `|cross| = 0.9` → `beta = asin(0.9) = 64.2°`. The bone is rotated
|
|
899
|
+
64° instead of 116°, so instead of squaring up to the surface it ends up 52° off, tilted into the
|
|
900
|
+
surface. Any surface whose normal is more than 90° from the bone's own axis (steep slopes,
|
|
901
|
+
overhangs, an inverted foot-bone rig) is aligned to the wrong angle rather than clamped by
|
|
902
|
+
`constraint.limit`.
|
|
903
|
+
- **Fix**: recover the angle unambiguously — `Math.atan2(axis.length(), v3.dot(Vector3.forward))` —
|
|
904
|
+
before normalising `axis`.
|
|
905
|
+
- **Confidence**: Likely (the trigonometric limitation is unambiguous; I did not exercise it against a
|
|
906
|
+
real rig, so how often a live skeleton crosses 90° is unverified)
|
|
907
|
+
|
|
908
|
+
---
|
|
909
|
+
|
|
910
|
+
## Status of previously-known items
|
|
911
|
+
|
|
912
|
+
| Item | Status |
|
|
913
|
+
| --- | --- |
|
|
914
|
+
| `Entity.promiseEvent` calling `removeEventListener` instead of `add` | **FIXED** (`Entity.js:326-327` now `addEventListener`) |
|
|
915
|
+
| `EntityComponentDataset.dropData` not clearing `__entityAnyEventListeners` | **FIXED** (`EntityComponentDataset.js:1882`) |
|
|
916
|
+
| `HeadsUpDisplaySystem` scaling world-offset Z by `scale.y` | **FIXED** (`HeadsUpDisplaySystem.js:88` uses `transform_scale.z`) |
|
|
917
|
+
| `obtainTileAtWorldPosition2D` dereferencing an out-of-bounds tile | **STILL PRESENT** (`TerrainTileManager.js:487-489`; `getTileByWorldPosition2D` can return `undefined`, then `tile.gridPosition`) |
|
|
918
|
+
| `FogOfWar.rebuildSampler` width/height swap | **FIXED** (`FogOfWar.js:707-713`, correct `Sampler2D(data, itemSize, width, height)` order) |
|
|
919
|
+
| `TerrainTileManager.stitchTile` floor-vs-ceil | **STILL PRESENT** (`:566` uses `floor`; `:353`, `:405`, `:408`, `:422` all use `ceil`) |
|
|
920
|
+
| Event-dispatch pointer restore missing `try/finally` | **STILL PRESENT** (`EntityComponentDataset.js:2138-2162` and `2180-2204`) |
|