@vgai/engine 0.5.5 → 0.5.7
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/dist/adapter/adapter-surface.d.ts +30 -0
- package/dist/adapter/adapter-surface.d.ts.map +1 -0
- package/dist/adapter/adapter-surface.js +18 -0
- package/dist/adapter/authoring.d.ts +597 -0
- package/dist/adapter/authoring.d.ts.map +1 -0
- package/dist/adapter/authoring.js +22 -0
- package/dist/adapter/colyseus-networking-adapter.d.ts +77 -0
- package/dist/adapter/colyseus-networking-adapter.d.ts.map +1 -0
- package/dist/adapter/colyseus-networking-adapter.js +76 -0
- package/dist/adapter/first-party-systems.d.ts +23 -0
- package/dist/adapter/first-party-systems.d.ts.map +1 -0
- package/dist/adapter/first-party-systems.js +196 -0
- package/dist/adapter/host-context.d.ts +194 -0
- package/dist/adapter/host-context.d.ts.map +1 -0
- package/dist/adapter/host-context.js +80 -0
- package/dist/adapter/index.d.ts +33 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +25 -0
- package/dist/adapter/ingest/game-contract.d.ts +53 -0
- package/dist/adapter/ingest/game-contract.d.ts.map +1 -0
- package/dist/adapter/ingest/game-contract.js +30 -0
- package/dist/adapter/ingest/scene-capture.d.ts +121 -0
- package/dist/adapter/ingest/scene-capture.d.ts.map +1 -0
- package/dist/adapter/ingest/scene-capture.js +270 -0
- package/dist/adapter/ingest/structural-ids.d.ts +64 -0
- package/dist/adapter/ingest/structural-ids.d.ts.map +1 -0
- package/dist/adapter/ingest/structural-ids.js +110 -0
- package/dist/adapter/ingest/upstream-pin.d.ts +44 -0
- package/dist/adapter/ingest/upstream-pin.d.ts.map +1 -0
- package/dist/adapter/ingest/upstream-pin.js +47 -0
- package/dist/adapter/loop-gate-report.d.ts +34 -0
- package/dist/adapter/loop-gate-report.d.ts.map +1 -0
- package/dist/adapter/loop-gate-report.js +37 -0
- package/dist/adapter/rapier-physics-adapter.d.ts +69 -0
- package/dist/adapter/rapier-physics-adapter.d.ts.map +1 -0
- package/dist/adapter/rapier-physics-adapter.js +135 -0
- package/dist/adapter/root-adapter.d.ts +195 -0
- package/dist/adapter/root-adapter.d.ts.map +1 -0
- package/dist/adapter/root-adapter.js +25 -0
- package/dist/adapter/setup-three-root-adapter.d.ts +87 -0
- package/dist/adapter/setup-three-root-adapter.d.ts.map +1 -0
- package/dist/adapter/setup-three-root-adapter.js +791 -0
- package/dist/adapter/system-adapter.d.ts +453 -0
- package/dist/adapter/system-adapter.d.ts.map +1 -0
- package/dist/adapter/system-adapter.js +13 -0
- package/dist/adapter/transform.d.ts +18 -0
- package/dist/adapter/transform.d.ts.map +1 -0
- package/dist/adapter/transform.js +1 -0
- package/dist/ai/navigation.d.ts +78 -0
- package/dist/ai/navigation.d.ts.map +1 -0
- package/dist/ai/navigation.js +152 -0
- package/dist/animation/anim-graph-types.d.ts +25 -0
- package/dist/animation/anim-graph-types.d.ts.map +1 -0
- package/dist/animation/anim-graph-types.js +1 -0
- package/dist/animation/animation-clock.d.ts +213 -0
- package/dist/animation/animation-clock.d.ts.map +1 -0
- package/dist/animation/animation-clock.js +301 -0
- package/dist/animation/blend-node.d.ts +22 -0
- package/dist/animation/blend-node.d.ts.map +1 -0
- package/dist/animation/blend-node.js +97 -0
- package/dist/animation/camera-ownership.d.ts +249 -0
- package/dist/animation/camera-ownership.d.ts.map +1 -0
- package/dist/animation/camera-ownership.js +169 -0
- package/dist/animation/cinematic-cues.d.ts +200 -0
- package/dist/animation/cinematic-cues.d.ts.map +1 -0
- package/dist/animation/cinematic-cues.js +213 -0
- package/dist/animation/clip-map.d.ts +12 -0
- package/dist/animation/clip-map.d.ts.map +1 -0
- package/dist/animation/clip-map.js +31 -0
- package/dist/animation/cubic-spline-interpolant.d.ts +54 -0
- package/dist/animation/cubic-spline-interpolant.d.ts.map +1 -0
- package/dist/animation/cubic-spline-interpolant.js +111 -0
- package/dist/animation/theatre-clock-binding.d.ts +89 -0
- package/dist/animation/theatre-clock-binding.d.ts.map +1 -0
- package/dist/animation/theatre-clock-binding.js +29 -0
- package/dist/animation/theatre-director.d.ts +277 -0
- package/dist/animation/theatre-director.d.ts.map +1 -0
- package/dist/animation/theatre-director.js +248 -0
- package/dist/animation/theatre-object-binding.d.ts +305 -0
- package/dist/animation/theatre-object-binding.d.ts.map +1 -0
- package/dist/animation/theatre-object-binding.js +351 -0
- package/dist/animation/xstate-animation-binding.d.ts +119 -0
- package/dist/animation/xstate-animation-binding.d.ts.map +1 -0
- package/dist/animation/xstate-animation-binding.js +349 -0
- package/dist/animation/xstate-animation-meta.d.ts +266 -0
- package/dist/animation/xstate-animation-meta.d.ts.map +1 -0
- package/dist/animation/xstate-animation-meta.js +252 -0
- package/dist/asset-formats/camera.d.ts +15 -0
- package/dist/asset-formats/camera.d.ts.map +1 -0
- package/dist/asset-formats/camera.js +15 -0
- package/dist/asset-formats/collider.d.ts +21 -0
- package/dist/asset-formats/collider.d.ts.map +1 -0
- package/dist/asset-formats/collider.js +53 -0
- package/dist/asset-formats/index.d.ts +46 -0
- package/dist/asset-formats/index.d.ts.map +1 -0
- package/dist/asset-formats/index.js +36 -0
- package/dist/asset-formats/instances.d.ts +17 -0
- package/dist/asset-formats/instances.d.ts.map +1 -0
- package/dist/asset-formats/instances.js +31 -0
- package/dist/asset-formats/light.d.ts +21 -0
- package/dist/asset-formats/light.d.ts.map +1 -0
- package/dist/asset-formats/light.js +35 -0
- package/dist/asset-formats/material.d.ts +129 -0
- package/dist/asset-formats/material.d.ts.map +1 -0
- package/dist/asset-formats/material.js +88 -0
- package/dist/asset-formats/mesh.d.ts +22 -0
- package/dist/asset-formats/mesh.d.ts.map +1 -0
- package/dist/asset-formats/mesh.js +64 -0
- package/dist/asset-formats/parse.d.ts +20 -0
- package/dist/asset-formats/parse.d.ts.map +1 -0
- package/dist/asset-formats/parse.js +34 -0
- package/dist/asset-formats/particles.d.ts +1390 -0
- package/dist/asset-formats/particles.d.ts.map +1 -0
- package/dist/asset-formats/particles.js +319 -0
- package/dist/asset-formats/render-env.d.ts +387 -0
- package/dist/asset-formats/render-env.d.ts.map +1 -0
- package/dist/asset-formats/render-env.js +300 -0
- package/dist/asset-formats/tuples.d.ts +12 -0
- package/dist/asset-formats/tuples.d.ts.map +1 -0
- package/dist/asset-formats/tuples.js +14 -0
- package/dist/asset-loaders.d.ts +97 -0
- package/dist/asset-loaders.d.ts.map +1 -0
- package/dist/asset-loaders.js +209 -0
- package/dist/asset-parse-error.d.ts +24 -0
- package/dist/asset-parse-error.d.ts.map +1 -0
- package/dist/asset-parse-error.js +29 -0
- package/dist/asset-registry.d.ts +38 -0
- package/dist/asset-registry.d.ts.map +1 -0
- package/dist/asset-registry.js +66 -0
- package/dist/assets.d.ts +26 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/assets.js +55 -0
- package/dist/audio/wav-encode.d.ts +38 -0
- package/dist/audio/wav-encode.d.ts.map +1 -0
- package/dist/audio/wav-encode.js +97 -0
- package/dist/canvas-react/engine-bridge.d.ts +45 -0
- package/dist/canvas-react/engine-bridge.d.ts.map +1 -0
- package/dist/canvas-react/engine-bridge.js +45 -0
- package/dist/canvas-react/index.d.ts +42 -0
- package/dist/canvas-react/index.d.ts.map +1 -0
- package/dist/canvas-react/index.js +41 -0
- package/dist/canvas-react/pixi-primitive.d.ts +85 -0
- package/dist/canvas-react/pixi-primitive.d.ts.map +1 -0
- package/dist/canvas-react/pixi-primitive.js +181 -0
- package/dist/canvas-react/pixi-react-adapter.d.ts +71 -0
- package/dist/canvas-react/pixi-react-adapter.d.ts.map +1 -0
- package/dist/canvas-react/pixi-react-adapter.js +229 -0
- package/dist/canvas-react/pixi-react-root-factory.d.ts +56 -0
- package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -0
- package/dist/canvas-react/pixi-react-root-factory.js +73 -0
- package/dist/canvas-react/world-context.d.ts +88 -0
- package/dist/canvas-react/world-context.d.ts.map +1 -0
- package/dist/canvas-react/world-context.js +212 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +8 -0
- package/dist/core/frame-pacing.d.ts +90 -0
- package/dist/core/frame-pacing.d.ts.map +1 -0
- package/dist/core/frame-pacing.js +89 -0
- package/dist/core/game-loop.d.ts +62 -0
- package/dist/core/game-loop.d.ts.map +1 -0
- package/dist/core/game-loop.js +189 -0
- package/dist/core/seeded-random.d.ts +69 -0
- package/dist/core/seeded-random.d.ts.map +1 -0
- package/dist/core/seeded-random.js +121 -0
- package/dist/core/sim-clock.d.ts +199 -0
- package/dist/core/sim-clock.d.ts.map +1 -0
- package/dist/core/sim-clock.js +291 -0
- package/dist/core/system-runner.d.ts +96 -0
- package/dist/core/system-runner.d.ts.map +1 -0
- package/dist/core/system-runner.js +253 -0
- package/dist/core/types.d.ts +111 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +25 -0
- package/dist/data/data-asset.d.ts +101 -0
- package/dist/data/data-asset.d.ts.map +1 -0
- package/dist/data/data-asset.js +122 -0
- package/dist/data/data-check-core.d.ts +109 -0
- package/dist/data/data-check-core.d.ts.map +1 -0
- package/dist/data/data-check-core.js +177 -0
- package/dist/data/data-ref.d.ts +79 -0
- package/dist/data/data-ref.d.ts.map +1 -0
- package/dist/data/data-ref.js +130 -0
- package/dist/data/vite-plugin-data.d.ts +79 -0
- package/dist/data/vite-plugin-data.d.ts.map +1 -0
- package/dist/data/vite-plugin-data.js +217 -0
- package/dist/defaults.d.ts +233 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +137 -0
- package/dist/dev/chrome-trace.d.ts +45 -0
- package/dist/dev/chrome-trace.d.ts.map +1 -0
- package/dist/dev/chrome-trace.js +114 -0
- package/dist/dev/console-bridge.d.ts +22 -0
- package/dist/dev/console-bridge.d.ts.map +1 -0
- package/dist/dev/console-bridge.js +71 -0
- package/dist/dev/debug-draw.d.ts +24 -0
- package/dist/dev/debug-draw.d.ts.map +1 -0
- package/dist/dev/debug-draw.js +73 -0
- package/dist/dev/instruments.d.ts +133 -0
- package/dist/dev/instruments.d.ts.map +1 -0
- package/dist/dev/instruments.js +231 -0
- package/dist/dev/logger.d.ts +30 -0
- package/dist/dev/logger.d.ts.map +1 -0
- package/dist/dev/logger.js +90 -0
- package/dist/dev/performance-profiler.d.ts +111 -0
- package/dist/dev/performance-profiler.d.ts.map +1 -0
- package/dist/dev/performance-profiler.js +215 -0
- package/dist/dev/render-debug-adapter.d.ts +57 -0
- package/dist/dev/render-debug-adapter.d.ts.map +1 -0
- package/dist/dev/render-debug-adapter.js +141 -0
- package/dist/dev/render-memory.d.ts +53 -0
- package/dist/dev/render-memory.d.ts.map +1 -0
- package/dist/dev/render-memory.js +190 -0
- package/dist/dev/webgl-frame-capture.d.ts +134 -0
- package/dist/dev/webgl-frame-capture.d.ts.map +1 -0
- package/dist/dev/webgl-frame-capture.js +270 -0
- package/dist/dev/webgl-gpu-timer.d.ts +8 -0
- package/dist/dev/webgl-gpu-timer.d.ts.map +1 -0
- package/dist/dev/webgl-gpu-timer.js +52 -0
- package/dist/ecs/scene-index.d.ts +179 -0
- package/dist/ecs/scene-index.d.ts.map +1 -0
- package/dist/ecs/scene-index.js +331 -0
- package/dist/ecs/scene-query.d.ts +6 -0
- package/dist/ecs/scene-query.d.ts.map +1 -0
- package/dist/ecs/scene-query.js +40 -0
- package/dist/ecs/user-data.d.ts +193 -0
- package/dist/ecs/user-data.d.ts.map +1 -0
- package/dist/ecs/user-data.js +145 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/input/input-manager.d.ts +944 -0
- package/dist/input/input-manager.d.ts.map +1 -0
- package/dist/input/input-manager.js +2209 -0
- package/dist/input/input-types.d.ts +174 -0
- package/dist/input/input-types.d.ts.map +1 -0
- package/dist/input/input-types.js +1 -0
- package/dist/input/prompt-labels.d.ts +21 -0
- package/dist/input/prompt-labels.d.ts.map +1 -0
- package/dist/input/prompt-labels.js +121 -0
- package/dist/input/rebind-controller.d.ts +59 -0
- package/dist/input/rebind-controller.d.ts.map +1 -0
- package/dist/input/rebind-controller.js +85 -0
- package/dist/input/schema.d.ts +16 -0
- package/dist/input/schema.d.ts.map +1 -0
- package/dist/input/schema.js +228 -0
- package/dist/loader.d.ts +76 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +114 -0
- package/dist/manifest/editor-port.d.ts +53 -0
- package/dist/manifest/editor-port.d.ts.map +1 -0
- package/dist/manifest/editor-port.js +67 -0
- package/dist/manifest/filename.d.ts +14 -0
- package/dist/manifest/filename.d.ts.map +1 -0
- package/dist/manifest/filename.js +43 -0
- package/dist/manifest/index.d.ts +6 -0
- package/dist/manifest/index.d.ts.map +1 -0
- package/dist/manifest/index.js +9 -0
- package/dist/manifest/load-file.d.ts +7 -0
- package/dist/manifest/load-file.d.ts.map +1 -0
- package/dist/manifest/load-file.js +23 -0
- package/dist/manifest/load.d.ts +161 -0
- package/dist/manifest/load.d.ts.map +1 -0
- package/dist/manifest/load.js +269 -0
- package/dist/manifest/locate.d.ts +24 -0
- package/dist/manifest/locate.d.ts.map +1 -0
- package/dist/manifest/locate.js +48 -0
- package/dist/manifest/schema.d.ts +293 -0
- package/dist/manifest/schema.d.ts.map +1 -0
- package/dist/manifest/schema.js +633 -0
- package/dist/physics/collider-dimensions.d.ts +63 -0
- package/dist/physics/collider-dimensions.d.ts.map +1 -0
- package/dist/physics/collider-dimensions.js +98 -0
- package/dist/physics/collision-system.d.ts +41 -0
- package/dist/physics/collision-system.d.ts.map +1 -0
- package/dist/physics/collision-system.js +64 -0
- package/dist/physics/physics-registry.d.ts +45 -0
- package/dist/physics/physics-registry.d.ts.map +1 -0
- package/dist/physics/physics-registry.js +65 -0
- package/dist/physics/transform-writer.d.ts +16 -0
- package/dist/physics/transform-writer.d.ts.map +1 -0
- package/dist/physics/transform-writer.js +38 -0
- package/dist/pixi/authoring.d.ts +93 -0
- package/dist/pixi/authoring.d.ts.map +1 -0
- package/dist/pixi/authoring.js +207 -0
- package/dist/pixi/index.d.ts +16 -0
- package/dist/pixi/index.d.ts.map +1 -0
- package/dist/pixi/index.js +15 -0
- package/dist/pixi/ingest-iframe.d.ts +82 -0
- package/dist/pixi/ingest-iframe.d.ts.map +1 -0
- package/dist/pixi/ingest-iframe.js +166 -0
- package/dist/pixi/ingest.d.ts +97 -0
- package/dist/pixi/ingest.d.ts.map +1 -0
- package/dist/pixi/ingest.js +40 -0
- package/dist/pixi/physics-registry.d.ts +24 -0
- package/dist/pixi/physics-registry.d.ts.map +1 -0
- package/dist/pixi/physics-registry.js +38 -0
- package/dist/pixi/scene-capture.d.ts +43 -0
- package/dist/pixi/scene-capture.d.ts.map +1 -0
- package/dist/pixi/scene-capture.js +92 -0
- package/dist/pixi/system-adapters.d.ts +16 -0
- package/dist/pixi/system-adapters.d.ts.map +1 -0
- package/dist/pixi/system-adapters.js +37 -0
- package/dist/react/unmanaged-root-detector.d.ts +18 -0
- package/dist/react/unmanaged-root-detector.d.ts.map +1 -0
- package/dist/react/unmanaged-root-detector.js +70 -0
- package/dist/react/use-data.d.ts +60 -0
- package/dist/react/use-data.d.ts.map +1 -0
- package/dist/react/use-data.js +101 -0
- package/dist/react/use-selection.d.ts +54 -0
- package/dist/react/use-selection.d.ts.map +1 -0
- package/dist/react/use-selection.js +71 -0
- package/dist/react/world-state.d.ts +154 -0
- package/dist/react/world-state.d.ts.map +1 -0
- package/dist/react/world-state.js +220 -0
- package/dist/render/auto-batcher.d.ts +34 -0
- package/dist/render/auto-batcher.d.ts.map +1 -0
- package/dist/render/auto-batcher.js +140 -0
- package/dist/render/instance-mesh.d.ts +5 -0
- package/dist/render/instance-mesh.d.ts.map +1 -0
- package/dist/render/instance-mesh.js +19 -0
- package/dist/render/light-camera-factory.d.ts +24 -0
- package/dist/render/light-camera-factory.d.ts.map +1 -0
- package/dist/render/light-camera-factory.js +71 -0
- package/dist/render/lod.d.ts +13 -0
- package/dist/render/lod.d.ts.map +1 -0
- package/dist/render/lod.js +16 -0
- package/dist/render/material-factory.d.ts +15 -0
- package/dist/render/material-factory.d.ts.map +1 -0
- package/dist/render/material-factory.js +186 -0
- package/dist/render/mesh-shadow.d.ts +11 -0
- package/dist/render/mesh-shadow.d.ts.map +1 -0
- package/dist/render/mesh-shadow.js +19 -0
- package/dist/render/particles-factory.d.ts +99 -0
- package/dist/render/particles-factory.d.ts.map +1 -0
- package/dist/render/particles-factory.js +361 -0
- package/dist/render/render-batch-system.d.ts +33 -0
- package/dist/render/render-batch-system.d.ts.map +1 -0
- package/dist/render/render-batch-system.js +223 -0
- package/dist/render/render-features.d.ts +47 -0
- package/dist/render/render-features.d.ts.map +1 -0
- package/dist/render/render-features.js +105 -0
- package/dist/render/render-settings.d.ts +35 -0
- package/dist/render/render-settings.d.ts.map +1 -0
- package/dist/render/render-settings.js +51 -0
- package/dist/render/spark-renderer-lifecycle.d.ts +18 -0
- package/dist/render/spark-renderer-lifecycle.d.ts.map +1 -0
- package/dist/render/spark-renderer-lifecycle.js +57 -0
- package/dist/render/viewport-shading.d.ts +17 -0
- package/dist/render/viewport-shading.d.ts.map +1 -0
- package/dist/render/viewport-shading.js +87 -0
- package/dist/runtime/create-runtime.d.ts +259 -0
- package/dist/runtime/create-runtime.d.ts.map +1 -0
- package/dist/runtime/create-runtime.js +666 -0
- package/dist/runtime/debug-bridge.d.ts +288 -0
- package/dist/runtime/debug-bridge.d.ts.map +1 -0
- package/dist/runtime/debug-bridge.js +277 -0
- package/dist/runtime/debug-registry.d.ts +287 -0
- package/dist/runtime/debug-registry.d.ts.map +1 -0
- package/dist/runtime/debug-registry.js +528 -0
- package/dist/runtime/dev-layers.d.ts +34 -0
- package/dist/runtime/dev-layers.d.ts.map +1 -0
- package/dist/runtime/dev-layers.js +40 -0
- package/dist/runtime/frame-selector-cache.d.ts +41 -0
- package/dist/runtime/frame-selector-cache.d.ts.map +1 -0
- package/dist/runtime/frame-selector-cache.js +65 -0
- package/dist/runtime/game.d.ts +576 -0
- package/dist/runtime/game.d.ts.map +1 -0
- package/dist/runtime/game.js +1003 -0
- package/dist/runtime/gameplay-rng-trap.d.ts +47 -0
- package/dist/runtime/gameplay-rng-trap.d.ts.map +1 -0
- package/dist/runtime/gameplay-rng-trap.js +95 -0
- package/dist/runtime/input-router.d.ts +78 -0
- package/dist/runtime/input-router.d.ts.map +1 -0
- package/dist/runtime/input-router.js +171 -0
- package/dist/runtime/mount-game.d.ts +126 -0
- package/dist/runtime/mount-game.d.ts.map +1 -0
- package/dist/runtime/mount-game.js +159 -0
- package/dist/runtime/mount-manifest.d.ts +174 -0
- package/dist/runtime/mount-manifest.d.ts.map +1 -0
- package/dist/runtime/mount-manifest.js +304 -0
- package/dist/runtime/presentation.d.ts +85 -0
- package/dist/runtime/presentation.d.ts.map +1 -0
- package/dist/runtime/presentation.js +92 -0
- package/dist/runtime/render-audio-control.d.ts +126 -0
- package/dist/runtime/render-audio-control.d.ts.map +1 -0
- package/dist/runtime/render-audio-control.js +116 -0
- package/dist/runtime/render-control.d.ts +347 -0
- package/dist/runtime/render-control.d.ts.map +1 -0
- package/dist/runtime/render-control.js +317 -0
- package/dist/runtime/render-seed.d.ts +56 -0
- package/dist/runtime/render-seed.d.ts.map +1 -0
- package/dist/runtime/render-seed.js +73 -0
- package/dist/runtime/state-bridge.d.ts +60 -0
- package/dist/runtime/state-bridge.d.ts.map +1 -0
- package/dist/runtime/state-bridge.js +62 -0
- package/dist/runtime/types.d.ts +285 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/runtime/types.js +1 -0
- package/dist/runtime/unexported-game-trap.d.ts +36 -0
- package/dist/runtime/unexported-game-trap.d.ts.map +1 -0
- package/dist/runtime/unexported-game-trap.js +80 -0
- package/dist/setup/setup-audio.d.ts +20 -0
- package/dist/setup/setup-audio.d.ts.map +1 -0
- package/dist/setup/setup-audio.js +41 -0
- package/dist/setup/setup-particles.d.ts +17 -0
- package/dist/setup/setup-particles.d.ts.map +1 -0
- package/dist/setup/setup-particles.js +16 -0
- package/dist/setup/setup-physics.d.ts +25 -0
- package/dist/setup/setup-physics.d.ts.map +1 -0
- package/dist/setup/setup-physics.js +51 -0
- package/dist/setup/setup-renderer.d.ts +82 -0
- package/dist/setup/setup-renderer.d.ts.map +1 -0
- package/dist/setup/setup-renderer.js +381 -0
- package/dist/world3d-react/engine-bridge.d.ts +47 -0
- package/dist/world3d-react/engine-bridge.d.ts.map +1 -0
- package/dist/world3d-react/engine-bridge.js +48 -0
- package/dist/world3d-react/index.d.ts +47 -0
- package/dist/world3d-react/index.d.ts.map +1 -0
- package/dist/world3d-react/index.js +46 -0
- package/dist/world3d-react/r3f-adapter.d.ts +51 -0
- package/dist/world3d-react/r3f-adapter.d.ts.map +1 -0
- package/dist/world3d-react/r3f-adapter.js +368 -0
- package/dist/world3d-react/r3f-root-factory.d.ts +62 -0
- package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -0
- package/dist/world3d-react/r3f-root-factory.js +79 -0
- package/dist/world3d-react/rapier-physics-bridge.d.ts +76 -0
- package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -0
- package/dist/world3d-react/rapier-physics-bridge.js +120 -0
- package/dist/world3d-react/renderer-config.d.ts +58 -0
- package/dist/world3d-react/renderer-config.d.ts.map +1 -0
- package/dist/world3d-react/renderer-config.js +96 -0
- package/dist/world3d-react/world-context.d.ts +123 -0
- package/dist/world3d-react/world-context.d.ts.map +1 -0
- package/dist/world3d-react/world-context.js +204 -0
- package/dist-config/config.js +303 -0
- package/dist-config/config.js.map +7 -0
- package/package.json +14 -3
- package/src/adapter/rapier-physics-adapter.ts +77 -34
- package/src/config.ts +8 -0
- package/src/core/frame-pacing.ts +26 -0
- package/src/core/game-loop.ts +4 -3
- package/src/dev/instruments.ts +302 -0
- package/src/setup/setup-renderer.ts +2 -0
- package/src/world3d-react/index.ts +9 -0
- package/src/world3d-react/rapier-physics-bridge.tsx +137 -0
|
@@ -0,0 +1,1003 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Game root — T7.1 slice 1 ("introduce Game internally") + slice 2 ("world
|
|
3
|
+
* registry + ordered frame algorithm"), extended in T7.3 slice 1 with real
|
|
4
|
+
* `'canvas'`-kind `RootInstance` support (`stage`/`physics2d`, a real
|
|
5
|
+
* `pixiStage()`) so a canvas adapter can register its world onto a real `Game`
|
|
6
|
+
* instead of driving its own loop, and in T7.3 slice 2 with a
|
|
7
|
+
* construction-time stage check (symmetric with the three/scene check).
|
|
8
|
+
* T6.2 slice 1 adds real `'react'`-kind
|
|
9
|
+
* `RootInstance` support (`container`, a real `reactRoot()`) — the DOM-root
|
|
10
|
+
* world surface `runtime/create-runtime.ts`'s roots path now mounts, with
|
|
11
|
+
* `physics`/`collisions`/`camera`/`frame` `undefined` for them exactly
|
|
12
|
+
* like an opaque/foreign mount.
|
|
13
|
+
*
|
|
14
|
+
* (D6), particularly §4 (the frame algorithm) and §8 stage 2, for the full
|
|
15
|
+
* target shape. This file holds the Game/ RootInstance shell, the NEW
|
|
16
|
+
* game-scoped `SystemRunner`, and the host-facing `runFrame` frame executor.
|
|
17
|
+
* The public surface is NOT frozen yet — do not add these exports to a
|
|
18
|
+
* barrel/index; only the in-scope runtime/adapter files import this module
|
|
19
|
+
* directly. The `gpu` per-world cache is the one remaining slice-1 omission.
|
|
20
|
+
*/
|
|
21
|
+
import { formatAudioGateMessage, formatLoopGateMessage } from '../adapter/loop-gate-report';
|
|
22
|
+
import { createSeededRandom, DEFAULT_SEEDED_RANDOM_SEED, registerSeededRandom, } from '../core/seeded-random';
|
|
23
|
+
import { createSimClock, registerSimClock } from '../core/sim-clock';
|
|
24
|
+
import { createSystemRunner } from '../core/system-runner';
|
|
25
|
+
import { PHASE_ORDER, SystemPhase } from '../core/types';
|
|
26
|
+
import { createPerformanceProfiler } from '../dev/performance-profiler';
|
|
27
|
+
import { hasUserData } from '../ecs/user-data';
|
|
28
|
+
import { InputManager } from '../input/input-manager';
|
|
29
|
+
import { createDebugRegistry, DebugError, registerDebugRegistry, } from './debug-registry';
|
|
30
|
+
import { createGameplayRngTrap, registerGameplayRngTrapControl } from './gameplay-rng-trap';
|
|
31
|
+
import { createStateBridge } from './state-bridge';
|
|
32
|
+
/**
|
|
33
|
+
* WO-P13 — the tail of `PHASE_ORDER` that presents rather than simulates, and
|
|
34
|
+
* therefore runs once per DISPLAY frame (`runRenderFrameImpl`) rather than
|
|
35
|
+
* once per fixed substep. The same two phases `runTicks`' `skipRenderPhases`
|
|
36
|
+
* fast-forward skips and `render-control.ts`'s `renderOnce()` drives — one
|
|
37
|
+
* definition of "the render phases" across all three, kept in `PHASE_ORDER`'s
|
|
38
|
+
* own order.
|
|
39
|
+
*/
|
|
40
|
+
const DISPLAY_RATE_PHASES = PHASE_ORDER.filter((phase) => phase === SystemPhase.PRE_RENDER || phase === SystemPhase.RENDER);
|
|
41
|
+
/**
|
|
42
|
+
* Build a `RootInstance` whose kind-narrowed accessors throw descriptively
|
|
43
|
+
* on kind mismatch. This is the one place that builds
|
|
44
|
+
* `threeScene`/`pixiStage`, so every world (however it's constructed, in this
|
|
45
|
+
* slice or later ones) gets identical throw behavior.
|
|
46
|
+
*/
|
|
47
|
+
export function createRootInstance(init) {
|
|
48
|
+
const { id, kind, pausable = true, adapter, mounted, scene, stage, container, physics, collisions, physics2d, camera, frame, } = init;
|
|
49
|
+
// A three world with no scene has nothing for `threeScene()` to return —
|
|
50
|
+
// fail loudly HERE, at construction, rather than letting `threeScene()`
|
|
51
|
+
// throw its generic "not a three world" message later for a world whose
|
|
52
|
+
// kind IS three (checklist item 5; a misleading error for this case).
|
|
53
|
+
if (kind === 'three' && !scene) {
|
|
54
|
+
throw new Error(`RootInstance "${id}" (kind: three): a three world requires a scene — ` +
|
|
55
|
+
'pass `scene` in RootInstanceInit.');
|
|
56
|
+
}
|
|
57
|
+
// Symmetric check for canvas (T7.3 slice 2) — a canvas world with no stage
|
|
58
|
+
// has nothing for `pixiStage()` to return; fail loudly here, at
|
|
59
|
+
// construction, same as the three/scene check above.
|
|
60
|
+
if (kind === 'canvas' && !stage) {
|
|
61
|
+
throw new Error(`RootInstance "${id}" (kind: canvas): a canvas world requires a stage — ` +
|
|
62
|
+
'pass `stage` in RootInstanceInit.');
|
|
63
|
+
}
|
|
64
|
+
// Symmetric check for react (T6.2 slice 1) — a react world with no
|
|
65
|
+
// container has nothing for `reactRoot()` to return; fail loudly here,
|
|
66
|
+
// at construction, same as the two checks above.
|
|
67
|
+
if (kind === 'dom' && !container) {
|
|
68
|
+
throw new Error(`RootInstance "${id}" (kind: react): a react world requires a container — ` +
|
|
69
|
+
'pass `container` in RootInstanceInit.');
|
|
70
|
+
}
|
|
71
|
+
// Disposed-world guard (T7.1 slice 3). There
|
|
72
|
+
// is no `unregisterRoot` (decision 4 — roots are manifest-declared; a
|
|
73
|
+
// whole Game is disposed, not one world out of its registry), so a caller
|
|
74
|
+
// that disposes ONE world's `mounted` directly (e.g. ending a sub-session)
|
|
75
|
+
// leaves that `RootInstance` sitting in `Game.roots` — and `runFrame`
|
|
76
|
+
// would otherwise keep invoking its (now-torn-down) frame hooks every
|
|
77
|
+
// subsequent frame. `MountedThreeRoot` has no public "am I disposed" flag to
|
|
78
|
+
// read, so this wraps `mounted.dispose` in place (mutating the SAME mount
|
|
79
|
+
// object every holder of `mounted` shares — calling `mounted.dispose()`
|
|
80
|
+
// directly, exactly like calling `world.mounted.dispose()`, trips this)
|
|
81
|
+
// to flip a private flag, and wraps the frame hooks so they silently no-op
|
|
82
|
+
// once that flag is set. `runFrame` itself needs no knowledge of disposal
|
|
83
|
+
// — a disposed world is skipped by construction from the next call
|
|
84
|
+
// onward. (The opaque-world `mounted.update` fallback path is unaffected
|
|
85
|
+
// by this guard — no opaque-mount two-world scenario exists yet to need it.)
|
|
86
|
+
let disposed = false;
|
|
87
|
+
const originalDispose = mounted.dispose.bind(mounted);
|
|
88
|
+
mounted.dispose = () => {
|
|
89
|
+
disposed = true;
|
|
90
|
+
originalDispose();
|
|
91
|
+
};
|
|
92
|
+
const guardedFrame = frame
|
|
93
|
+
? {
|
|
94
|
+
runPhase(phase, dt) {
|
|
95
|
+
if (disposed)
|
|
96
|
+
return;
|
|
97
|
+
frame.runPhase(phase, dt);
|
|
98
|
+
},
|
|
99
|
+
endFrame() {
|
|
100
|
+
if (disposed)
|
|
101
|
+
return;
|
|
102
|
+
frame.endFrame?.();
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
: undefined;
|
|
106
|
+
return {
|
|
107
|
+
id,
|
|
108
|
+
kind,
|
|
109
|
+
pausable,
|
|
110
|
+
adapter,
|
|
111
|
+
mounted,
|
|
112
|
+
physics,
|
|
113
|
+
collisions,
|
|
114
|
+
physics2d,
|
|
115
|
+
camera,
|
|
116
|
+
frame: guardedFrame,
|
|
117
|
+
threeScene() {
|
|
118
|
+
if (kind !== 'three' || !scene) {
|
|
119
|
+
throw new Error(`RootInstance "${id}" (kind: ${kind}): threeScene() requested but this world is not ` +
|
|
120
|
+
'a three world');
|
|
121
|
+
}
|
|
122
|
+
return scene;
|
|
123
|
+
},
|
|
124
|
+
pixiStage() {
|
|
125
|
+
// The `!stage` branch a canvas world could hit here is now unreachable
|
|
126
|
+
// (T7.3 slice 2): construction above throws for `kind === 'canvas'`
|
|
127
|
+
// with no `stage`, symmetric with `threeScene()`'s `scene` guard.
|
|
128
|
+
if (kind !== 'canvas' || !stage) {
|
|
129
|
+
throw new Error(`RootInstance "${id}" (kind: ${kind}): pixiStage() requested but this world is not ` +
|
|
130
|
+
'a canvas world');
|
|
131
|
+
}
|
|
132
|
+
return stage;
|
|
133
|
+
},
|
|
134
|
+
reactRoot() {
|
|
135
|
+
// The `!container` branch a react world could hit here is now
|
|
136
|
+
// unreachable (T6.2 slice 1): construction above throws for
|
|
137
|
+
// `kind === 'dom'` with no `container`, symmetric with
|
|
138
|
+
// `threeScene()`/`pixiStage()`'s guards.
|
|
139
|
+
if (kind !== 'dom' || !container) {
|
|
140
|
+
throw new Error(`RootInstance "${id}" (kind: ${kind}): reactRoot() requested but this world is not ` +
|
|
141
|
+
'a react world');
|
|
142
|
+
}
|
|
143
|
+
return container;
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Type guard for whether a `MountedThreeRoot` is a first-party
|
|
149
|
+
* `SetupThreeRootAdapter` mount (has a live `GameContext` at `.ctx`). Used to
|
|
150
|
+
* decide whether a world's `physics`/`collisions`/`camera` (and, on `Game`,
|
|
151
|
+
* `input`/`audio`) can be populated from it — an external
|
|
152
|
+
* adapter's mount has none of these first-party handles.
|
|
153
|
+
*
|
|
154
|
+
* Checks the `firstParty: true` brand (checklist item 1), NOT `'ctx' in
|
|
155
|
+
* mounted` — a foreign mount may carry an unrelated `ctx` key, so a
|
|
156
|
+
* structural `'ctx' in mounted` check would misfire. The probe is a plain property read, not an
|
|
157
|
+
* `instanceof`/value import of `setup-three-root-adapter.ts` — the
|
|
158
|
+
* `MountedSetupThreeRoot` import above stays type-only.
|
|
159
|
+
*/
|
|
160
|
+
export function isFirstPartyMounted(mounted) {
|
|
161
|
+
return mounted.firstParty === true;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Read an owner's Rapier handles, treating an absent pair as "this owner holds
|
|
165
|
+
* no rigid bodies" — the honest reading for a world (R3F, or a bare
|
|
166
|
+
* `ComponentManager`-only owner) that builds no first-party physics runtime.
|
|
167
|
+
*/
|
|
168
|
+
function readRapierHandles(owner) {
|
|
169
|
+
const { physics, rapierWorld } = owner;
|
|
170
|
+
if (!physics || !rapierWorld)
|
|
171
|
+
return null;
|
|
172
|
+
return { physics, rapierWorld };
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* P3 — what `SimClock.disposeAfter` actually does. `core/sim-clock.ts` knows
|
|
176
|
+
* only *when*; this is the *what*, and it lives here because it is the runtime
|
|
177
|
+
* that knows about Rapier and shared geometry.
|
|
178
|
+
*
|
|
179
|
+
* The order mirrors `adapter/setup-three-root-adapter.ts`'s world teardown for
|
|
180
|
+
* ONE subtree. Within the physics step,
|
|
181
|
+
* `rapierWorld.removeRigidBody(body)` comes BEFORE `physics.remove(node)`: the
|
|
182
|
+
* registry's `remove()` only drops index entries, so reversing the two leaks
|
|
183
|
+
* the Rapier body.
|
|
184
|
+
*
|
|
185
|
+
* `owners` is every world that could own part of the subtree — `createGame`
|
|
186
|
+
* passes all of its first-party three roots as their `GameContext`, a bare
|
|
187
|
+
* adapter mount passes its own ctx. `physics.get` is a no-op for a node the
|
|
188
|
+
* owner does not own, so offering the subtree to each is safe.
|
|
189
|
+
*
|
|
190
|
+
* Safe on an object already removed or already disposed: `removeFromParent` on
|
|
191
|
+
* a parentless `Object3D` is a no-op and three's `dispose()` calls are
|
|
192
|
+
* idempotent.
|
|
193
|
+
*/
|
|
194
|
+
export function disposeDebrisSubtree(obj, owners) {
|
|
195
|
+
const nodes = [];
|
|
196
|
+
obj.traverse((node) => nodes.push(node));
|
|
197
|
+
for (const owner of owners) {
|
|
198
|
+
const rapier = readRapierHandles(owner);
|
|
199
|
+
if (!rapier)
|
|
200
|
+
continue;
|
|
201
|
+
for (const node of nodes) {
|
|
202
|
+
const refs = rapier.physics.get(node);
|
|
203
|
+
if (!refs)
|
|
204
|
+
continue;
|
|
205
|
+
rapier.rapierWorld.removeRigidBody(refs.body);
|
|
206
|
+
rapier.physics.remove(node);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
obj.removeFromParent();
|
|
210
|
+
for (const node of nodes) {
|
|
211
|
+
// Duck-typed rather than `instanceof THREE.Mesh` so this module keeps its
|
|
212
|
+
// TYPE-ONLY three import (it is surface-neutral — it also hosts pixi
|
|
213
|
+
// roots). Covers Points/Line/Sprite debris too, which a Mesh check would
|
|
214
|
+
// silently leak.
|
|
215
|
+
const drawable = node;
|
|
216
|
+
if (!hasUserData(node, '__sharedGeometry'))
|
|
217
|
+
drawable.geometry?.dispose();
|
|
218
|
+
const material = drawable.material;
|
|
219
|
+
if (Array.isArray(material)) {
|
|
220
|
+
for (const m of material)
|
|
221
|
+
m.dispose();
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
material?.dispose();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The {@link DebrisOwner}s one registered world contributes: its `GameContext`
|
|
230
|
+
* when the mount is first-party, else nothing. Written as a standalone function
|
|
231
|
+
* so `createGame`'s clock disposer is one line and the "which mounts count"
|
|
232
|
+
* rule has exactly one home.
|
|
233
|
+
*/
|
|
234
|
+
function debrisOwnersOf(world) {
|
|
235
|
+
return isFirstPartyMounted(world.mounted) ? [world.mounted.ctx] : [];
|
|
236
|
+
}
|
|
237
|
+
function describeMismatch(handle, world) {
|
|
238
|
+
return (`Game.${handle}: default world "${world.id}" (kind: ${world.kind}) is not a first-party ` +
|
|
239
|
+
"mount — hoisting these to true Game ownership is a later slice's work; available only " +
|
|
240
|
+
'via a first-party default world today');
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Construct the (host-internal) Game shell. Callers: `createGameRuntime`
|
|
244
|
+
* builds this BEFORE mounting its one adapter, then registers the default
|
|
245
|
+
* three world once mount resolves (see `registerThreeRoot` in
|
|
246
|
+
* `create-runtime.ts`).
|
|
247
|
+
*/
|
|
248
|
+
export function createGame(opts) {
|
|
249
|
+
const roots = [];
|
|
250
|
+
const profiler = createPerformanceProfiler();
|
|
251
|
+
const systems = createSystemRunner(profiler.systemObserver, 'game');
|
|
252
|
+
const input = new InputManager();
|
|
253
|
+
const stateBridge = createStateBridge();
|
|
254
|
+
// D15 (T-D15.1) — the game-scoped seeded-random surface every world's
|
|
255
|
+
// `ctx.random` aliases (see `setup-three-root-adapter.ts`'s `ctx.random =
|
|
256
|
+
// ...`, wired the same way `ctx.debug` is just below). Constructed
|
|
257
|
+
// unconditionally (cheap — a handful of closures) regardless of whether
|
|
258
|
+
// this project ever declares `determinism.seededRandom`; only the BOOT
|
|
259
|
+
// SEED and the enforcement (the burn-down scan, the trap right below) are
|
|
260
|
+
// conditional on that declaration.
|
|
261
|
+
const seededRandom = createSeededRandom(opts.seed ?? DEFAULT_SEEDED_RANDOM_SEED);
|
|
262
|
+
// D15 (T-D15.3) — the dev-mode `Math.random` phase trap. Constructed
|
|
263
|
+
// unconditionally too (disabled by default: `rngTrapEnabled` starts
|
|
264
|
+
// `false`, so `runFrameImpl`'s enable/disable calls below are no-ops) —
|
|
265
|
+
// the manifest-aware boot path flips `rngTrapControl.setEnabled(true)`
|
|
266
|
+
// once it resolves `manifest.determinism?.seededRandom` (mirrors
|
|
267
|
+
// `debugRegistry.setRoomDeclared` being flipped post-hoc from the same
|
|
268
|
+
// boot path for the very same "only the caller who read the manifest
|
|
269
|
+
// knows" reason).
|
|
270
|
+
const rngTrap = createGameplayRngTrap();
|
|
271
|
+
let rngTrapEnabled = false;
|
|
272
|
+
const rngTrapControl = {
|
|
273
|
+
setEnabled(enabled) {
|
|
274
|
+
rngTrapEnabled = enabled;
|
|
275
|
+
},
|
|
276
|
+
get enabled() {
|
|
277
|
+
return rngTrapEnabled;
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
// `tick` counts completed fixed substeps, `simT` accumulates their `dt` —
|
|
281
|
+
// both game-scoped, bumped ONLY where `stateBridge.bump()` is (guarded by
|
|
282
|
+
// `advanced`, `runFrame`'s tail below), so a paused/frozen frame never
|
|
283
|
+
// advances either. The debug registry reads them via suppliers (not by
|
|
284
|
+
// capturing the numbers now) so its built-in `time` provider always sees
|
|
285
|
+
// the CURRENT values.
|
|
286
|
+
let tick = 0;
|
|
287
|
+
let simT = 0;
|
|
288
|
+
// WO-P13 — the display-rate half. `renderAlpha` is the last alpha
|
|
289
|
+
// `runRenderFrameImpl` presented at (0 until a host drives one); the set is
|
|
290
|
+
// the `RenderStepped`-shaped registry `Game.onRenderStep` feeds. Deliberately
|
|
291
|
+
// NOT beside `tick`/`simT` in meaning: neither of these ever advances sim
|
|
292
|
+
// state, which is exactly what makes running them at the monitor's rate safe.
|
|
293
|
+
let renderAlpha = 0;
|
|
294
|
+
const renderStepCallbacks = new Set();
|
|
295
|
+
// P3 — the ONE sim clock this Game owns, declared beside the accumulator it
|
|
296
|
+
// is bound to (`flush(simT)` runs at the tail of `runFrameImpl`, inside the
|
|
297
|
+
// same `advanced` guard as the two bumps above, so a paused/frozen frame
|
|
298
|
+
// fires no timers). Every world's `ctx.clock` is THIS instance, reached the
|
|
299
|
+
// same way `ctx.random`/`ctx.debug` reach their game-scoped singletons —
|
|
300
|
+
// `getSimClock(host.game)` over the WeakMap filed below.
|
|
301
|
+
//
|
|
302
|
+
// The disposer is supplied HERE rather than inside the clock because
|
|
303
|
+
// `core/sim-clock.ts` deliberately knows nothing about Rapier or shared
|
|
304
|
+
// geometry. It mirrors `setup-three-root-adapter.ts`'s world teardown
|
|
305
|
+
// ordering for ONE subtree.
|
|
306
|
+
const simClock = createSimClock({
|
|
307
|
+
// Every mount that could own part of the subtree is offered it:
|
|
308
|
+
// `physics.get` is a no-op for a node the mount does not own, so this is
|
|
309
|
+
// correct with several three roots on one Game and needs no ownership
|
|
310
|
+
// bookkeeping.
|
|
311
|
+
dispose: (obj) => disposeDebrisSubtree(obj, roots.flatMap(debrisOwnersOf)),
|
|
312
|
+
});
|
|
313
|
+
const debugRegistry = createDebugRegistry({
|
|
314
|
+
getTick: () => tick,
|
|
315
|
+
getSimT: () => simT,
|
|
316
|
+
// D15/T-D15.5 — "the manifest's first/default world" for the debug
|
|
317
|
+
// registry's world-addressed input-target compatibility surface. Every
|
|
318
|
+
// first-party root now registers the SAME game-owned InputManager, but the
|
|
319
|
+
// stable default id still keeps explicit/implicit debug routing coherent.
|
|
320
|
+
// Use the SAME "first three world, else first world" rule
|
|
321
|
+
// `requireDefaultRoot` (declared just below — safe: this closure is
|
|
322
|
+
// only ever CALLED later, once at least one world has mounted) already
|
|
323
|
+
// defines for `Game.defaultRoot`.
|
|
324
|
+
getDefaultRootId: () => (roots.length > 0 ? requireDefaultRoot().id : null),
|
|
325
|
+
// Issue #175 — the built-in `time` provider's `loopLiveness` field reads
|
|
326
|
+
// the REAL loop, not any UI-level play-state store: `opts.loop` is the
|
|
327
|
+
// SAME `GameLoop` this `Game`'s own `.loop` field exposes, so this
|
|
328
|
+
// registry can never disagree with `game.loop.liveness` about whether
|
|
329
|
+
// the loop is actually ticking.
|
|
330
|
+
getLoopLiveness: () => opts.loop.liveness,
|
|
331
|
+
});
|
|
332
|
+
let inputFrameActive = false;
|
|
333
|
+
systems.add('input', () => {
|
|
334
|
+
if (inputFrameActive)
|
|
335
|
+
input.poll(debugRegistry.getGameTick());
|
|
336
|
+
}, { name: 'input.poll' });
|
|
337
|
+
input.setDebugEmit((event, detail) => debugRegistry.forRoot('(game)').emit(event, detail));
|
|
338
|
+
let inputMapPath = null;
|
|
339
|
+
let inputMapLoad = null;
|
|
340
|
+
function requireDefaultRoot() {
|
|
341
|
+
if (roots.length === 0) {
|
|
342
|
+
throw new Error('Game.defaultRoot: no roots registered yet');
|
|
343
|
+
}
|
|
344
|
+
return roots.find((w) => w.kind === 'three') ?? roots[0];
|
|
345
|
+
}
|
|
346
|
+
function requireFirstPartyCtx(handle) {
|
|
347
|
+
const world = requireDefaultRoot();
|
|
348
|
+
if (!isFirstPartyMounted(world.mounted)) {
|
|
349
|
+
throw new Error(describeMismatch(handle, world));
|
|
350
|
+
}
|
|
351
|
+
return world.mounted.ctx;
|
|
352
|
+
}
|
|
353
|
+
// --- Game.systemAdapters aggregation (§7.1-3, probe1) --------------------
|
|
354
|
+
// Warn-once-per-colliding-key state, scoped to this Game instance (a fresh
|
|
355
|
+
// Game gets a fresh warn history) — deliberately NOT reset by anything
|
|
356
|
+
// short of a new `createGame` call, matching `reportedGateShortfalls`
|
|
357
|
+
// above's "once per game instance" idiom.
|
|
358
|
+
const warnedSystemAdapterKeys = new Set();
|
|
359
|
+
const systemAdapterListeners = new Set();
|
|
360
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one cohesive merge-with-collision-report walk (per-world × per-key); splitting the collision-warn branch out would obscure that it's part of the same pass, not reduce real complexity
|
|
361
|
+
function computeSystemAdapters() {
|
|
362
|
+
// Debug is game-scoped: React hooks, probes, and the built-in time
|
|
363
|
+
// provider all register with the one registry created above, regardless
|
|
364
|
+
// of whether any mounted root happens to expose a `systems` object. A
|
|
365
|
+
// root may still publish this SAME adapter (the first-party Three/Pixi
|
|
366
|
+
// mounts do); the reference-equality branch below treats that as the
|
|
367
|
+
// intentional shared registration it is.
|
|
368
|
+
const result = { debug: debugRegistry.adapter };
|
|
369
|
+
const ownerRootId = new Map([['debug', '(game)']]);
|
|
370
|
+
for (const world of roots) {
|
|
371
|
+
const adapters = world.mounted.systems;
|
|
372
|
+
if (!adapters)
|
|
373
|
+
continue;
|
|
374
|
+
for (const key of Object.keys(adapters)) {
|
|
375
|
+
if (adapters[key] === undefined)
|
|
376
|
+
continue;
|
|
377
|
+
const existingOwner = ownerRootId.get(key);
|
|
378
|
+
if (existingOwner !== undefined) {
|
|
379
|
+
// Reference-equality short-circuit: two roots sharing the ONE
|
|
380
|
+
// game-scoped debug registry's adapter (T1.1) both expose the SAME
|
|
381
|
+
// object under `systems.debug` — that is by design, not a
|
|
382
|
+
// collision, so it must never warn.
|
|
383
|
+
if (result[key] === adapters[key])
|
|
384
|
+
continue;
|
|
385
|
+
if (!warnedSystemAdapterKeys.has(key)) {
|
|
386
|
+
warnedSystemAdapterKeys.add(key);
|
|
387
|
+
// biome-ignore lint/suspicious/noConsole: structured, greppable — mirrors this file's own reportGateShortfallOnce's deliberate direct console.warn just above
|
|
388
|
+
console.warn(`[game] systemAdapters: "${key}" is registered by both world "${existingOwner}" and ` +
|
|
389
|
+
`world "${world.id}" — the FIRST registration ("${existingOwner}") wins; the later ` +
|
|
390
|
+
'one is shadowed (game-scoped system adapters).');
|
|
391
|
+
}
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
// biome-ignore lint/suspicious/noExplicitAny: SystemAdapters is a plain optional-field record; the per-key copy is correct by construction (same key on both sides), just not expressible without a cast
|
|
395
|
+
result[key] = adapters[key];
|
|
396
|
+
ownerRootId.set(key, world.id);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return result;
|
|
400
|
+
}
|
|
401
|
+
// --- D10/T7.6 play-state (Game.play) -------------------------------------
|
|
402
|
+
let paused = false;
|
|
403
|
+
// Dedupe loop-gate/audio-gate shortfall warnings to ONCE per (world, kind)
|
|
404
|
+
// — `pause()` fans out over every world every call; without this a
|
|
405
|
+
// multi-second play session would re-log the same "can't gate" shortfall
|
|
406
|
+
// every time the user hits Pause.
|
|
407
|
+
const reportedGateShortfalls = new Set();
|
|
408
|
+
function reportGateShortfallOnce(kind, worldId, reason) {
|
|
409
|
+
const key = `${kind}:${worldId}`;
|
|
410
|
+
if (reportedGateShortfalls.has(key))
|
|
411
|
+
return;
|
|
412
|
+
reportedGateShortfalls.add(key);
|
|
413
|
+
const message = kind === 'loop'
|
|
414
|
+
? formatLoopGateMessage({ worldId, reason })
|
|
415
|
+
: formatAudioGateMessage({ worldId, reason });
|
|
416
|
+
// Other native `console.warn`/`console.error` call sites in this file are
|
|
417
|
+
// unsuppressed and already counted in the lint baseline (see `runFrame`'s
|
|
418
|
+
// impl below); this one is a NEW site, so it's suppressed to keep this
|
|
419
|
+
// task's diff at zero NEW warnings (same reasoning as `achieved-tier.ts`'s
|
|
420
|
+
// identical suppression).
|
|
421
|
+
// biome-ignore lint/suspicious/noConsole: see comment above
|
|
422
|
+
console.warn(message);
|
|
423
|
+
}
|
|
424
|
+
/** Fan out a loop-gate (self-driven roots) + audio-gate call over every
|
|
425
|
+
* `pausable` world, reporting honestly (once) wherever the capability is
|
|
426
|
+
* absent — shared by `pause()`/`resume()` below (same fan-out, opposite
|
|
427
|
+
* boolean). */
|
|
428
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: fans out TWO independent capability gates (loop, audio) with the same "call it, else report once" shape per world — splitting the two gates into separate loops would duplicate the fan-out, not reduce real complexity
|
|
429
|
+
function setRootGates(next) {
|
|
430
|
+
for (const world of roots) {
|
|
431
|
+
if (!world.pausable)
|
|
432
|
+
continue; // pausable:false roots are untouched by design
|
|
433
|
+
if (world.mounted.drivesOwnLoop) {
|
|
434
|
+
if (world.mounted.setPaused) {
|
|
435
|
+
world.mounted.setPaused(next);
|
|
436
|
+
}
|
|
437
|
+
else if (next) {
|
|
438
|
+
reportGateShortfallOnce('loop', world.id, 'self-driven world, adapter declares no setPaused capability');
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const audio = world.mounted.systems?.audio;
|
|
442
|
+
if (audio) {
|
|
443
|
+
audio.setMuted(next);
|
|
444
|
+
}
|
|
445
|
+
else if (next) {
|
|
446
|
+
reportGateShortfallOnce('audio', world.id, 'no SystemAdapters.audio on this world');
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: the frame algorithm (now with D10's per-world pause gate + the onlyFrozen step()-only mode) is one cohesive nested loop over phases/roots — splitting it would obscure the ordering contract documented on GameInternal.runFrame
|
|
451
|
+
function runFrameImpl(dt, frameOpts) {
|
|
452
|
+
// D15 (T-D15.3) — brackets the ENTIRE frame body (every phase, every
|
|
453
|
+
// world, both the `onlyFrozen` and normal branches below converge on the
|
|
454
|
+
// single `profiler.endFrame()` at the tail) with zero reordering of the
|
|
455
|
+
// phase algorithm itself — a no-op pair of calls while
|
|
456
|
+
// `rngTrapEnabled` is false (the common case: most projects never
|
|
457
|
+
// declare `determinism.seededRandom`).
|
|
458
|
+
if (rngTrapEnabled)
|
|
459
|
+
rngTrap.enable();
|
|
460
|
+
profiler.beginFrame();
|
|
461
|
+
const ignorePause = frameOpts?.ignorePause ?? false;
|
|
462
|
+
// D15/T-D15.4 + WO-P13: the render-phase skip. Two callers set it, for the
|
|
463
|
+
// same reason — this substep is not the thing that paints. `runTicks`'s
|
|
464
|
+
// `render: 'none'|'last'` fast-forward sets it on every tick it doesn't
|
|
465
|
+
// want to paint (see `runTicks`'s doc comment on `GameInternal`), and a
|
|
466
|
+
// DISPLAY-RATE host sets it on every substep because `runRenderFrame`
|
|
467
|
+
// paints once per real frame instead. Unlike `onlyFrozen` below it is no
|
|
468
|
+
// longer internal-only: it is part of the public `GameInternal.runFrame`
|
|
469
|
+
// signature, since an external host is now one of those two callers.
|
|
470
|
+
const skipRenderPhases = frameOpts?.skipRenderPhases ?? false;
|
|
471
|
+
// `onlyFrozen` is internal-only (not part of the public `GameInternal.runFrame`
|
|
472
|
+
// signature — no external caller sets it) — `Game.play.step()` below is the
|
|
473
|
+
// one and only caller. It ticks EXACTLY the currently-frozen set (host-driven,
|
|
474
|
+
// `pausable`, and `paused`) through every phase + `endFrame`, with the REAL
|
|
475
|
+
// `dt` (not the render-phase's forced `0`), and touches no other world at all
|
|
476
|
+
// — §7.1-2 fix, probe4: the previous `ignorePause` full-frame re-run ticked
|
|
477
|
+
// EVERY host-driven world, double-ticking every already-running
|
|
478
|
+
// `pausable: false` world (D10's ordinary loop never stops ticking them).
|
|
479
|
+
const onlyFrozen = frameOpts?.onlyFrozen ?? false;
|
|
480
|
+
// Checklist item 7: snapshot the world count ONCE at entry and iterate
|
|
481
|
+
// by index in both loops below. A world registered mid-frame (e.g. from
|
|
482
|
+
// a game-scoped system's side effect) joins at the NEXT `runFrame` call,
|
|
483
|
+
// not this one — `for (const world of roots)` would otherwise pick up
|
|
484
|
+
// a world pushed during this very frame. This also drops the two
|
|
485
|
+
// per-phase `for...of` iterator allocations.
|
|
486
|
+
const n = roots.length;
|
|
487
|
+
inputFrameActive = false;
|
|
488
|
+
for (let i = 0; i < n; i++) {
|
|
489
|
+
const world = roots[i];
|
|
490
|
+
if (onlyFrozen) {
|
|
491
|
+
if (paused && world.pausable && !world.mounted.drivesOwnLoop) {
|
|
492
|
+
inputFrameActive = true;
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
else if (ignorePause ||
|
|
497
|
+
(world.mounted.drivesOwnLoop
|
|
498
|
+
? !paused || !world.pausable || !world.mounted.setPaused
|
|
499
|
+
: !paused || !world.pausable)) {
|
|
500
|
+
inputFrameActive = true;
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// §7.1-11 fix (probe5): `stateBridge.bump()` must fire iff at least one
|
|
505
|
+
// world actually advanced this call — not unconditionally. `advanced`
|
|
506
|
+
// covers rule (a) below (host-driven roots this call actually ticked);
|
|
507
|
+
// rule (b): a self-driven world that is RUNNING — which is every
|
|
508
|
+
// self-driven world while not paused, and, while paused, the ones the
|
|
509
|
+
// gate can't reach (`pausable: false`, or no `setPaused` capability) —
|
|
510
|
+
// checked once, up front, over the same `roots` array (no extra
|
|
511
|
+
// allocation, matching every other loop here). Rule (b) is skipped in
|
|
512
|
+
// `onlyFrozen` mode: a `step()` call bumps iff it ticked a frozen world
|
|
513
|
+
// — self-driven notifications belong to the loop's own `runFrame`s.
|
|
514
|
+
let advanced = false;
|
|
515
|
+
if (!onlyFrozen) {
|
|
516
|
+
for (let i = 0; i < n; i++) {
|
|
517
|
+
const world = roots[i];
|
|
518
|
+
if (!world.mounted.drivesOwnLoop)
|
|
519
|
+
continue;
|
|
520
|
+
if (!paused || !world.pausable || !world.mounted.setPaused) {
|
|
521
|
+
advanced = true;
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (onlyFrozen) {
|
|
527
|
+
// step(): the frozen set is host-driven + pausable + currently paused.
|
|
528
|
+
// While NOT paused this set is empty by construction, so this whole
|
|
529
|
+
// branch is a natural no-op — `Game.play.step()`'s "no-op while
|
|
530
|
+
// running" behavior falls straight out of this, no separate guard
|
|
531
|
+
// needed.
|
|
532
|
+
for (const phase of PHASE_ORDER) {
|
|
533
|
+
profiler.beginPhase();
|
|
534
|
+
// Game-owned systems follow the same one-run-per-phase contract as a
|
|
535
|
+
// normal frame whenever Step advances at least one frozen world.
|
|
536
|
+
// In particular, the game-owned InputManager must poll before those
|
|
537
|
+
// worlds read actions; its matching endFrame remains at the shared
|
|
538
|
+
// frame tail below.
|
|
539
|
+
if (inputFrameActive)
|
|
540
|
+
systems.runPhase(phase, dt);
|
|
541
|
+
for (let i = 0; i < n; i++) {
|
|
542
|
+
const world = roots[i];
|
|
543
|
+
if (world.mounted.drivesOwnLoop)
|
|
544
|
+
continue;
|
|
545
|
+
if (!(paused && world.pausable))
|
|
546
|
+
continue; // only the frozen set
|
|
547
|
+
try {
|
|
548
|
+
world.frame?.runPhase(phase, dt);
|
|
549
|
+
}
|
|
550
|
+
catch (err) {
|
|
551
|
+
console.error(`[game] world "${world.id}" (kind: ${world.kind}) runPhase("${phase}") threw ` +
|
|
552
|
+
'(step()):', err);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
profiler.endPhase(phase);
|
|
556
|
+
}
|
|
557
|
+
for (let i = 0; i < n; i++) {
|
|
558
|
+
const world = roots[i];
|
|
559
|
+
if (world.mounted.drivesOwnLoop)
|
|
560
|
+
continue;
|
|
561
|
+
if (!(paused && world.pausable))
|
|
562
|
+
continue;
|
|
563
|
+
advanced = true;
|
|
564
|
+
if (world.frame) {
|
|
565
|
+
try {
|
|
566
|
+
world.frame.endFrame?.();
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
console.error(`[game] world "${world.id}" (kind: ${world.kind}) endFrame() threw (step()):`, err);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
try {
|
|
574
|
+
world.mounted.update?.(dt);
|
|
575
|
+
}
|
|
576
|
+
catch (err) {
|
|
577
|
+
console.error(`[game] world "${world.id}" (kind: ${world.kind}) update() threw (step()):`, err);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
for (const phase of PHASE_ORDER) {
|
|
584
|
+
profiler.beginPhase();
|
|
585
|
+
// D15/T-D15.4: `runTicks`'s fast-forward skip — `preRender`/`render`
|
|
586
|
+
// are the ONLY phases ever skipped this way (every earlier gameplay
|
|
587
|
+
// phase, and `endFrame` below, always run) — see `skipRenderPhases`'s
|
|
588
|
+
// declaration above and `runTicks`'s doc comment on `GameInternal`.
|
|
589
|
+
const skipThisPhase = skipRenderPhases && (phase === SystemPhase.PRE_RENDER || phase === SystemPhase.RENDER);
|
|
590
|
+
if (!skipThisPhase) {
|
|
591
|
+
systems.runPhase(phase, dt);
|
|
592
|
+
for (let i = 0; i < n; i++) {
|
|
593
|
+
const world = roots[i];
|
|
594
|
+
if (world.mounted.drivesOwnLoop)
|
|
595
|
+
continue;
|
|
596
|
+
// D10/T7.6: a `pausable` world under an active (non-ignored) pause
|
|
597
|
+
// skips every phase except `render` — its render still runs, every
|
|
598
|
+
// substep, but with `dt` forced to `0` (deterministic: no
|
|
599
|
+
// time-based render effect silently keeps animating a "frozen"
|
|
600
|
+
// scene). A `pausable: false` world (or any world while
|
|
601
|
+
// `ignorePause`) is unaffected.
|
|
602
|
+
const frozen = !ignorePause && paused && world.pausable;
|
|
603
|
+
if (frozen && phase !== SystemPhase.RENDER)
|
|
604
|
+
continue;
|
|
605
|
+
const phaseDt = frozen ? 0 : dt;
|
|
606
|
+
// Checklist item 2: isolate each world's per-phase work — one
|
|
607
|
+
// world's `runPhase` throwing must not starve sibling roots still
|
|
608
|
+
// due this phase, nor abort the frame. Mirrors `runOne`'s style in
|
|
609
|
+
// `core/system-runner.ts` (loud console.error, never swallowed).
|
|
610
|
+
try {
|
|
611
|
+
world.frame?.runPhase(phase, phaseDt);
|
|
612
|
+
}
|
|
613
|
+
catch (err) {
|
|
614
|
+
console.error(`[game] world "${world.id}" (kind: ${world.kind}) runPhase("${phase}") threw:`, err);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
profiler.endPhase(phase);
|
|
619
|
+
}
|
|
620
|
+
for (let i = 0; i < n; i++) {
|
|
621
|
+
const world = roots[i];
|
|
622
|
+
if (world.mounted.drivesOwnLoop)
|
|
623
|
+
continue;
|
|
624
|
+
// A fully-frozen world gets no `endFrame`/opaque-`update` call either —
|
|
625
|
+
// there is nothing to "end the frame" of when nothing ran this substep.
|
|
626
|
+
const frozen = !ignorePause && paused && world.pausable;
|
|
627
|
+
if (frozen)
|
|
628
|
+
continue;
|
|
629
|
+
advanced = true;
|
|
630
|
+
if (world.frame) {
|
|
631
|
+
try {
|
|
632
|
+
world.frame.endFrame?.();
|
|
633
|
+
}
|
|
634
|
+
catch (err) {
|
|
635
|
+
console.error(`[game] world "${world.id}" (kind: ${world.kind}) endFrame() threw:`, err);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
// Checklist item 2: same isolation for the opaque-world fallback —
|
|
640
|
+
// one foreign mount's `update` throwing must not starve its
|
|
641
|
+
// siblings' `endFrame`/`update` calls this same loop.
|
|
642
|
+
try {
|
|
643
|
+
world.mounted.update?.(dt);
|
|
644
|
+
}
|
|
645
|
+
catch (err) {
|
|
646
|
+
console.error(`[game] world "${world.id}" (kind: ${world.kind}) update() threw:`, err);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// T7.4 slice 1: bump + notify LAST, after
|
|
652
|
+
// every phase of every world and every world's endFrame/update above —
|
|
653
|
+
// subscribers must only ever observe post-frame state. Bumped at most
|
|
654
|
+
// once per completed `runFrame`/`step()` call, never per phase/world —
|
|
655
|
+
// and, per §7.1-11's fix, only when `advanced` (see above) is true: a
|
|
656
|
+
// fully-gated paused game produces no notifications at all.
|
|
657
|
+
if (inputFrameActive)
|
|
658
|
+
input.endFrame();
|
|
659
|
+
if (advanced) {
|
|
660
|
+
stateBridge.bump();
|
|
661
|
+
tick++;
|
|
662
|
+
simT += dt;
|
|
663
|
+
// P3 — sim timers fire IMMEDIATELY after the bump, inside this same
|
|
664
|
+
// `advanced` guard: a paused/frozen world advances no sim time, so it
|
|
665
|
+
// must fire no timers either, and `Game.play.step()` (which reaches this
|
|
666
|
+
// block with `advanced` set by the frozen-set loop above) fires exactly
|
|
667
|
+
// the timers that ONE substep makes due. One flush === one completed
|
|
668
|
+
// substep, which is what lets `SimClock.tickNow()` simply count flushes
|
|
669
|
+
// and always agree with `tick`.
|
|
670
|
+
simClock.flush(simT);
|
|
671
|
+
}
|
|
672
|
+
profiler.endFrame();
|
|
673
|
+
if (rngTrapEnabled)
|
|
674
|
+
rngTrap.disable();
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* WO-P13 — one display frame's PRESENTATION pass. See
|
|
678
|
+
* `GameInternal.runRenderFrame`'s doc comment for the contract; this is the
|
|
679
|
+
* `preRender`+`render` slice of `runFrameImpl`'s phase loop, lifted out and
|
|
680
|
+
* driven by the loop's own per-real-frame callback instead of by the substep
|
|
681
|
+
* loop.
|
|
682
|
+
*
|
|
683
|
+
* Three deliberate parallels with `runFrameImpl`, so the two passes cannot
|
|
684
|
+
* drift into different rules for the same situation:
|
|
685
|
+
* - game-scoped systems run before per-world hooks, per phase;
|
|
686
|
+
* - one world's throw is logged and isolated, never allowed to starve a
|
|
687
|
+
* sibling still due this phase;
|
|
688
|
+
* - the RNG trap brackets the whole pass, so `Math.random` called from a
|
|
689
|
+
* render-phase system is still caught by D15's dev-mode trap — it was,
|
|
690
|
+
* back when this pass lived inside `runFrame`, and moving code must not
|
|
691
|
+
* quietly move it out from under a guard.
|
|
692
|
+
*
|
|
693
|
+
* Profiler note (a real, accepted consequence): the profiler's "frame" has
|
|
694
|
+
* always meant "one `runFrame` call", so a display frame now produces the
|
|
695
|
+
* substep records it always did PLUS one record for this pass, carrying the
|
|
696
|
+
* `preRender`/`render` spans and the renderer counters. Re-modelling the
|
|
697
|
+
* profiler's frame boundary around the display frame is a separate change to
|
|
698
|
+
* a debugging surface, not part of flipping the loop.
|
|
699
|
+
*/
|
|
700
|
+
function runRenderFrameImpl(alpha, displayDt) {
|
|
701
|
+
renderAlpha = alpha;
|
|
702
|
+
if (rngTrapEnabled)
|
|
703
|
+
rngTrap.enable();
|
|
704
|
+
profiler.beginFrame();
|
|
705
|
+
// `RenderStepped` first: a camera adjusted here is drawn by THIS frame's
|
|
706
|
+
// render phase, not next frame's. Iterated over a snapshot so a callback
|
|
707
|
+
// that unsubscribes itself (or registers another) cannot mutate the set
|
|
708
|
+
// mid-iteration.
|
|
709
|
+
if (renderStepCallbacks.size > 0) {
|
|
710
|
+
for (const fn of [...renderStepCallbacks]) {
|
|
711
|
+
try {
|
|
712
|
+
fn(alpha, displayDt);
|
|
713
|
+
}
|
|
714
|
+
catch (err) {
|
|
715
|
+
console.error('[game] an onRenderStep callback threw:', err);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const n = roots.length;
|
|
720
|
+
for (const phase of DISPLAY_RATE_PHASES) {
|
|
721
|
+
profiler.beginPhase();
|
|
722
|
+
systems.runPhase(phase, displayDt);
|
|
723
|
+
for (let i = 0; i < n; i++) {
|
|
724
|
+
const world = roots[i];
|
|
725
|
+
if (world.mounted.drivesOwnLoop)
|
|
726
|
+
continue;
|
|
727
|
+
// D10/T7.6, carried over verbatim in substance: a frozen world still
|
|
728
|
+
// renders (a paused viewport stays painted) but with `dt` forced to
|
|
729
|
+
// `0`, and skips every other phase.
|
|
730
|
+
const frozen = paused && world.pausable;
|
|
731
|
+
if (frozen && phase !== SystemPhase.RENDER)
|
|
732
|
+
continue;
|
|
733
|
+
try {
|
|
734
|
+
world.frame?.runPhase(phase, frozen ? 0 : displayDt);
|
|
735
|
+
}
|
|
736
|
+
catch (err) {
|
|
737
|
+
console.error(`[game] world "${world.id}" (kind: ${world.kind}) runPhase("${phase}") threw ` +
|
|
738
|
+
'(display frame):', err);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
profiler.endPhase(phase);
|
|
742
|
+
}
|
|
743
|
+
profiler.endFrame();
|
|
744
|
+
if (rngTrapEnabled)
|
|
745
|
+
rngTrap.disable();
|
|
746
|
+
}
|
|
747
|
+
const gameInternal = {
|
|
748
|
+
loop: opts.loop,
|
|
749
|
+
assets: opts.assets,
|
|
750
|
+
playtest: opts.playtest ?? null,
|
|
751
|
+
profiler,
|
|
752
|
+
systems,
|
|
753
|
+
get roots() {
|
|
754
|
+
return roots;
|
|
755
|
+
},
|
|
756
|
+
world(id) {
|
|
757
|
+
return roots.find((w) => w.id === id) ?? null;
|
|
758
|
+
},
|
|
759
|
+
get defaultRoot() {
|
|
760
|
+
return requireDefaultRoot();
|
|
761
|
+
},
|
|
762
|
+
get systemAdapters() {
|
|
763
|
+
return computeSystemAdapters();
|
|
764
|
+
},
|
|
765
|
+
subscribeSystemAdapters(listener) {
|
|
766
|
+
systemAdapterListeners.add(listener);
|
|
767
|
+
return () => systemAdapterListeners.delete(listener);
|
|
768
|
+
},
|
|
769
|
+
notifySystemAdaptersChanged() {
|
|
770
|
+
for (const listener of systemAdapterListeners)
|
|
771
|
+
listener();
|
|
772
|
+
},
|
|
773
|
+
input,
|
|
774
|
+
get audio() {
|
|
775
|
+
return requireFirstPartyCtx('audio').audio;
|
|
776
|
+
},
|
|
777
|
+
state: stateBridge,
|
|
778
|
+
play: {
|
|
779
|
+
get paused() {
|
|
780
|
+
return paused;
|
|
781
|
+
},
|
|
782
|
+
pause() {
|
|
783
|
+
if (paused)
|
|
784
|
+
return;
|
|
785
|
+
paused = true;
|
|
786
|
+
setRootGates(true);
|
|
787
|
+
},
|
|
788
|
+
resume() {
|
|
789
|
+
if (!paused)
|
|
790
|
+
return;
|
|
791
|
+
paused = false;
|
|
792
|
+
setRootGates(false);
|
|
793
|
+
},
|
|
794
|
+
step(dt = 1 / 60) {
|
|
795
|
+
// Self-driven pausable roots advance via their adapter's `step()`
|
|
796
|
+
// capability — but only the ones that are actually FROZEN: the game
|
|
797
|
+
// must be paused, and the world must have been gate-able in the
|
|
798
|
+
// first place (`setPaused` present — a world the gate couldn't reach
|
|
799
|
+
// never stopped, so "stepping" it would double-tick a still-running
|
|
800
|
+
// loop, the same §7.1-2 class as the host-driven fix below). While
|
|
801
|
+
// not paused, nothing here runs — step() is a whole-call no-op.
|
|
802
|
+
if (paused) {
|
|
803
|
+
for (const world of roots) {
|
|
804
|
+
if (!world.pausable || !world.mounted.drivesOwnLoop)
|
|
805
|
+
continue;
|
|
806
|
+
if (!world.mounted.setPaused)
|
|
807
|
+
continue; // never gated — still running
|
|
808
|
+
if (world.mounted.step) {
|
|
809
|
+
world.mounted.step();
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
reportGateShortfallOnce('loop', world.id, 'self-driven world, adapter declares no step capability');
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
// §7.1-2 fix (probe4): tick EXACTLY the frozen (host-driven, pausable,
|
|
817
|
+
// paused) set — not `{ ignorePause: true }`, which re-ran a FULL
|
|
818
|
+
// extra frame for every host-driven world (including already-running
|
|
819
|
+
// `pausable: false` ones — a double-tick, since D10's ordinary loop
|
|
820
|
+
// never stops ticking them). See `runFrameImpl`'s `onlyFrozen` mode.
|
|
821
|
+
runFrameImpl(dt, { onlyFrozen: true });
|
|
822
|
+
},
|
|
823
|
+
},
|
|
824
|
+
registerRoot(world) {
|
|
825
|
+
if (roots.some((w) => w.id === world.id)) {
|
|
826
|
+
throw new Error(`Game.registerRoot: duplicate world id "${world.id}"`);
|
|
827
|
+
}
|
|
828
|
+
// Checklist item 6: the SAME `mounted` object registered under two
|
|
829
|
+
// world ids would be double-ticked by `runFrame` (its `frame.runPhase`/
|
|
830
|
+
// `endFrame` called once per registration) and double-dispose-wrapped
|
|
831
|
+
// (`createRootInstance` wraps `mounted.dispose` in place — a second
|
|
832
|
+
// wrap would flip `disposed` and call through on ITS OWN wrapped
|
|
833
|
+
// `originalDispose`, which is harmless today only by accident of
|
|
834
|
+
// `SetupThreeRootAdapter.dispose` being idempotent; a foreign adapter
|
|
835
|
+
// has no such guarantee). Reject it outright instead.
|
|
836
|
+
if (roots.some((w) => w.mounted === world.mounted)) {
|
|
837
|
+
throw new Error(`Game.registerRoot: world "${world.id}" shares its \`mounted\` object with an ` +
|
|
838
|
+
`already-registered world ("${roots.find((w) => w.mounted === world.mounted).id}") — ` +
|
|
839
|
+
'the same mount cannot be registered twice.');
|
|
840
|
+
}
|
|
841
|
+
roots.push(world);
|
|
842
|
+
gameInternal.notifySystemAdaptersChanged();
|
|
843
|
+
// Notify state-bridge subscribers THE INSTANT the world list changes —
|
|
844
|
+
// not just at the next completed `runFrame`. Root-cause fix for the
|
|
845
|
+
// "roots: " (empty) hang in `36-r3f-first-party.spec.ts` on CI (gate
|
|
846
|
+
// 4): `mountAllRootSpecs` (`create-runtime.ts`) mounts roots
|
|
847
|
+
// SEQUENTIALLY, and a react world's `adapter.mount()`
|
|
848
|
+
// (`resolveDefaultReactAdapter`/`mountOneReactRoot`) renders its tree —
|
|
849
|
+
// synchronously in some React builds, but React 19's concurrent
|
|
850
|
+
// renderer does NOT guarantee a synchronous first commit (see
|
|
851
|
+
// `r3f-adapter.tsx`'s own doc comment on `onCreated`) — BEFORE the
|
|
852
|
+
// caller calls `registerRoot` for that very world. A `useWorldState`
|
|
853
|
+
// selector reading `g.roots` can therefore render for the first time
|
|
854
|
+
// while `roots` is still missing entries that register moments later.
|
|
855
|
+
// Previously the ONLY way such a subscriber ever saw the corrected
|
|
856
|
+
// value was the state bridge's next `bump()`, which fires exclusively
|
|
857
|
+
// from `GameInternal.runFrame`'s tail — i.e. only once the host's loop
|
|
858
|
+
// has actually ticked (`started = true; loop.start()` in
|
|
859
|
+
// `create-runtime.ts`, itself gated on EVERY world finishing its
|
|
860
|
+
// mount). On a slow/contended host (CI's 4-core SwiftShader runners)
|
|
861
|
+
// that first tick can be delayed well past a test's assertion window,
|
|
862
|
+
// or — if the tab is ever backgrounded — not fire at all for a long
|
|
863
|
+
// stretch; the subscriber's cached snapshot then sits on its stale
|
|
864
|
+
// (possibly fully empty) first render for that whole time, matching the
|
|
865
|
+
// observed CI symptom exactly ("the HUD element IS mounted; the roots
|
|
866
|
+
// list is EMPTY"). Proven red-then-green by
|
|
867
|
+
// `packages/engine/test/state-bridge.test.ts`'s "registerRoot notifies
|
|
868
|
+
// subscribers immediately" case: a subscriber registered before a
|
|
869
|
+
// world, with `runFrame` NEVER called, only saw the update after this
|
|
870
|
+
// fix. `bump()` also advances `frameVersion` (the only invalidation key
|
|
871
|
+
// `createFrameSelectorCache`/`useWorldState` understand — see
|
|
872
|
+
// `frame-selector-cache.ts`), so this doubles as "frameVersion is
|
|
873
|
+
// bumped once per completed runFrame OR once per world registered",
|
|
874
|
+
// documented on `GameStateBridge.frameVersion` below.
|
|
875
|
+
stateBridge.bump();
|
|
876
|
+
// T7.4 slice 2: a NON-first-party mount
|
|
877
|
+
// (an ingested/foreign world — first-party mounts are exempt, they're
|
|
878
|
+
// observed via `Game.state`/`useWorldState` instead) with no `observe`
|
|
879
|
+
// has no state bridge at all — react HUDs cannot subscribe to it, and
|
|
880
|
+
// silently returning `undefined` forever would hide that. Report ONCE
|
|
881
|
+
// per mount, at registration time, matching this file's existing
|
|
882
|
+
// `[game] world "<id>" (kind: <kind>) ...` console idiom (see
|
|
883
|
+
// `runFrame` below).
|
|
884
|
+
//
|
|
885
|
+
// §7.1-15: a `kind: 'dom'` world is ALSO exempt — it has no `observe`
|
|
886
|
+
// BY DESIGN (D8: a react world's own mounted tree
|
|
887
|
+
// reads state via `Game.state`/`useWorldState`, the SAME first-party
|
|
888
|
+
// bridge a three/canvas world's HUD uses, never `RootStateObserver`
|
|
889
|
+
// — that hook is scoped to the ingested/foreign-world case). Without
|
|
890
|
+
// this exemption every production react world logged a false-positive
|
|
891
|
+
// "no state bridge" warning at registration (probe: every real react
|
|
892
|
+
// world mount), eroding the signal for a genuinely un-observable
|
|
893
|
+
// ingested world.
|
|
894
|
+
// A HOST-DRIVEN mount (`drivesOwnLoop: false`, notably the R3F adapter)
|
|
895
|
+
// is observable through `Game.state` even though it does not carry the
|
|
896
|
+
// classic scene adapter's `firstParty` brand: it ticks inside
|
|
897
|
+
// `runFrame`, so the frame-versioned bridge covers it. Requiring a
|
|
898
|
+
// foreign-world `observe` bridge there produces a false warning while
|
|
899
|
+
// the canonical bridge is already live.
|
|
900
|
+
if (!isFirstPartyMounted(world.mounted) &&
|
|
901
|
+
world.mounted.drivesOwnLoop &&
|
|
902
|
+
world.kind !== 'dom' &&
|
|
903
|
+
!world.mounted.observe) {
|
|
904
|
+
console.warn(`[game] world "${world.id}" (kind: ${world.kind}, adapter: "${world.adapter.id}"): ` +
|
|
905
|
+
'no state bridge — this mounted game has no `observe` (RootStateObserver); ' +
|
|
906
|
+
'react HUDs/useRootObservation cannot subscribe to its state.');
|
|
907
|
+
}
|
|
908
|
+
},
|
|
909
|
+
runFrame(dt, frameOpts) {
|
|
910
|
+
runFrameImpl(dt, frameOpts);
|
|
911
|
+
},
|
|
912
|
+
runRenderFrame(alpha, displayDt) {
|
|
913
|
+
runRenderFrameImpl(alpha, displayDt);
|
|
914
|
+
},
|
|
915
|
+
get renderAlpha() {
|
|
916
|
+
return renderAlpha;
|
|
917
|
+
},
|
|
918
|
+
onRenderStep(fn, stepOpts) {
|
|
919
|
+
const signal = stepOpts?.signal;
|
|
920
|
+
// Already-aborted is a no-op registration, never a throw — the same
|
|
921
|
+
// `AbortSignal` contract `SimClock.after` honors.
|
|
922
|
+
if (signal?.aborted)
|
|
923
|
+
return () => { };
|
|
924
|
+
renderStepCallbacks.add(fn);
|
|
925
|
+
let detachAbort = null;
|
|
926
|
+
const unsubscribe = () => {
|
|
927
|
+
renderStepCallbacks.delete(fn);
|
|
928
|
+
detachAbort?.();
|
|
929
|
+
detachAbort = null;
|
|
930
|
+
};
|
|
931
|
+
if (signal) {
|
|
932
|
+
signal.addEventListener('abort', unsubscribe, { once: true });
|
|
933
|
+
detachAbort = () => signal.removeEventListener('abort', unsubscribe);
|
|
934
|
+
}
|
|
935
|
+
return unsubscribe;
|
|
936
|
+
},
|
|
937
|
+
runTicks(n, ticksOpts) {
|
|
938
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
939
|
+
throw new RangeError(`Game.runTicks: n must be a non-negative integer, got ${n}`);
|
|
940
|
+
}
|
|
941
|
+
// Refuse while paused (D15 §2.b): stepping the frozen set is
|
|
942
|
+
// `Game.play.step()`'s contract, not this one's — see `runTicks`'s doc
|
|
943
|
+
// comment on `GameInternal` above.
|
|
944
|
+
if (paused) {
|
|
945
|
+
throw new DebugError('RUN_TICKS_PAUSED', 'Game.runTicks: refused — Game.play.paused is true; stepping the frozen set is ' +
|
|
946
|
+
"Game.play.step()'s contract, not runTicks'");
|
|
947
|
+
}
|
|
948
|
+
const render = ticksOpts?.render ?? 'last';
|
|
949
|
+
const fixedDt = opts.loop.fixedDt;
|
|
950
|
+
for (let i = 0; i < n; i++) {
|
|
951
|
+
const isFinalTick = i === n - 1;
|
|
952
|
+
const skipRenderPhases = render === 'all' ? false : render === 'none' ? true : !isFinalTick;
|
|
953
|
+
runFrameImpl(fixedDt, { skipRenderPhases });
|
|
954
|
+
}
|
|
955
|
+
},
|
|
956
|
+
loadInputMap(path) {
|
|
957
|
+
if (inputMapPath && inputMapPath !== path) {
|
|
958
|
+
throw new Error(`Game.loadInputMap: input map is already "${inputMapPath}"; root requested "${path}". ` +
|
|
959
|
+
'Input is game-owned, so roots cannot load competing maps.');
|
|
960
|
+
}
|
|
961
|
+
inputMapPath = path;
|
|
962
|
+
inputMapLoad ??= input.loadMap(path);
|
|
963
|
+
return inputMapLoad;
|
|
964
|
+
},
|
|
965
|
+
dispose() {
|
|
966
|
+
// P3 — the sim clock is GAME-scoped, so this is the only correct place to
|
|
967
|
+
// dispose it: `create-runtime.ts`'s `fullCleanup` calls us after EVERY
|
|
968
|
+
// root's `mounted.dispose()`, whereas a per-root teardown may be ending
|
|
969
|
+
// just one sub-session while sibling roots keep running (see the
|
|
970
|
+
// disposed-world guard in `runFrame`). It used to be disposed from
|
|
971
|
+
// `setup-three-root-adapter.ts`'s teardown, which meant disposing one of
|
|
972
|
+
// two three roots froze `now()` for the whole Game, rejected the other
|
|
973
|
+
// world's pending `delay`s and turned its `after()` calls into silent
|
|
974
|
+
// no-ops. Note the asymmetry that gives the bug away: `seededRandom` and
|
|
975
|
+
// `debugRegistry` are game-scoped too, and per-root teardown has never
|
|
976
|
+
// destroyed either — only ever `strip(this.id)`, its own slice.
|
|
977
|
+
simClock.dispose();
|
|
978
|
+
input.dispose();
|
|
979
|
+
debugRegistry.strip();
|
|
980
|
+
},
|
|
981
|
+
};
|
|
982
|
+
// Filed AFTER the shell exists (the WeakMap keys on the Game object
|
|
983
|
+
// itself) so `getDebugRegistry(game)` — the react hooks' and any later
|
|
984
|
+
// consumer's reach-in — works from the moment `createGame` returns.
|
|
985
|
+
registerDebugRegistry(gameInternal, debugRegistry);
|
|
986
|
+
// D15/T-D15.4: wire the run-ticks target the instant the Game shell exists
|
|
987
|
+
// (unlike `setVirtualInputTarget`, which waits for a per-world mount, a
|
|
988
|
+
// Game's own `runTicks` needs nothing else) — this is what makes
|
|
989
|
+
// `window.__vgai.runTicks` (`debug-bridge.ts`) and the editor relay's
|
|
990
|
+
// `run-ticks` case reach the SAME implementation `game.runTicks` above is.
|
|
991
|
+
debugRegistry.setRunTicksTarget({ runTicks: gameInternal.runTicks });
|
|
992
|
+
// D15 (T-D15.1/.3) — same "file after the shell exists" ordering as the
|
|
993
|
+
// debug registry above: `getSeededRandom(game)`/`getGameplayRngTrapControl
|
|
994
|
+
// (game)` (per-world `ctx.random` wiring, and the manifest-aware boot
|
|
995
|
+
// path's post-hoc `setEnabled` call) both work from the moment
|
|
996
|
+
// `createGame` returns.
|
|
997
|
+
registerSeededRandom(gameInternal, seededRandom);
|
|
998
|
+
registerGameplayRngTrapControl(gameInternal, rngTrapControl);
|
|
999
|
+
// P3 — same "file after the shell exists" ordering: `getSimClock(game)` is
|
|
1000
|
+
// how every world's mount resolves `ctx.clock` to THIS game's one clock.
|
|
1001
|
+
registerSimClock(gameInternal, simClock);
|
|
1002
|
+
return gameInternal;
|
|
1003
|
+
}
|