@vgai/engine 0.5.21 → 0.5.23
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 +6 -3
- package/dist/adapter/adapter-module.d.ts.map +1 -1
- package/dist/adapter/adapter-module.js +2 -4
- package/dist/adapter/adapter-surface.d.ts +1 -1
- package/dist/adapter/adapter-surface.js +1 -1
- package/dist/adapter/authoring.d.ts +19 -21
- package/dist/adapter/authoring.d.ts.map +1 -1
- package/dist/adapter/authoring.js +2 -2
- package/dist/adapter/binding.d.ts +341 -0
- package/dist/adapter/binding.d.ts.map +1 -0
- package/dist/adapter/binding.js +148 -0
- package/dist/adapter/colyseus-networking-adapter.d.ts.map +1 -1
- package/dist/adapter/colyseus-networking-adapter.js +23 -4
- package/dist/adapter/entrypoint-selection-readers.d.ts +24 -0
- package/dist/adapter/entrypoint-selection-readers.d.ts.map +1 -0
- package/dist/adapter/entrypoint-selection-readers.js +63 -0
- package/dist/adapter/entrypoint-selection-source.d.ts.map +1 -1
- package/dist/adapter/entrypoint-selection-source.js +1 -44
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -1
- package/dist/adapter/finders/scenes-from-entrypoint-selection.js +1 -52
- package/dist/adapter/host-context.d.ts +46 -15
- package/dist/adapter/host-context.d.ts.map +1 -1
- package/dist/adapter/host-context.js +2 -9
- package/dist/adapter/index.d.ts +11 -12
- package/dist/adapter/index.d.ts.map +1 -1
- package/dist/adapter/index.js +9 -10
- package/dist/adapter/ingest/game-contract.d.ts +0 -3
- package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
- package/dist/adapter/ingest/game-contract.js +0 -3
- package/dist/adapter/ingest/scene-capture.d.ts +19 -23
- package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
- package/dist/adapter/ingest/scene-capture.js +143 -17
- package/dist/adapter/ingest/visible-capture-window.d.ts +34 -0
- package/dist/adapter/ingest/visible-capture-window.d.ts.map +1 -1
- package/dist/adapter/manifest-interpreter.d.ts +59 -0
- package/dist/adapter/manifest-interpreter.d.ts.map +1 -0
- package/dist/adapter/manifest-interpreter.js +50 -0
- package/dist/adapter/native-debug-module.d.ts +71 -3
- package/dist/adapter/native-debug-module.d.ts.map +1 -1
- package/dist/adapter/native-debug-module.js +146 -10
- package/dist/adapter/rapier-physics-adapter.d.ts +31 -1
- package/dist/adapter/rapier-physics-adapter.d.ts.map +1 -1
- package/dist/adapter/rapier-physics-adapter.js +50 -36
- package/dist/adapter/renderer-config.d.ts +3 -3
- package/dist/adapter/renderer-config.js +3 -3
- package/dist/adapter/root-adapter.d.ts +7 -7
- package/dist/adapter/root-adapter.js +4 -4
- package/dist/adapter/system-adapter.d.ts +45 -7
- package/dist/adapter/system-adapter.d.ts.map +1 -1
- package/dist/adapter/system-seam-contract.d.ts +3 -3
- package/dist/adapter/system-seam-contract.js +3 -3
- package/dist/adapter/system-slot.d.ts +168 -0
- package/dist/adapter/system-slot.d.ts.map +1 -0
- package/dist/adapter/system-slot.js +229 -0
- package/dist/adapter/transform.d.ts +11 -1
- package/dist/adapter/transform.d.ts.map +1 -1
- package/dist/ai/navigation.d.ts +17 -1
- package/dist/ai/navigation.d.ts.map +1 -1
- package/dist/ai/navigation.js +26 -5
- package/dist/animation/anim-graph-types.d.ts +11 -3
- package/dist/animation/anim-graph-types.d.ts.map +1 -1
- package/dist/animation/theatre-clock-binding.d.ts +4 -4
- package/dist/animation/theatre-clock-binding.js +4 -4
- package/dist/animation/xstate-animation-binding.d.ts.map +1 -1
- package/dist/animation/xstate-animation-binding.js +2 -2
- package/dist/animation/xstate-animation-meta.d.ts +13 -0
- package/dist/animation/xstate-animation-meta.d.ts.map +1 -1
- package/dist/animation/xstate-animation-meta.js +14 -0
- package/dist/asset-formats/index.d.ts +4 -7
- package/dist/asset-formats/index.d.ts.map +1 -1
- package/dist/asset-formats/index.js +4 -7
- package/dist/asset-formats/render-env.d.ts +5 -8
- package/dist/asset-formats/render-env.d.ts.map +1 -1
- package/dist/asset-formats/render-env.js +3 -6
- package/dist/asset-parse-error.d.ts +2 -5
- package/dist/asset-parse-error.d.ts.map +1 -1
- package/dist/asset-parse-error.js +2 -5
- package/dist/audio/bus-mixer.d.ts +101 -0
- package/dist/audio/bus-mixer.d.ts.map +1 -0
- package/dist/audio/bus-mixer.js +115 -0
- package/dist/canvas-react/index.d.ts +5 -12
- package/dist/canvas-react/index.d.ts.map +1 -1
- package/dist/canvas-react/index.js +5 -12
- package/dist/canvas-react/pixi-react-root-factory.d.ts +34 -21
- package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -1
- package/dist/canvas-react/pixi-react-root-factory.js +274 -36
- package/dist/core/countdown-timer.d.ts +118 -0
- package/dist/core/countdown-timer.d.ts.map +1 -0
- package/dist/core/countdown-timer.js +137 -0
- package/dist/core/deferred-commands.d.ts +127 -0
- package/dist/core/deferred-commands.d.ts.map +1 -0
- package/dist/core/deferred-commands.js +132 -0
- package/dist/core/frame-pacing.d.ts +1 -1
- package/dist/core/frame-pacing.js +2 -2
- package/dist/core/game-loop.d.ts +1 -3
- package/dist/core/game-loop.d.ts.map +1 -1
- package/dist/core/game-loop.js +1 -3
- package/dist/core/seeded-random.d.ts +1 -1
- package/dist/core/sim-clock.d.ts +19 -8
- package/dist/core/sim-clock.d.ts.map +1 -1
- package/dist/core/sim-clock.js +18 -7
- package/dist/core/types.d.ts +1 -3
- package/dist/core/types.d.ts.map +1 -1
- package/dist/defaults.d.ts +6 -14
- package/dist/defaults.d.ts.map +1 -1
- package/dist/defaults.js +6 -14
- package/dist/dev/instruments.d.ts +38 -11
- package/dist/dev/instruments.d.ts.map +1 -1
- package/dist/dev/instruments.js +81 -11
- package/dist/dev/register-render-vitals.d.ts +1 -4
- package/dist/dev/register-render-vitals.d.ts.map +1 -1
- package/dist/dev/register-render-vitals.js +1 -4
- package/dist/dev/render-debug-adapter.d.ts +2 -3
- package/dist/dev/render-debug-adapter.d.ts.map +1 -1
- package/dist/dev/render-debug-adapter.js +2 -3
- package/dist/dev/render-vitals.d.ts +3 -3
- package/dist/dev/render-vitals.js +3 -3
- package/dist/dev/webgl-frame-capture.d.ts +1 -1
- package/dist/dev/webgl-frame-capture.js +1 -1
- package/dist/index.d.ts +2 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -4
- package/dist/input/host-pointer.d.ts +122 -0
- package/dist/input/host-pointer.d.ts.map +1 -0
- package/dist/input/host-pointer.js +160 -0
- package/dist/input/input-manager.d.ts +33 -5
- package/dist/input/input-manager.d.ts.map +1 -1
- package/dist/input/input-manager.js +48 -13
- package/dist/input/rebind-controller.d.ts.map +1 -1
- package/dist/input/rebind-controller.js +6 -8
- package/dist/input/schema.d.ts.map +1 -1
- package/dist/input/schema.js +56 -28
- package/dist/manifest/filename.d.ts.map +1 -1
- package/dist/manifest/filename.js +8 -12
- package/dist/manifest/load.d.ts +10 -10
- package/dist/manifest/load.d.ts.map +1 -1
- package/dist/manifest/load.js +9 -6
- package/dist/manifest/schema.d.ts +17 -21
- package/dist/manifest/schema.d.ts.map +1 -1
- package/dist/manifest/schema.js +45 -35
- package/dist/physics/collider-dimensions.d.ts +8 -12
- package/dist/physics/collider-dimensions.d.ts.map +1 -1
- package/dist/physics/collider-dimensions.js +8 -12
- package/dist/pixi/scene-capture.d.ts +11 -13
- package/dist/pixi/scene-capture.d.ts.map +1 -1
- package/dist/react/world-state.d.ts +1 -1
- package/dist/react/world-state.d.ts.map +1 -1
- package/dist/render/directional-shadow-fit.d.ts +88 -0
- package/dist/render/directional-shadow-fit.d.ts.map +1 -0
- package/dist/render/directional-shadow-fit.js +133 -0
- package/dist/render/environment-capture.d.ts +68 -0
- package/dist/render/environment-capture.d.ts.map +1 -0
- package/dist/render/environment-capture.js +90 -0
- package/dist/render/ibl-override-material.d.ts +97 -0
- package/dist/render/ibl-override-material.d.ts.map +1 -0
- package/dist/render/ibl-override-material.js +114 -0
- package/dist/render/light-camera-factory.d.ts +5 -7
- package/dist/render/light-camera-factory.d.ts.map +1 -1
- package/dist/render/light-camera-factory.js +8 -11
- package/dist/render/material-factory.js +1 -1
- package/dist/render/render-batch-system.d.ts.map +1 -1
- package/dist/render/render-features.d.ts +17 -6
- package/dist/render/render-features.d.ts.map +1 -1
- package/dist/render/render-features.js +0 -28
- package/dist/runtime/create-runtime.d.ts +34 -25
- package/dist/runtime/create-runtime.d.ts.map +1 -1
- package/dist/runtime/create-runtime.js +218 -93
- package/dist/runtime/debug-registry.d.ts +77 -8
- package/dist/runtime/debug-registry.d.ts.map +1 -1
- package/dist/runtime/debug-registry.js +3 -3
- package/dist/runtime/dev-build.d.ts +2 -2
- package/dist/runtime/dev-build.js +2 -2
- package/dist/runtime/game-input-seams.d.ts +51 -0
- package/dist/runtime/game-input-seams.d.ts.map +1 -0
- package/dist/runtime/game-input-seams.js +86 -0
- package/dist/runtime/game.d.ts +104 -40
- package/dist/runtime/game.d.ts.map +1 -1
- package/dist/runtime/game.js +119 -61
- package/dist/runtime/mount-game.d.ts +0 -16
- package/dist/runtime/mount-game.d.ts.map +1 -1
- package/dist/runtime/mount-game.js +26 -41
- package/dist/runtime/mount-manifest.d.ts +22 -14
- package/dist/runtime/mount-manifest.d.ts.map +1 -1
- package/dist/runtime/mount-manifest.js +22 -20
- package/dist/runtime/playtest.d.ts +22 -0
- package/dist/runtime/playtest.d.ts.map +1 -0
- package/dist/runtime/playtest.js +12 -0
- package/dist/runtime/state-bridge.d.ts +2 -3
- package/dist/runtime/state-bridge.d.ts.map +1 -1
- package/dist/runtime/state-bridge.js +1 -0
- package/dist/setup/setup-audio.d.ts +12 -0
- package/dist/setup/setup-audio.d.ts.map +1 -1
- package/dist/setup/setup-audio.js +9 -10
- package/dist/setup/setup-renderer.d.ts +2 -25
- package/dist/setup/setup-renderer.d.ts.map +1 -1
- package/dist/setup/setup-renderer.js +2 -31
- package/dist/world3d-react/index.d.ts +15 -25
- package/dist/world3d-react/index.d.ts.map +1 -1
- package/dist/world3d-react/index.js +15 -25
- package/dist/world3d-react/r3f-root-factory.d.ts +28 -34
- package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
- package/dist/world3d-react/r3f-root-factory.js +403 -57
- package/dist/world3d-react/rapier-physics-bridge.d.ts +30 -17
- package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
- package/dist/world3d-react/rapier-physics-bridge.js +143 -58
- package/package.json +1 -1
- package/schemas/engine-capabilities.json +16 -14
- package/schemas/vgai-project.schema.json +35 -26
- package/src/adapter/adapter-module.ts +2 -4
- package/src/adapter/adapter-surface.ts +1 -1
- package/src/adapter/authoring.ts +19 -21
- package/src/adapter/binding.ts +468 -0
- package/src/adapter/colyseus-networking-adapter.ts +20 -5
- package/src/adapter/entrypoint-selection-readers.ts +66 -0
- package/src/adapter/entrypoint-selection-source.ts +1 -40
- package/src/adapter/finders/scenes-from-entrypoint-selection.ts +1 -48
- package/src/adapter/host-context.ts +45 -15
- package/src/adapter/index.ts +23 -11
- package/src/adapter/ingest/game-contract.ts +0 -3
- package/src/adapter/ingest/scene-capture.ts +190 -39
- package/src/adapter/ingest/visible-capture-window.ts +35 -0
- package/src/adapter/manifest-interpreter.ts +64 -0
- package/src/adapter/native-debug-module.ts +248 -15
- package/src/adapter/rapier-physics-adapter.ts +76 -20
- package/src/adapter/renderer-config.ts +3 -3
- package/src/adapter/root-adapter.ts +7 -7
- package/src/adapter/system-adapter.ts +43 -8
- package/src/adapter/system-seam-contract.ts +3 -3
- package/src/adapter/system-slot.ts +291 -0
- package/src/adapter/transform.ts +18 -1
- package/src/ai/navigation.ts +30 -3
- package/src/animation/anim-graph-types.ts +11 -3
- package/src/animation/theatre-clock-binding.ts +4 -4
- package/src/animation/xstate-animation-binding.ts +2 -2
- package/src/animation/xstate-animation-meta.ts +16 -0
- package/src/asset-formats/index.ts +4 -7
- package/src/asset-formats/render-env.ts +3 -6
- package/src/asset-parse-error.ts +2 -5
- package/src/audio/bus-mixer.ts +161 -0
- package/src/canvas-react/index.ts +5 -21
- package/src/canvas-react/pixi-react-root-factory.tsx +301 -39
- package/src/core/countdown-timer.ts +188 -0
- package/src/core/deferred-commands.ts +174 -0
- package/src/core/frame-pacing.ts +2 -2
- package/src/core/game-loop.ts +1 -3
- package/src/core/seeded-random.ts +1 -1
- package/src/core/sim-clock.ts +19 -8
- package/src/core/types.ts +1 -3
- package/src/defaults.ts +6 -14
- package/src/dev/instruments.ts +108 -11
- package/src/dev/register-render-vitals.ts +1 -4
- package/src/dev/render-debug-adapter.ts +2 -3
- package/src/dev/render-vitals.ts +3 -3
- package/src/dev/webgl-frame-capture.ts +1 -1
- package/src/index.ts +2 -5
- package/src/input/host-pointer.ts +230 -0
- package/src/input/input-manager.ts +49 -13
- package/src/input/input-types.ts +1 -1
- package/src/input/rebind-controller.ts +6 -8
- package/src/input/schema.ts +216 -188
- package/src/manifest/filename.ts +8 -12
- package/src/manifest/load.ts +17 -13
- package/src/manifest/schema.ts +55 -47
- package/src/physics/collider-dimensions.ts +8 -12
- package/src/pixi/scene-capture.ts +12 -15
- package/src/react/world-state.tsx +1 -1
- package/src/render/directional-shadow-fit.ts +156 -0
- package/src/render/environment-capture.ts +102 -0
- package/src/render/ibl-override-material.ts +170 -0
- package/src/render/light-camera-factory.ts +8 -11
- package/src/render/material-factory.ts +1 -1
- package/src/render/render-batch-system.ts +14 -6
- package/src/render/render-features.ts +17 -35
- package/src/runtime/create-runtime.ts +270 -108
- package/src/runtime/debug-registry.ts +84 -8
- package/src/runtime/dev-build.ts +2 -2
- package/src/runtime/game-input-seams.ts +108 -0
- package/src/runtime/game.ts +231 -85
- package/src/runtime/mount-game.ts +31 -57
- package/src/runtime/mount-manifest.ts +47 -36
- package/src/runtime/playtest.ts +22 -0
- package/src/runtime/state-bridge.ts +3 -3
- package/src/setup/setup-audio.ts +21 -11
- package/src/setup/setup-renderer.ts +2 -61
- package/src/world3d-react/index.ts +15 -31
- package/src/world3d-react/r3f-root-factory.tsx +451 -60
- package/src/world3d-react/rapier-physics-bridge.tsx +154 -58
- package/dist/adapter/setup-three-root-adapter.d.ts +0 -86
- package/dist/adapter/setup-three-root-adapter.d.ts.map +0 -1
- package/dist/adapter/setup-three-root-adapter.js +0 -908
- package/dist/animation/clip-map.d.ts +0 -12
- package/dist/animation/clip-map.d.ts.map +0 -1
- package/dist/animation/clip-map.js +0 -31
- package/dist/asset-registry.d.ts +0 -38
- package/dist/asset-registry.d.ts.map +0 -1
- package/dist/asset-registry.js +0 -66
- package/dist/canvas-react/engine-bridge.d.ts +0 -45
- package/dist/canvas-react/engine-bridge.d.ts.map +0 -1
- package/dist/canvas-react/engine-bridge.js +0 -45
- package/dist/canvas-react/pixi-react-adapter.d.ts +0 -77
- package/dist/canvas-react/pixi-react-adapter.d.ts.map +0 -1
- package/dist/canvas-react/pixi-react-adapter.js +0 -294
- package/dist/canvas-react/world-context.d.ts +0 -98
- package/dist/canvas-react/world-context.d.ts.map +0 -1
- package/dist/canvas-react/world-context.js +0 -173
- package/dist/dev/debug-draw.d.ts +0 -24
- package/dist/dev/debug-draw.d.ts.map +0 -1
- package/dist/dev/debug-draw.js +0 -73
- package/dist/render/auto-batcher.d.ts +0 -34
- package/dist/render/auto-batcher.d.ts.map +0 -1
- package/dist/render/auto-batcher.js +0 -140
- package/dist/render/lod.d.ts +0 -13
- package/dist/render/lod.d.ts.map +0 -1
- package/dist/render/lod.js +0 -16
- package/dist/runtime/types.d.ts +0 -303
- package/dist/runtime/types.d.ts.map +0 -1
- package/dist/runtime/types.js +0 -1
- package/dist/world3d-react/engine-bridge.d.ts +0 -47
- package/dist/world3d-react/engine-bridge.d.ts.map +0 -1
- package/dist/world3d-react/engine-bridge.js +0 -73
- package/dist/world3d-react/r3f-adapter.d.ts +0 -58
- package/dist/world3d-react/r3f-adapter.d.ts.map +0 -1
- package/dist/world3d-react/r3f-adapter.js +0 -634
- package/dist/world3d-react/world-context.d.ts +0 -182
- package/dist/world3d-react/world-context.d.ts.map +0 -1
- package/dist/world3d-react/world-context.js +0 -235
- package/src/adapter/setup-three-root-adapter.ts +0 -1032
- package/src/animation/clip-map.ts +0 -34
- package/src/asset-registry.ts +0 -89
- package/src/canvas-react/engine-bridge.ts +0 -59
- package/src/canvas-react/pixi-react-adapter.tsx +0 -356
- package/src/canvas-react/world-context.ts +0 -253
- package/src/dev/debug-draw.ts +0 -80
- package/src/render/auto-batcher.ts +0 -168
- package/src/render/lod.ts +0 -17
- package/src/runtime/types.ts +0 -328
- package/src/world3d-react/engine-bridge.ts +0 -86
- package/src/world3d-react/r3f-adapter.tsx +0 -717
- package/src/world3d-react/world-context.ts +0 -358
|
@@ -1,908 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SetupThreeRootAdapter — the first-party implementer of {@link RootAdapter}
|
|
3
|
-
* that mounts ONE three root from an imperative `setup(ctx)` function.
|
|
4
|
-
*
|
|
5
|
-
* This is where Rapier lives now: the host no longer knows about it.
|
|
6
|
-
* `mount(host)` builds the full first-party runtime (the body
|
|
7
|
-
* that used to live inline in `create-runtime.ts`) from a neutral
|
|
8
|
-
* {@link ThreeHostContext}, runs the game's `setup`, and returns a
|
|
9
|
-
* {@link MountedThreeRoot}. First-party content is now just one implementer of
|
|
10
|
-
* the same interface an unmodified external game implements (Phase B).
|
|
11
|
-
*
|
|
12
|
-
* It was `VgaiSceneGameAdapter` until WO-9 tasks #6/#9. Both halves of that
|
|
13
|
-
* name had stopped being true: it loads no scene (the `.vscn.json` branches
|
|
14
|
-
* went with the format in WO-8 — `setup` is the only mount path left), and what
|
|
15
|
-
* it mounts is one three ROOT, not a game (a game is the manifest's whole set
|
|
16
|
-
* of roots). The R3F/TSX world root is a SEPARATE adapter; this one is
|
|
17
|
-
* specifically the `fromSetup` path.
|
|
18
|
-
*
|
|
19
|
-
* `GameSetupFn` / `GameContext` are imported ONLY here (and in the example
|
|
20
|
-
* `fromSetup` wrappers) — never by the host. That confinement is the inversion.
|
|
21
|
-
*/
|
|
22
|
-
import RAPIER from '@dimforge/rapier3d-compat';
|
|
23
|
-
import * as THREE from 'three';
|
|
24
|
-
import { clearAssetCaches } from '../asset-loaders';
|
|
25
|
-
import { createSeededRandom, DEFAULT_SEEDED_RANDOM_SEED, getSeededRandom, } from '../core/seeded-random';
|
|
26
|
-
import { createSimClock, getSimClock } from '../core/sim-clock';
|
|
27
|
-
import { createSystemRunner } from '../core/system-runner';
|
|
28
|
-
import { createDebugDraw } from '../dev/debug-draw';
|
|
29
|
-
import { registerRenderVitals } from '../dev/register-render-vitals';
|
|
30
|
-
import { createRenderDebugAdapter, frameCaptureContextFor, } from '../dev/render-debug-adapter';
|
|
31
|
-
import { collectRenderMemory } from '../dev/render-memory';
|
|
32
|
-
import { RENDER_SUBMIT_PHASE } from '../dev/render-vitals';
|
|
33
|
-
import { createWebGLFrameCapture } from '../dev/webgl-frame-capture';
|
|
34
|
-
import { createWebGLGpuTimer } from '../dev/webgl-gpu-timer';
|
|
35
|
-
import { createSceneIndex } from '../ecs/scene-index';
|
|
36
|
-
import { getUserData, hasUserData, setUserData } from '../ecs/user-data';
|
|
37
|
-
import { InputManager } from '../input/input-manager';
|
|
38
|
-
import { setAssetPrefix } from '../loader';
|
|
39
|
-
import { createCollisionSystem } from '../physics/collision-system';
|
|
40
|
-
import { createPhysicsRegistry } from '../physics/physics-registry';
|
|
41
|
-
import { createTransformWriter } from '../physics/transform-writer';
|
|
42
|
-
import { updateSceneLODs } from '../render/lod';
|
|
43
|
-
import { createSoftParticleDepthPass } from '../render/soft-particle-depth';
|
|
44
|
-
import { disposeSparkRendererWhenIdle, SPARK_DISCOVERY_INTERVAL_MS, sceneHasGaussianSplat, shouldDiscoverGaussianSplat, } from '../render/spark-renderer-lifecycle';
|
|
45
|
-
import { ViewportShadingRenderer } from '../render/viewport-shading';
|
|
46
|
-
import { createDebugRegistry, getDebugRegistry, } from '../runtime/debug-registry';
|
|
47
|
-
import { devBuildEnabled } from '../runtime/dev-build';
|
|
48
|
-
import { disposeDebrisSubtree } from '../runtime/game';
|
|
49
|
-
import { setupAudio } from '../setup/setup-audio';
|
|
50
|
-
import { setupParticles } from '../setup/setup-particles';
|
|
51
|
-
import { setupPhysics, updatePhysicsDebug } from '../setup/setup-physics';
|
|
52
|
-
import { createSceneView } from '../setup/setup-renderer';
|
|
53
|
-
import { createAudioSystemAdapter, releaseAudioMeters } from './first-party-systems';
|
|
54
|
-
import { createRapierPhysicsAdapter } from './rapier-physics-adapter';
|
|
55
|
-
/** Minimal stand-in for the GPU composer when mounting headlessly. */
|
|
56
|
-
function headlessComposer(renderer) {
|
|
57
|
-
return {
|
|
58
|
-
render: () => { },
|
|
59
|
-
setSize: () => { },
|
|
60
|
-
dispose: () => { },
|
|
61
|
-
addPass: () => { },
|
|
62
|
-
removeAllPasses: () => { },
|
|
63
|
-
passes: [],
|
|
64
|
-
getRenderer: () => renderer,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Wrap a first-party `setup` function as a {@link RootAdapter}. This is how every
|
|
69
|
-
* first-party example becomes an implementer of the same interface an external
|
|
70
|
-
* game implements — with no change to the setup's body.
|
|
71
|
-
*/
|
|
72
|
-
export function fromSetup(id, setup) {
|
|
73
|
-
return new SetupThreeRootAdapter({ id, setup });
|
|
74
|
-
}
|
|
75
|
-
export class SetupThreeRootAdapter {
|
|
76
|
-
config;
|
|
77
|
-
id;
|
|
78
|
-
constructor(config = {}) {
|
|
79
|
-
this.config = config;
|
|
80
|
-
this.id = config.id ?? 'setup-three';
|
|
81
|
-
}
|
|
82
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one cohesive first-party bootstrap (moved verbatim from create-runtime); splitting it would obscure the ordering contract
|
|
83
|
-
async mount(host) {
|
|
84
|
-
const { setup, editorPreview, inputMapPath = '/inputmaps/default.inputmap.json', assetPrefix = '/', } = this.config;
|
|
85
|
-
const headless = host.headless === true;
|
|
86
|
-
const { width, height } = host.surface;
|
|
87
|
-
const renderer = host.renderer;
|
|
88
|
-
setAssetPrefix(assetPrefix);
|
|
89
|
-
// --- Scene view (scene/camera/composer). Browser: canonical builder.
|
|
90
|
-
// Headless: plain scene/camera + a no-op composer (no GPU). ---
|
|
91
|
-
let scene;
|
|
92
|
-
let camera;
|
|
93
|
-
let composer;
|
|
94
|
-
if (headless) {
|
|
95
|
-
scene = new THREE.Scene();
|
|
96
|
-
scene.background = new THREE.Color(0x1a1a2e);
|
|
97
|
-
camera = new THREE.PerspectiveCamera(60, width / height || 1, 0.1, 1000);
|
|
98
|
-
camera.position.set(0, 5, 10);
|
|
99
|
-
camera.lookAt(0, 0, 0);
|
|
100
|
-
composer = headlessComposer(renderer);
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
const view = createSceneView(renderer, width, height);
|
|
104
|
-
scene = view.scene;
|
|
105
|
-
camera = view.camera;
|
|
106
|
-
composer = view.composer;
|
|
107
|
-
}
|
|
108
|
-
// --- Rapier physics ---
|
|
109
|
-
await RAPIER.init();
|
|
110
|
-
const physics = setupPhysics(RAPIER, scene);
|
|
111
|
-
const physicsRegistry = createPhysicsRegistry();
|
|
112
|
-
const writeTransforms = createTransformWriter(physicsRegistry, scene);
|
|
113
|
-
// --- Input (InputManager attaches window listeners; stub it headlessly) ---
|
|
114
|
-
const ownsInput = !host.game;
|
|
115
|
-
const input = host.game?.input ?? (headless ? headlessInput() : new InputManager());
|
|
116
|
-
if (!headless) {
|
|
117
|
-
if (host.game)
|
|
118
|
-
await host.game.loadInputMap(inputMapPath);
|
|
119
|
-
else
|
|
120
|
-
await input.loadMap(inputMapPath);
|
|
121
|
-
}
|
|
122
|
-
// --- Collision system ---
|
|
123
|
-
const collisions = createCollisionSystem(physics.rapierWorld, physics.eventQueue);
|
|
124
|
-
// --- Audio (Web Audio — browser only) ---
|
|
125
|
-
const audio = headless ? headlessAudio() : setupAudio(camera);
|
|
126
|
-
// --- Particles (three.quarks BatchedRenderer) ---
|
|
127
|
-
const particles = setupParticles(scene);
|
|
128
|
-
// Scene depth for any soft-particle system registered against this world.
|
|
129
|
-
// Inert (not one traversal, not one draw) until `registerParticleSystem`
|
|
130
|
-
// arms it — see `../render/soft-particle-depth.ts`.
|
|
131
|
-
const softParticleDepth = createSoftParticleDepthPass();
|
|
132
|
-
// --- Debug draw / assets ---
|
|
133
|
-
const debugDraw = createDebugDraw(scene);
|
|
134
|
-
const assets = host.assets;
|
|
135
|
-
// Test/headless adapters may provide the renderer surface needed by the
|
|
136
|
-
// runtime without a real WebGL context. GPU timing is optional diagnostics:
|
|
137
|
-
// degrade honestly when the renderer/context cannot expose extensions.
|
|
138
|
-
const rendererContext = typeof renderer.getContext === 'function' ? renderer.getContext() : undefined;
|
|
139
|
-
const gpuTimer = !headless && rendererContext && typeof rendererContext.getExtension === 'function'
|
|
140
|
-
? createWebGLGpuTimer(rendererContext)
|
|
141
|
-
: null;
|
|
142
|
-
// W4b (F11 frame debugger): first-party WebGL2 frame capture, constructed
|
|
143
|
-
// under the SAME real-context guard as gpuTimer — a headless/stub mount
|
|
144
|
-
// (`frameCaptureContextFor` returns null) never builds it and never
|
|
145
|
-
// registers `renderDebug`, so the capability is honestly absent there. The
|
|
146
|
-
// capture only patches the context WHILE a `captureFrame()` is armed (see
|
|
147
|
-
// beginPass/endPass in the render system below); every non-capture frame
|
|
148
|
-
// is zero-overhead.
|
|
149
|
-
const frameCaptureContext = frameCaptureContextFor(headless, rendererContext);
|
|
150
|
-
const frameCapture = frameCaptureContext ? createWebGLFrameCapture(frameCaptureContext) : null;
|
|
151
|
-
const renderDebugWiring = frameCapture
|
|
152
|
-
? createRenderDebugAdapter({
|
|
153
|
-
capture: frameCapture,
|
|
154
|
-
scene,
|
|
155
|
-
memory: () => collectRenderMemory(scene, renderer.info),
|
|
156
|
-
})
|
|
157
|
-
: null;
|
|
158
|
-
const viewportShading = new ViewportShadingRenderer();
|
|
159
|
-
let viewportShadingMode = 'solid';
|
|
160
|
-
/** Top-level `renderer.render()` calls the last presentation cost — written
|
|
161
|
-
* by the render system, read by `postFrame`. See both for why the count is
|
|
162
|
-
* latched rather than differenced at report time. */
|
|
163
|
-
let lastPresentationPasses = 0;
|
|
164
|
-
// --- System runner (engine-level systems) ---
|
|
165
|
-
const systems = createSystemRunner(host.game?.profiler.systemObserver, 'three');
|
|
166
|
-
// D15/T-D15.3/.5: pass the LIVE shared game tick (not a locally-counted
|
|
167
|
-
// one) into every `poll()` call — `debugRegistry` is declared further
|
|
168
|
-
// down in this same `mount()` (it needs `ctx`/`systemAdapters` to exist
|
|
169
|
-
// first), but this callback only ever executes during real frame
|
|
170
|
-
// ticks, long after `mount()` has fully returned and `debugRegistry` is
|
|
171
|
-
// initialized (a safe closure-over-a-later-const, not a genuine
|
|
172
|
-
// use-before-init). `host.game` gates it: a bare mount with no `Game`
|
|
173
|
-
// shell behind it (test harnesses predating T7.1) has no shared tick to
|
|
174
|
-
// key off, so `InputManager.poll()` falls back to its own internal
|
|
175
|
-
// self-incrementing counter instead (unchanged pre-D15 behavior) rather
|
|
176
|
-
// than being pinned to a constant `0` forever.
|
|
177
|
-
if (ownsInput)
|
|
178
|
-
systems.add('input', () => input.poll(), { name: 'input.poll' });
|
|
179
|
-
systems.add('physics', () => physics.rapierWorld.step(physics.eventQueue), {
|
|
180
|
-
name: 'physics.step',
|
|
181
|
-
});
|
|
182
|
-
systems.add('postPhysics', () => {
|
|
183
|
-
collisions.drain();
|
|
184
|
-
writeTransforms();
|
|
185
|
-
}, { name: 'physics.sync' });
|
|
186
|
-
systems.add('preRender', (dt) => {
|
|
187
|
-
particles.batchedRenderer.update(dt);
|
|
188
|
-
updateSceneLODs(scene, camera);
|
|
189
|
-
sparkDiscoveryElapsedMs += dt * 1_000;
|
|
190
|
-
const shouldDiscover = shouldDiscoverGaussianSplat(0, sparkDiscoveryElapsedMs);
|
|
191
|
-
if (!sparkRenderer && !sparkRendererLoading && !sparkRendererFailed && shouldDiscover) {
|
|
192
|
-
sparkDiscoveryElapsedMs = 0;
|
|
193
|
-
if (!sceneHasGaussianSplat(scene))
|
|
194
|
-
return;
|
|
195
|
-
sparkRendererLoading = true;
|
|
196
|
-
void import('@sparkjsdev/spark')
|
|
197
|
-
.then(({ SparkRenderer }) => {
|
|
198
|
-
if (disposed || sparkRenderer)
|
|
199
|
-
return;
|
|
200
|
-
sparkRenderer = new SparkRenderer({ renderer, enableLod: false });
|
|
201
|
-
sparkRenderer.traverse((node) => setUserData(node, 'engineInternal', true));
|
|
202
|
-
})
|
|
203
|
-
.catch((error) => {
|
|
204
|
-
sparkRendererFailed = true;
|
|
205
|
-
// biome-ignore lint/suspicious/noConsole: A missing renderer chunk must fail once and loudly, not retry every frame.
|
|
206
|
-
console.error('Failed to install SparkRenderer for a live Gaussian splat.', error);
|
|
207
|
-
})
|
|
208
|
-
.finally(() => {
|
|
209
|
-
sparkRendererLoading = false;
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
}, { name: 'materials-and-particles' });
|
|
213
|
-
if (!headless) {
|
|
214
|
-
systems.add('render', (dt) => {
|
|
215
|
-
const profiler = host.game?.profiler;
|
|
216
|
-
if (profiler?.enabled)
|
|
217
|
-
gpuTimer?.begin();
|
|
218
|
-
if (sparkRenderer)
|
|
219
|
-
scene.add(sparkRenderer);
|
|
220
|
-
// W4b: arm-gated frame capture wraps the real draw calls. beginPass
|
|
221
|
-
// is a no-op unless a captureFrame() armed it; afterRender delivers
|
|
222
|
-
// the assembled capture to a pending captureFrame() and restores the
|
|
223
|
-
// patched context — in the SAME finally as gpuTimer.end(). Spark is
|
|
224
|
-
// added before this so its draws fall inside the captured frame.
|
|
225
|
-
// Soft-particle scene depth goes in FRONT of the frame's own draw:
|
|
226
|
-
// the fade samples the geometry behind the particle, which cannot be
|
|
227
|
-
// the depth attachment the particle is drawn into. Deliberately
|
|
228
|
-
// OUTSIDE the capture and profiler brackets below — it is a prepass,
|
|
229
|
-
// not part of the frame's presentation, and a captured frame should
|
|
230
|
-
// show the draws the user sees.
|
|
231
|
-
softParticleDepth.render(renderer, scene, camera);
|
|
232
|
-
renderDebugWiring?.beforeRender();
|
|
233
|
-
// Issue #1504: CPU render submission, bracketed as its OWN profiler
|
|
234
|
-
// phase nested inside the frame's enclosing `render` phase. This is
|
|
235
|
-
// the frame's only isolated measurement of "how long did it take to
|
|
236
|
-
// hand the draws to the driver" — the `render` phase around it also
|
|
237
|
-
// contains physics-debug redraw and every other world's render work,
|
|
238
|
-
// and a system span would key the reading to this adapter's system
|
|
239
|
-
// NAME. The profiler's phase clock is a stack precisely so this
|
|
240
|
-
// bracket cannot truncate its parent (`dev/performance-profiler.ts`).
|
|
241
|
-
// It is also what makes a frame a PRESENTATION as far as the vitals
|
|
242
|
-
// fold is concerned: no draw, no bracket, no display frame.
|
|
243
|
-
profiler?.beginPhase();
|
|
244
|
-
// `info.render.frame` counts top-level renderer.render() calls and
|
|
245
|
-
// survives `info.reset()`, so its delta across the draw is how many
|
|
246
|
-
// passes the composer chain (plus shadows) actually cost.
|
|
247
|
-
const passesBefore = renderer.info.render.frame;
|
|
248
|
-
try {
|
|
249
|
-
viewportShading.render(scene, viewportShadingMode, () => composer.render(dt),
|
|
250
|
-
// Scene authoring can temporarily attach editor infrastructure
|
|
251
|
-
// (gizmos/helpers on their own layer) to this same native scene.
|
|
252
|
-
// Only replace materials the actual game camera can render.
|
|
253
|
-
(mesh) => camera.layers.test(mesh.layers));
|
|
254
|
-
}
|
|
255
|
-
finally {
|
|
256
|
-
profiler?.endPhase(RENDER_SUBMIT_PHASE);
|
|
257
|
-
lastPresentationPasses = renderer.info.render.frame - passesBefore;
|
|
258
|
-
renderDebugWiring?.afterRender();
|
|
259
|
-
sparkRenderer?.removeFromParent();
|
|
260
|
-
gpuTimer?.end();
|
|
261
|
-
}
|
|
262
|
-
if (physics.debugEnabled)
|
|
263
|
-
updatePhysicsDebug(physics.rapierWorld, physics.debugMesh);
|
|
264
|
-
}, { name: 'three.render' });
|
|
265
|
-
}
|
|
266
|
-
// --- Debug toggle (browser only) ---
|
|
267
|
-
const onDebugToggle = (e) => {
|
|
268
|
-
if (e.code === 'KeyP') {
|
|
269
|
-
physics.debugEnabled = !physics.debugEnabled;
|
|
270
|
-
physics.debugMesh.visible = physics.debugEnabled;
|
|
271
|
-
}
|
|
272
|
-
};
|
|
273
|
-
if (!headless)
|
|
274
|
-
window.addEventListener('keydown', onDebugToggle);
|
|
275
|
-
const postFrame = () => {
|
|
276
|
-
if (ownsInput)
|
|
277
|
-
input.endFrame();
|
|
278
|
-
// `renderer.info` is checked, not assumed: a headless/stub renderer
|
|
279
|
-
// supplies only what the runtime needs to mount, and a mount with no
|
|
280
|
-
// counters must report NOTHING rather than throw once per frame. This
|
|
281
|
-
// used to be unreachable because only the editor ever enabled the
|
|
282
|
-
// profiler; the engine enables it itself under the dev gate now (see the
|
|
283
|
-
// render-vitals seed below), so the assumption has to be paid for.
|
|
284
|
-
if (host.game?.profiler.enabled && renderer.info) {
|
|
285
|
-
host.game.profiler.reportRender({
|
|
286
|
-
gpuMs: gpuTimer?.poll() ?? null,
|
|
287
|
-
drawCalls: renderer.info.render.calls,
|
|
288
|
-
triangles: renderer.info.render.triangles,
|
|
289
|
-
geometries: renderer.info.memory.geometries,
|
|
290
|
-
textures: renderer.info.memory.textures,
|
|
291
|
-
// The LAST presentation's pass count, not a delta taken here: under
|
|
292
|
-
// the display-rate loop this hook runs once per fixed SUBSTEP, so a
|
|
293
|
-
// delta measured at this point would read as 0 on every substep that
|
|
294
|
-
// did not draw. Same one-frame-warm semantics as the renderer.info
|
|
295
|
-
// counters beside it.
|
|
296
|
-
renderPasses: lastPresentationPasses,
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
// The mounted game's adapter surface (`mounted.systems`). Declared before
|
|
301
|
-
// ctx so `ctx.registerSystemAdapter` can close over it: the engine fills
|
|
302
|
-
// the first-party entries below; GAME-owned capabilities (networking,
|
|
303
|
-
// navigation) are registered by the game's setup through the ctx hook.
|
|
304
|
-
// The editor holds this same object reference (setActiveSystems), so
|
|
305
|
-
// late registrations are visible to its panels at read time.
|
|
306
|
-
const systemAdapters = {};
|
|
307
|
-
// P3 — `ctx.clock`. Same "ONE per Game root, shared across every world"
|
|
308
|
-
// reach-in as `ctx.debug`/`ctx.random` below: `getSimClock(host.game)`
|
|
309
|
-
// returns the clock `createGame` filed, and `runFrameImpl` is what flushes
|
|
310
|
-
// it. A bare mount with no Game shell (headless harness, foreign host) gets
|
|
311
|
-
// a private, mount-local clock so the field is always a real value — but
|
|
312
|
-
// nothing flushes that one, because a mount with no Game has no fixed loop
|
|
313
|
-
// to bind timers to and inventing a wall-clock frontier here is exactly the
|
|
314
|
-
// hazard `core/sim-clock.ts` exists to avoid. Its `disposeAfter` is
|
|
315
|
-
// therefore unreachable; the disposer is still the real one.
|
|
316
|
-
const gameClock = host.game ? getSimClock(host.game) : null;
|
|
317
|
-
const clock = gameClock ?? createSimClock({ dispose: (obj) => disposeDebrisSubtree(obj, [ctx]) });
|
|
318
|
-
/** Only a mount-local fallback is THIS mount's to dispose — see the
|
|
319
|
-
* `clock.dispose` teardown step below for why the game-scoped one is not. */
|
|
320
|
-
const ownsClock = gameClock === null;
|
|
321
|
-
// --- GameContext (the first-party runtime surface) ---
|
|
322
|
-
const ctx = {
|
|
323
|
-
scene,
|
|
324
|
-
camera,
|
|
325
|
-
rapierWorld: physics.rapierWorld,
|
|
326
|
-
rapier: RAPIER,
|
|
327
|
-
physics: physicsRegistry,
|
|
328
|
-
input,
|
|
329
|
-
collisions,
|
|
330
|
-
composer,
|
|
331
|
-
audio,
|
|
332
|
-
particles,
|
|
333
|
-
debugDraw,
|
|
334
|
-
assets,
|
|
335
|
-
systems,
|
|
336
|
-
clock,
|
|
337
|
-
// The live scene index for THIS root (P2 `observe`). Created before
|
|
338
|
-
// setup() runs so a game's own objects are indexed as they are added —
|
|
339
|
-
// it subscribes to three's `childadded`/`childremoved` on the scene, so
|
|
340
|
-
// everything the game builds arrives through it. Disposed in
|
|
341
|
-
// disposeGame() below (a warm restart keeps it: same `scene`, same ctx).
|
|
342
|
-
sceneIndex: createSceneIndex(scene),
|
|
343
|
-
registerSystemAdapter: (kind, adapter) => {
|
|
344
|
-
systemAdapters[kind] = adapter;
|
|
345
|
-
host.game?.notifySystemAdaptersChanged();
|
|
346
|
-
},
|
|
347
|
-
};
|
|
348
|
-
// Game root (T7.1 slice 1): only present once the host has constructed a
|
|
349
|
-
// Game shell (createGameRuntime does this before mount; headless/foreign
|
|
350
|
-
// hosts may omit it — ctx.game/ctx.roots simply stay undefined, which is
|
|
351
|
-
// the documented zero-break behavior). `ctx.roots` is assigned the SAME
|
|
352
|
-
// live array `host.game` mutates via `registerRoot` (not a copy) — the
|
|
353
|
-
// default world is registered AFTER mount returns, and this reference
|
|
354
|
-
// must observe that later push.
|
|
355
|
-
if (host.game) {
|
|
356
|
-
ctx.game = host.game;
|
|
357
|
-
ctx.roots = host.game.roots;
|
|
358
|
-
ctx.playtest = host.game.playtest ?? null;
|
|
359
|
-
}
|
|
360
|
-
// Everything registered above (the engine systems) is "engine" and must
|
|
361
|
-
// survive a warm
|
|
362
|
-
// restart. Everything a game's setup() (below) adds
|
|
363
|
-
// from here on is "game" content that `hotReload` bulk-removes via
|
|
364
|
-
// `systems.removeAllNonEngine()` on every restart (T1.7).
|
|
365
|
-
systems.markEngineBoundary();
|
|
366
|
-
// First-party System adapters over the real subsystems — seeded HERE,
|
|
367
|
-
// BEFORE setup()/scene load runs below, so that a game which registers any
|
|
368
|
-
// of these kinds during its own setup (via `ctx.registerSystemAdapter`)
|
|
369
|
-
// always wins over the engine's first-party entry (see
|
|
370
|
-
// registerSystemAdapter's docstring in runtime/types.ts). `navigation` is
|
|
371
|
-
// ALSO first-party now (R8 item 7): `createNavigationAdapter` wraps the
|
|
372
|
-
// resolves, not here (no NavMeshManager exists yet at this point in the
|
|
373
|
-
// mount). `networking` remains the one game-owned capability with no
|
|
374
|
-
// first-party implementer; it simply starts absent here.
|
|
375
|
-
// P-4: the physics seam is keyed by node id, so the first-party mount
|
|
376
|
-
// supplies the first-party `id → Object3D` map. `userData.entityId` is
|
|
377
|
-
// THIS stack's identity convention (the scene loader stamps it, and the
|
|
378
|
-
// editor's `objectMap`/`hierarchy.object3D` agree on it), which is exactly
|
|
379
|
-
// why the lookup belongs here and not inside `rapier-physics-adapter.ts`.
|
|
380
|
-
// Memoized because the gizmo/inspector ask repeatedly for one selection;
|
|
381
|
-
// a miss (or an object since detached) re-walks once and re-caches.
|
|
382
|
-
const nodeObjectCache = new Map();
|
|
383
|
-
const resolveNodeObject = (nodeId) => {
|
|
384
|
-
const cached = nodeObjectCache.get(nodeId);
|
|
385
|
-
if (cached?.parent)
|
|
386
|
-
return cached;
|
|
387
|
-
let found = null;
|
|
388
|
-
scene.traverse((o) => {
|
|
389
|
-
if (!found && getUserData(o, 'entityId') === nodeId)
|
|
390
|
-
found = o;
|
|
391
|
-
});
|
|
392
|
-
if (found)
|
|
393
|
-
nodeObjectCache.set(nodeId, found);
|
|
394
|
-
else
|
|
395
|
-
nodeObjectCache.delete(nodeId);
|
|
396
|
-
return found;
|
|
397
|
-
};
|
|
398
|
-
systemAdapters.physics = createRapierPhysicsAdapter(physicsRegistry, physics, resolveNodeObject);
|
|
399
|
-
// D10/T7.6: the audio seam `Game.play.pause()` silences on pause. Works
|
|
400
|
-
// against the headless stand-in too (`headlessAudio()`'s plain
|
|
401
|
-
// `masterGain.gain` object) — this world's `pause()` mutes it harmlessly.
|
|
402
|
-
systemAdapters.audio = createAudioSystemAdapter(audio);
|
|
403
|
-
// Debug/synthetic-player seam (T1.1): ONE registry per Game root, shared
|
|
404
|
-
// across every world that mounts onto it — `getDebugRegistry(host.game)`
|
|
405
|
-
// reaches the SAME accumulator `createGame` filed, so a second/third
|
|
406
|
-
// world's `ctx.debug` feeds it too (see debug-registry.ts's module doc
|
|
407
|
-
// for the provenance/id note). A mount with no Game shell at all (bare
|
|
408
|
-
// test harnesses predating T7.1) gets a private, mount-local registry
|
|
409
|
-
// instead — same absence precedent as `ctx.game`/`ctx.roots` above.
|
|
410
|
-
const debugRegistry = (host.game ? getDebugRegistry(host.game) : null) ??
|
|
411
|
-
createDebugRegistry({ getTick: () => 0, getSimT: () => 0 });
|
|
412
|
-
systemAdapters.debug = debugRegistry.adapter;
|
|
413
|
-
// W4b: the render-debug capability is engine-owned and first-party, seeded
|
|
414
|
-
// here (like physics/input/assets) BEFORE the engineAdapterKinds snapshot
|
|
415
|
-
// below so a warm restart / stop never mistakes it for game content. Only
|
|
416
|
-
// present when a real WebGL2 context backed the mount (renderDebugWiring).
|
|
417
|
-
if (renderDebugWiring)
|
|
418
|
-
systemAdapters.renderDebug = renderDebugWiring.adapter;
|
|
419
|
-
ctx.debug = debugRegistry.forRoot(this.id);
|
|
420
|
-
// T1.2: the InputManager is a first-party per-world handle the registry
|
|
421
|
-
// has no other way to reach — wired as a lazy supplier (not a direct
|
|
422
|
-
// read here), scoped to THIS world's id, so the built-in `input.actions`
|
|
423
|
-
// provider always reflects the CURRENT action set, not a snapshot taken
|
|
424
|
-
// at mount time. Per-world (not a single last-writer-wins slot) since
|
|
425
|
-
// D15/T-D15.5 — see `debug-registry.ts`'s `setInputActionsSource` doc.
|
|
426
|
-
debugRegistry.setInputActionsSource(this.id, () => typeof input.actionNames === 'function'
|
|
427
|
-
? input.actionNames().map((name) => ({ name, valueType: input.getActionValueType(name) }))
|
|
428
|
-
: []);
|
|
429
|
-
// D15/T-D15.5 — same lazy-supplier pattern (and same headless-stand-in
|
|
430
|
-
// guard) as `setInputActionsSource` immediately above: the built-in
|
|
431
|
-
// `input.trace` provider always reads the CURRENT recorded trace, never
|
|
432
|
-
// a snapshot taken at mount time. `seed`/`fixedDt` are assembled here
|
|
433
|
-
// (not inside `InputManager`, which has no business knowing about
|
|
434
|
-
// `ctx.random` or the loop) — the two replay-critical metadata fields
|
|
435
|
-
// the design doc's format sketch (§2.c) calls for beside the raw
|
|
436
|
-
// per-tick deltas; `null` only absent a Game/ctx.random behind this
|
|
437
|
-
// mount, matching every other "no Game" fallback in this file.
|
|
438
|
-
debugRegistry.setInputTraceSource(this.id, () => {
|
|
439
|
-
const raw = typeof input.getInputTrace === 'function'
|
|
440
|
-
? input.getInputTrace()
|
|
441
|
-
: { version: 1, ticks: [] };
|
|
442
|
-
const seed = (host.game ? getSeededRandom(host.game) : null)?.seed ?? null;
|
|
443
|
-
const fixedDt = host.game?.loop.fixedDt ?? null;
|
|
444
|
-
return { version: raw.version, seed, fixedDt, ticks: raw.ticks };
|
|
445
|
-
});
|
|
446
|
-
// Task 2.1 — the same InputManager instance, wired as the debug bridge's
|
|
447
|
-
// actuation target (`runtime/debug-bridge.ts`'s `input.*` methods) at the
|
|
448
|
-
// same seed spot as `setInputActionsSource` above. Per-world (D15 review
|
|
449
|
-
// objection 2 fix) — see `debug-registry.ts`'s `setVirtualInputTarget`
|
|
450
|
-
// doc for why this used to be a single last-writer-wins slot.
|
|
451
|
-
debugRegistry.setVirtualInputTarget(this.id, {
|
|
452
|
-
setVirtualAction: (action, value) => input.setVirtualAction(action, value),
|
|
453
|
-
tapVirtualAction: (action) => input.tapVirtualAction(action),
|
|
454
|
-
clearVirtualActions: () => input.clearVirtualActions(),
|
|
455
|
-
scheduleActionAtTick: (tick, action, value) => input.scheduleActionAtTick(tick, action, value),
|
|
456
|
-
startInputRecording: () => input.startInputRecording(),
|
|
457
|
-
stopInputRecording: () => input.stopInputRecording(),
|
|
458
|
-
isInputRecording: () => input.isInputRecording(),
|
|
459
|
-
injectAxis: (sourceId, value) => input.injectAxis(sourceId, value),
|
|
460
|
-
injectVector2: (sourceId, value) => input.injectVector2(sourceId, value),
|
|
461
|
-
injectPointerDelta: (sourceId, delta) => input.injectPointerDelta(sourceId, delta),
|
|
462
|
-
injectPointerPosition: (sourceId, value) => input.injectPointerPosition(sourceId, value),
|
|
463
|
-
});
|
|
464
|
-
// R4-class fix — wire the InputManager's `'input.schedule.dropped'` sink
|
|
465
|
-
// to THIS world's debug registry, same seed spot/precedent as
|
|
466
|
-
// `setInputActionsSource`/`setVirtualInputTarget` above (typeof-guarded:
|
|
467
|
-
// a headless `InputManager` stand-in may have no `setDebugEmit`).
|
|
468
|
-
if (!host.game && typeof input.setDebugEmit === 'function') {
|
|
469
|
-
input.setDebugEmit((event, detail) => ctx.debug?.emit(event, detail));
|
|
470
|
-
}
|
|
471
|
-
// D15 (T-D15.1) — same "ONE per Game root, shared across every world"
|
|
472
|
-
// pattern as the debug registry immediately above: `getSeededRandom
|
|
473
|
-
// (host.game)` reaches the SAME `SeededRandom` `createGame` filed (boot-
|
|
474
|
-
// seeded by the manifest-aware mount path, `mount-manifest.ts`, before
|
|
475
|
-
// ANY world's `mount()`/`setup()` runs — see `createGame`'s own doc
|
|
476
|
-
// comment), so every world's `ctx.random` is the identical instance, not
|
|
477
|
-
// a per-world copy. Same absence precedent as `ctx.debug`: a mount with
|
|
478
|
-
// no Game shell at all gets a private, mount-local surface instead.
|
|
479
|
-
ctx.random =
|
|
480
|
-
(host.game ? getSeededRandom(host.game) : null) ??
|
|
481
|
-
createSeededRandom(DEFAULT_SEEDED_RANDOM_SEED);
|
|
482
|
-
// --- Live render vitals (issue #1504) -------------------------------------
|
|
483
|
-
// Engine-owned and first-party, seeded HERE like `renderDebug` above: a
|
|
484
|
-
// running game must be able to explain its own frame cost through the debug
|
|
485
|
-
// registry, with no capability to install and nothing for a game to write.
|
|
486
|
-
//
|
|
487
|
-
// THE GATE, in one place. Three conditions, and the reason for each:
|
|
488
|
-
// 1. `devBuildEnabled()` — the ONE owner of "is this a dev/editor
|
|
489
|
-
// context" (`runtime/dev-build.ts`). A ship build registers nothing,
|
|
490
|
-
// subscribes to nothing, and never enables the profiler.
|
|
491
|
-
// 2. a `Game` shell exists — the readings are folded out of that game's
|
|
492
|
-
// profiler frames, and a bare mount has no profiler to fold.
|
|
493
|
-
// 3. not headless — a headless mount registers no render system at all,
|
|
494
|
-
// so it never brackets a submission and never presents a frame. Its
|
|
495
|
-
// vitals could only ever read empty, and registering a door that is
|
|
496
|
-
// structurally incapable of answering is worse than not having one.
|
|
497
|
-
//
|
|
498
|
-
// The enabled-gating question, resolved: the profiler is AUTO-ENABLED here
|
|
499
|
-
// rather than given a second, cheaper always-on counter tier. A counter
|
|
500
|
-
// tier would be a parallel measurement path — exactly the side ledger this
|
|
501
|
-
// work exists to avoid — and the profiler already has the one property
|
|
502
|
-
// that made the choice: `enabled` is a plain flag whose cost is per-frame
|
|
503
|
-
// bookkeeping the editor ALREADY pays (it sets `profiler.enabled = true`
|
|
504
|
-
// on play, `CenterDocuments.tsx`), so under the dev gate this is not a new
|
|
505
|
-
// cost, it is the same cost arriving a little earlier. Nothing here ever
|
|
506
|
-
// turns it OFF: the profiler's other owners (the Performance panel,
|
|
507
|
-
// `render-control.ts`'s perfSample) save and restore the prior state, so a
|
|
508
|
-
// dev session that enabled it at mount keeps it enabled.
|
|
509
|
-
let renderVitals = null;
|
|
510
|
-
const seedRenderVitals = () => {
|
|
511
|
-
// `editorPreview ? true : undefined`: an adapter holding an editor
|
|
512
|
-
// preview is being authored IN the editor — a dev context by
|
|
513
|
-
// definition, even when the editor SPA itself is a production build.
|
|
514
|
-
// The packaged runtime bakes `import.meta.env.DEV = false` into its
|
|
515
|
-
// prebuilt bundle, which is a fact about how the EDITOR was built, not
|
|
516
|
-
// about whether this mount is a dev session — found live: a
|
|
517
|
-
// package-native project's vitals never seeded while its capabilities
|
|
518
|
-
// and every other project-graph module ran fine. The override argument
|
|
519
|
-
// is `devBuildEnabled`'s own documented seam for exactly this
|
|
520
|
-
// caller-knows-better case; a standalone/ship mount (no editorPreview)
|
|
521
|
-
// still resolves purely from the build env.
|
|
522
|
-
if (!host.game || headless || !devBuildEnabled(editorPreview ? true : undefined))
|
|
523
|
-
return;
|
|
524
|
-
renderVitals?.dispose();
|
|
525
|
-
host.game.profiler.enabled = true;
|
|
526
|
-
renderVitals = registerRenderVitals({
|
|
527
|
-
registry: debugRegistry,
|
|
528
|
-
worldId: this.id,
|
|
529
|
-
profiler: host.game.profiler,
|
|
530
|
-
scene,
|
|
531
|
-
renderer,
|
|
532
|
-
});
|
|
533
|
-
};
|
|
534
|
-
seedRenderVitals();
|
|
535
|
-
// Snapshot of the engine-owned adapter kinds, taken right after seeding
|
|
536
|
-
// and before any setup() has had a chance to run. `hotReload`/`disposeGame`
|
|
537
|
-
// use this to strip every GAME-registered kind (including an override of a
|
|
538
|
-
// first-party kind) left over from the outgoing game, so a warm restart or
|
|
539
|
-
// a full stop never exposes a disposed game's networking/etc. adapter to
|
|
540
|
-
// the next setup.
|
|
541
|
-
const engineAdapterKinds = new Set(Object.keys(systemAdapters));
|
|
542
|
-
// 'navigation' is engine-owned too (R8 item 7), but the scene load that
|
|
543
|
-
// populates it (below, `applySceneNavigation`) happens AFTER this
|
|
544
|
-
// snapshot — add it explicitly so the generic "strip every non-engine
|
|
545
|
-
// kind" step (hotReload/disposeGame) never mistakes a first-party
|
|
546
|
-
// navigation adapter for game-registered content and deletes it. (The
|
|
547
|
-
// adapter is still kept in sync with the CURRENT scene's NavMeshManager
|
|
548
|
-
// via the dedicated `clear navigation adapter` teardown step below, which
|
|
549
|
-
// runs independently of this snapshot.)
|
|
550
|
-
engineAdapterKinds.add('navigation');
|
|
551
|
-
// --- Scene / game setup ---
|
|
552
|
-
const engineSceneChildren = new Set(scene.children.slice());
|
|
553
|
-
// Tag engine-owned infra (particle BatchedRenderer, debug-draw + subtrees) so
|
|
554
|
-
// the editor's play-mode hierarchy skips them — they're not game entities.
|
|
555
|
-
for (const c of engineSceneChildren) {
|
|
556
|
-
c.traverse((n) => setUserData(n, 'engineInternal', true));
|
|
557
|
-
}
|
|
558
|
-
let currentCleanup = null;
|
|
559
|
-
let sparkRenderer = null;
|
|
560
|
-
let sparkRendererLoading = false;
|
|
561
|
-
let sparkRendererFailed = false;
|
|
562
|
-
// Discovery is render-loop scheduling, so advance it from the loop's `dt`
|
|
563
|
-
// instead of introducing a second wall-clock frontier into engine code.
|
|
564
|
-
// Start at the threshold so the first rendered frame discovers eager splats.
|
|
565
|
-
let sparkDiscoveryElapsedMs = SPARK_DISCOVERY_INTERVAL_MS;
|
|
566
|
-
if (!setup) {
|
|
567
|
-
throw new Error('SetupThreeRootAdapter: no `setup` supplied. The `.vscn.json` scene branches ' +
|
|
568
|
-
'(`scenePath` / `sceneData`) were removed with the format; a first-party three root ' +
|
|
569
|
-
'is either an imperative ' +
|
|
570
|
-
'`setup(ctx)` (wrap it with `fromSetup(id, setup)`) or a TSX/R3F world root ' +
|
|
571
|
-
'(`entry` pointing at a .tsx module — mounted by the R3F adapter, not this one).');
|
|
572
|
-
}
|
|
573
|
-
// Headless is legal here: `createSceneView` was skipped above, the composer
|
|
574
|
-
// is a no-op stand-in, and `setup` gets a real `THREE.Scene`/camera. It used
|
|
575
|
-
// to throw because headless existed only for the `.vscn` `sceneData` branch;
|
|
576
|
-
// with the format gone (WO-8) `setup` is the only mount path, and headless
|
|
577
|
-
// unit mounts are what exercise the Game/roots/frame machinery without WebGL.
|
|
578
|
-
currentCleanup = await setup(ctx, editorPreview);
|
|
579
|
-
// Warm the GPU program cache before the first visible frame. A mount gets
|
|
580
|
-
// a fresh WebGL context, so without this every scene material compiles
|
|
581
|
-
// lazily INSIDE the first draw calls — measured as ~0.5s of main-thread
|
|
582
|
-
// driver stalls on every editor play press. Gated on
|
|
583
|
-
// KHR_parallel_shader_compile: with the extension, compileAsync overlaps
|
|
584
|
-
// the compiles with the play transition; WITHOUT it (software
|
|
585
|
-
// rasterizers — SwiftShader in CI) compileAsync degenerates into one big
|
|
586
|
-
// synchronous compile that delays mount by whole seconds, strictly worse
|
|
587
|
-
// than the lazy first-draw path it was replacing. Best-effort: lazy
|
|
588
|
-
// compilation stays correct if it throws, and test hosts pass renderer
|
|
589
|
-
// stubs without these methods.
|
|
590
|
-
const glContext = typeof renderer.getContext === 'function' ? renderer.getContext() : undefined;
|
|
591
|
-
const parallelCompile = !!glContext &&
|
|
592
|
-
typeof glContext.getExtension === 'function' &&
|
|
593
|
-
!!glContext.getExtension('KHR_parallel_shader_compile');
|
|
594
|
-
if (!headless && parallelCompile && typeof renderer.compileAsync === 'function') {
|
|
595
|
-
try {
|
|
596
|
-
await renderer.compileAsync(scene, camera);
|
|
597
|
-
}
|
|
598
|
-
catch {
|
|
599
|
-
// Fall back to lazy compilation.
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
// Idempotency guard for disposeGame — see below. Declared here (not inside
|
|
603
|
-
// disposeGame) so hotReload can also refuse to run on an already-disposed
|
|
604
|
-
// adapter.
|
|
605
|
-
let disposed = false;
|
|
606
|
-
// Run one teardown step in isolation: a throwing step (e.g. a game's own
|
|
607
|
-
// cleanup misbehaving, or a WASM free() panicking) must
|
|
608
|
-
// not abort the remaining steps — otherwise a single bad step leaks
|
|
609
|
-
// the Rapier world and skips input.dispose() with no way to retry (a
|
|
610
|
-
// second call is a no-op once `disposed` is set). Logs and continues.
|
|
611
|
-
const safeStep = (label, fn) => {
|
|
612
|
-
try {
|
|
613
|
-
fn();
|
|
614
|
-
}
|
|
615
|
-
catch (err) {
|
|
616
|
-
console.error(`SetupThreeRootAdapter: teardown step "${label}" threw (continuing)`, err);
|
|
617
|
-
}
|
|
618
|
-
};
|
|
619
|
-
const disposeGame = () => {
|
|
620
|
-
// A second stop() must be a safe no-op — do NOT double-free the Rapier
|
|
621
|
-
// world/EventQueue (WASM handles panic/throw on a second free) or
|
|
622
|
-
// double-run teardown side effects.
|
|
623
|
-
if (disposed)
|
|
624
|
-
return;
|
|
625
|
-
disposed = true;
|
|
626
|
-
safeStep('currentCleanup.dispose', () => {
|
|
627
|
-
currentCleanup?.dispose();
|
|
628
|
-
currentCleanup = null;
|
|
629
|
-
});
|
|
630
|
-
safeStep('sparkRenderer.dispose', () => {
|
|
631
|
-
if (!sparkRenderer)
|
|
632
|
-
return;
|
|
633
|
-
sparkRenderer.removeFromParent();
|
|
634
|
-
disposeSparkRendererWhenIdle(sparkRenderer);
|
|
635
|
-
sparkRenderer = null;
|
|
636
|
-
});
|
|
637
|
-
// The scene instance's NavMeshManager (if any) was just disposed above
|
|
638
|
-
// (currentCleanup.dispose calls `inst.navMesh?.dispose(scene)`) — clear
|
|
639
|
-
// the wrapper too, or `mounted.systems.navigation` would keep pointing
|
|
640
|
-
// at a freed manager (R8 item 7). Unconditional: harmless if navigation
|
|
641
|
-
// was never set.
|
|
642
|
-
safeStep('clear navigation adapter', () => {
|
|
643
|
-
delete systemAdapters.navigation;
|
|
644
|
-
});
|
|
645
|
-
if (!headless) {
|
|
646
|
-
safeStep('removeEventListener(keydown)', () => window.removeEventListener('keydown', onDebugToggle));
|
|
647
|
-
}
|
|
648
|
-
// P3 — the MOUNT-LOCAL clock only, and only when this mount created one
|
|
649
|
-
// (a bare mount with no Game shell). The game-scoped clock is disposed by
|
|
650
|
-
// `GameInternal.dispose()`, which `create-runtime.ts` runs after every
|
|
651
|
-
// root has torn down — because disposing ONE world's mount is a supported
|
|
652
|
-
// way to end a sub-session while the Game keeps running, and destroying a
|
|
653
|
-
// shared clock there froze `now()` and killed the sibling worlds' timers.
|
|
654
|
-
// The asymmetry is the tell: `seededRandom` and `debugRegistry` are
|
|
655
|
-
// game-scoped too, and this teardown destroys neither (it strips only its
|
|
656
|
-
// OWN slice of the registry, a few steps below).
|
|
657
|
-
//
|
|
658
|
-
// Sim timers still go BEFORE the Rapier world they would touch: a pending
|
|
659
|
-
// `disposeAfter`/`after` fires into a live Rapier world, and once that is
|
|
660
|
-
// gone it would operate on freed WASM handles.
|
|
661
|
-
// Disposing cancels every timer and rejects every pending `delay` with an
|
|
662
|
-
// `AbortError` (which awaiting game code is documented to tolerate,
|
|
663
|
-
// exactly like an aborted `fetch`).
|
|
664
|
-
if (ownsClock)
|
|
665
|
-
safeStep('clock.dispose', () => clock.dispose());
|
|
666
|
-
// Before the scene is emptied below: dropping the listeners
|
|
667
|
-
// first means the teardown's ~N `scene.remove()` calls do no index
|
|
668
|
-
// bookkeeping, and — the failure mode this exists to prevent — no
|
|
669
|
-
// `childadded` listener is left behind on a disposed scene.
|
|
670
|
-
safeStep('sceneIndex.dispose', () => ctx.sceneIndex.dispose());
|
|
671
|
-
safeStep('remove rigid bodies', () => {
|
|
672
|
-
const handles = [];
|
|
673
|
-
physics.rapierWorld.forEachRigidBody((b) => handles.push(b.handle));
|
|
674
|
-
for (const h of handles) {
|
|
675
|
-
const b = physics.rapierWorld.getRigidBody(h);
|
|
676
|
-
if (b)
|
|
677
|
-
physics.rapierWorld.removeRigidBody(b);
|
|
678
|
-
}
|
|
679
|
-
});
|
|
680
|
-
safeStep('rapierWorld.free', () => physics.rapierWorld.free());
|
|
681
|
-
safeStep('eventQueue.free', () => physics.eventQueue.free()); // WASM handle — must be freed manually
|
|
682
|
-
safeStep('physicsRegistry.clear', () => physicsRegistry.clear());
|
|
683
|
-
safeStep('scene children dispose', () => {
|
|
684
|
-
for (const obj of [...scene.children]) {
|
|
685
|
-
scene.remove(obj);
|
|
686
|
-
obj.traverse((node) => {
|
|
687
|
-
if (node instanceof THREE.Mesh) {
|
|
688
|
-
if (!hasUserData(node, '__sharedGeometry'))
|
|
689
|
-
node.geometry?.dispose();
|
|
690
|
-
if (Array.isArray(node.material))
|
|
691
|
-
node.material.forEach((m) => {
|
|
692
|
-
m.dispose();
|
|
693
|
-
});
|
|
694
|
-
else
|
|
695
|
-
node.material?.dispose();
|
|
696
|
-
}
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
scene.fog = null;
|
|
700
|
-
scene.background = null;
|
|
701
|
-
scene.environment = null;
|
|
702
|
-
});
|
|
703
|
-
safeStep('clearAssetCaches', () => clearAssetCaches());
|
|
704
|
-
safeStep('debugDraw.clear', () => debugDraw.clear());
|
|
705
|
-
// R8 item 7 (C) — disposeGame (Stop) used to skip both of these, unlike
|
|
706
|
-
// hotReload (warm restart), which already ran them: a game lifecycle
|
|
707
|
-
// system registered via `ctx.systems.register(...)` never had its
|
|
708
|
-
// `dispose()` hook called on a full Stop, and a game-registered
|
|
709
|
-
// systemAdapters kind (e.g. a `setup`-registered 'networking') survived
|
|
710
|
-
// a Stop and would be visible on a STALE `mounted.systems` even though
|
|
711
|
-
// the game that owned it is gone. Same two steps, same relative
|
|
712
|
-
// ordering as hotReload (before the
|
|
713
|
-
// engine-owned audio/input teardown below, which hotReload doesn't
|
|
714
|
-
// touch at all since it warm-restarts rather than tearing engine
|
|
715
|
-
// subsystems down).
|
|
716
|
-
safeStep('systems.removeAllNonEngine', () => systems.removeAllNonEngine());
|
|
717
|
-
safeStep('strip game-registered systemAdapters', () => {
|
|
718
|
-
for (const kind of Object.keys(systemAdapters)) {
|
|
719
|
-
if (!engineAdapterKinds.has(kind))
|
|
720
|
-
delete systemAdapters[kind];
|
|
721
|
-
}
|
|
722
|
-
});
|
|
723
|
-
// A game-hosted mount owns only its world-scoped registrations; sibling
|
|
724
|
-
// roots remain alive during root replacement. A bare standalone adapter
|
|
725
|
-
// owns the registry and can clear it globally.
|
|
726
|
-
safeStep('debugRegistry.strip', () => host.game ? debugRegistry.strip(this.id) : debugRegistry.strip());
|
|
727
|
-
// The strip above removes the vitals REGISTRATIONS (their provenance is
|
|
728
|
-
// this world). This ends the fold's profiler subscription — the one
|
|
729
|
-
// resource `registerRenderVitals` allocates that a strip cannot reach.
|
|
730
|
-
safeStep('renderVitals.dispose', () => {
|
|
731
|
-
renderVitals?.dispose();
|
|
732
|
-
renderVitals = null;
|
|
733
|
-
});
|
|
734
|
-
// This mount created the depth pass, so this mount ends it — the render
|
|
735
|
-
// target it holds is drawing-buffer sized and nothing else can reach it.
|
|
736
|
-
safeStep('softParticleDepth.dispose', () => softParticleDepth.dispose());
|
|
737
|
-
// Release analyser taps BEFORE the audio teardown below. A consumer
|
|
738
|
-
// (the editor's meter poll) should dispose its own handle, but Stop must
|
|
739
|
-
// never leak taps regardless.
|
|
740
|
-
//
|
|
741
|
-
// This used to ride on the Tone introspection module's disposer, which
|
|
742
|
-
// meant a game that never touched Tone leaked them — the module was
|
|
743
|
-
// never loaded, so nothing ever called this. `acquireMeters` belongs to
|
|
744
|
-
// the base adapter (535d248dc), so its release does too, and the world
|
|
745
|
-
// that created the adapter is what disposes it.
|
|
746
|
-
safeStep('audio meters release', () => {
|
|
747
|
-
const audioAdapter = systemAdapters.audio;
|
|
748
|
-
if (audioAdapter)
|
|
749
|
-
releaseAudioMeters(audioAdapter);
|
|
750
|
-
});
|
|
751
|
-
if (!headless) {
|
|
752
|
-
safeStep('audio teardown', () => {
|
|
753
|
-
audio.masterGain.gain.value = 0;
|
|
754
|
-
audio.listener.context.suspend();
|
|
755
|
-
camera.remove(audio.listener);
|
|
756
|
-
composer.dispose();
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
if (ownsInput)
|
|
760
|
-
safeStep('input.dispose', () => input.dispose());
|
|
761
|
-
// W4b: restore any pending capture wrappers and reject a capture in
|
|
762
|
-
// flight (a Stop mid-capture must not leave the context patched).
|
|
763
|
-
safeStep('renderDebug.dispose', () => renderDebugWiring?.dispose());
|
|
764
|
-
safeStep('gpuTimer.dispose', () => gpuTimer?.dispose());
|
|
765
|
-
safeStep('viewportShading.dispose', () => viewportShading.dispose());
|
|
766
|
-
};
|
|
767
|
-
const hotReload = async (newSetup, newEditorPreview) => {
|
|
768
|
-
if (disposed) {
|
|
769
|
-
throw new Error('SetupThreeRootAdapter: cannot hotReload after dispose()/stop()');
|
|
770
|
-
}
|
|
771
|
-
// Full parity with disposeGame for the parts a warm restart rebuilds:
|
|
772
|
-
// game cleanup, component batch teardown (in that
|
|
773
|
-
// ordering, BEFORE bodies are pulled from the world — see disposeGame),
|
|
774
|
-
// scene-UI dispose (React root unmount), and bulk system removal. Each
|
|
775
|
-
// step isolated the same way as disposeGame — one throwing step (e.g. a
|
|
776
|
-
// component dispose) must not abort the rest of the warm restart.
|
|
777
|
-
safeStep('currentCleanup.dispose', () => {
|
|
778
|
-
currentCleanup?.dispose();
|
|
779
|
-
currentCleanup = null;
|
|
780
|
-
});
|
|
781
|
-
// Clear any navigation adapter a previous `setup` registered: `hotReload`'s
|
|
782
|
-
// `newSetup` (below) never auto-wires a replacement, so leaving the old
|
|
783
|
-
// wrapper would point at a disposed manager. A `newSetup` that wants navigation may
|
|
784
|
-
// register its own via `ctx.registerSystemAdapter('navigation', ...)`.
|
|
785
|
-
safeStep('clear navigation adapter', () => {
|
|
786
|
-
delete systemAdapters.navigation;
|
|
787
|
-
});
|
|
788
|
-
safeStep('remove rigid bodies', () => {
|
|
789
|
-
const handles = [];
|
|
790
|
-
physics.rapierWorld.forEachRigidBody((b) => handles.push(b.handle));
|
|
791
|
-
for (const h of handles) {
|
|
792
|
-
const b = physics.rapierWorld.getRigidBody(h);
|
|
793
|
-
if (b)
|
|
794
|
-
physics.rapierWorld.removeRigidBody(b);
|
|
795
|
-
}
|
|
796
|
-
physicsRegistry.clear();
|
|
797
|
-
});
|
|
798
|
-
safeStep('scene children dispose', () => {
|
|
799
|
-
for (const obj of [...scene.children]) {
|
|
800
|
-
if (engineSceneChildren.has(obj))
|
|
801
|
-
continue;
|
|
802
|
-
scene.remove(obj);
|
|
803
|
-
obj.traverse((node) => {
|
|
804
|
-
if (node instanceof THREE.Mesh) {
|
|
805
|
-
if (!hasUserData(node, '__sharedGeometry'))
|
|
806
|
-
node.geometry?.dispose();
|
|
807
|
-
if (Array.isArray(node.material))
|
|
808
|
-
node.material.forEach((m) => {
|
|
809
|
-
m.dispose();
|
|
810
|
-
});
|
|
811
|
-
else
|
|
812
|
-
node.material?.dispose();
|
|
813
|
-
}
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
scene.fog = null;
|
|
817
|
-
scene.background = null;
|
|
818
|
-
scene.environment = null;
|
|
819
|
-
});
|
|
820
|
-
safeStep('debugDraw.clear', () => debugDraw.clear());
|
|
821
|
-
// Bulk-remove every system the outgoing game registered (this restart's
|
|
822
|
-
// predecessor) so warm restarts never accumulate duplicate systems —
|
|
823
|
-
// engine systems registered before mount()'s boundary mark are untouched.
|
|
824
|
-
safeStep('systems.removeAllNonEngine', () => systems.removeAllNonEngine());
|
|
825
|
-
// Strip every GAME-registered systemAdapters kind left by the outgoing
|
|
826
|
-
// game (including any override of a first-party kind) so the disposed
|
|
827
|
-
// game's networking/etc. adapter is never exposed to the next setup —
|
|
828
|
-
// see registerSystemAdapter's docstring in runtime/types.ts. ('navigation'
|
|
829
|
-
// is engine-kind and is NOT touched here — it's a first-party adapter now,
|
|
830
|
-
// kept in sync with the current scene's NavMeshManager by the dedicated
|
|
831
|
-
// `clear navigation adapter` step above instead.) The new setup below
|
|
832
|
-
// either re-registers a kind or it stays absent.
|
|
833
|
-
safeStep('strip game-registered systemAdapters', () => {
|
|
834
|
-
for (const kind of Object.keys(systemAdapters)) {
|
|
835
|
-
if (!engineAdapterKinds.has(kind))
|
|
836
|
-
delete systemAdapters[kind];
|
|
837
|
-
}
|
|
838
|
-
});
|
|
839
|
-
// Hot-reload re-seed (spec §3.1): the incoming game's setup (below)
|
|
840
|
-
// re-registers its providers/commands under the SAME worldId — strip()
|
|
841
|
-
// clears the outgoing game's registrations first so that re-seed is
|
|
842
|
-
// silent (no stale duplicate-name warning) rather than "replacing" a
|
|
843
|
-
// dead game's entries. Defect 2 fix: scoped to THIS mount's worldId
|
|
844
|
-
// (`this.id`) — a blind global strip() here wiped every OTHER live
|
|
845
|
-
// world's registrations on every warm restart of just this one, plus
|
|
846
|
-
// every react-door registration (whose `useEffect` cleanup never
|
|
847
|
-
// re-fires to restore them after someone else's strip).
|
|
848
|
-
safeStep('debugRegistry.strip', () => debugRegistry.strip(this.id));
|
|
849
|
-
// The strip above takes the ENGINE's own vitals registrations with it
|
|
850
|
-
// (they carry this world's provenance, which is what makes the game's
|
|
851
|
-
// own re-seed silent). Re-seed them before the incoming setup runs, so a
|
|
852
|
-
// warm restart does not silently cost a game its render vitals — and so
|
|
853
|
-
// the fold starts clean rather than carrying the outgoing game's frames.
|
|
854
|
-
safeStep('renderVitals re-seed', seedRenderVitals);
|
|
855
|
-
currentCleanup = await newSetup(ctx, newEditorPreview);
|
|
856
|
-
};
|
|
857
|
-
return {
|
|
858
|
-
kind: 'three',
|
|
859
|
-
scene,
|
|
860
|
-
camera,
|
|
861
|
-
firstParty: true,
|
|
862
|
-
drivesOwnLoop: false,
|
|
863
|
-
update: (dt) => {
|
|
864
|
-
systems.run(dt);
|
|
865
|
-
postFrame();
|
|
866
|
-
},
|
|
867
|
-
resize: (w, h) => {
|
|
868
|
-
const safeWidth = Math.max(1, w);
|
|
869
|
-
const safeHeight = Math.max(1, h);
|
|
870
|
-
composer.setSize(safeWidth, safeHeight);
|
|
871
|
-
camera.aspect = safeWidth / safeHeight;
|
|
872
|
-
camera.updateProjectionMatrix();
|
|
873
|
-
},
|
|
874
|
-
dispose: disposeGame,
|
|
875
|
-
systems: systemAdapters,
|
|
876
|
-
ctx,
|
|
877
|
-
composer: headless ? null : composer,
|
|
878
|
-
setViewportShadingMode: (mode) => {
|
|
879
|
-
viewportShadingMode = mode;
|
|
880
|
-
},
|
|
881
|
-
hotReload,
|
|
882
|
-
frame: {
|
|
883
|
-
runPhase: (phase, dt) => systems.runPhase(phase, dt),
|
|
884
|
-
endFrame: postFrame,
|
|
885
|
-
},
|
|
886
|
-
};
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
/** Headless input stand-in — `InputManager` attaches `window` listeners in its
|
|
890
|
-
* constructor, which Node lacks. A headless mount never reads input. */
|
|
891
|
-
function headlessInput() {
|
|
892
|
-
return {
|
|
893
|
-
poll: () => { },
|
|
894
|
-
endFrame: () => { },
|
|
895
|
-
dispose: () => { },
|
|
896
|
-
loadMap: async () => { },
|
|
897
|
-
isPressed: () => false,
|
|
898
|
-
getAxis: () => 0,
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
/** Headless audio stand-in (no Web Audio). Only the fields the mount/cleanup
|
|
902
|
-
* touch are present. */
|
|
903
|
-
function headlessAudio() {
|
|
904
|
-
return {
|
|
905
|
-
listener: undefined,
|
|
906
|
-
masterGain: { gain: { value: 1 } },
|
|
907
|
-
};
|
|
908
|
-
}
|