@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,5 +1,6 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
import { assertNever } from '../adapter/adapter-surface';
|
|
3
|
+
import { createRootBinding, type RootBinding, type RootDeclaration } from '../adapter/binding';
|
|
3
4
|
import type { CanvasHostContext, DomHostContext, ThreeHostContext } from '../adapter/host-context';
|
|
4
5
|
import type {
|
|
5
6
|
MountedPixiRoot,
|
|
@@ -7,10 +8,12 @@ import type {
|
|
|
7
8
|
MountedRoot,
|
|
8
9
|
MountedThreeRoot,
|
|
9
10
|
RootAdapter,
|
|
11
|
+
SurfaceAdapter,
|
|
10
12
|
} from '../adapter/root-adapter';
|
|
11
13
|
import { createAssetCache } from '../assets';
|
|
12
14
|
import { createGameLoop } from '../core/game-loop';
|
|
13
15
|
import { createHostRenderer } from '../setup/setup-renderer';
|
|
16
|
+
import { getDebugRegistry } from './debug-registry';
|
|
14
17
|
// TYPE-ONLY: `create-runtime.ts` must never value-import `pixi.js` — a canvas
|
|
15
18
|
// `RootMountSpec`'s `adapter` is supplied ALREADY-CONSTRUCTED by the caller, so
|
|
16
19
|
// the host only ever needs the seam's types.
|
|
@@ -19,36 +22,11 @@ import {
|
|
|
19
22
|
createRootInstance,
|
|
20
23
|
type Game,
|
|
21
24
|
type GameInternal,
|
|
22
|
-
isFirstPartyMounted,
|
|
23
25
|
type RootInstance,
|
|
24
26
|
} from './game';
|
|
25
27
|
import { createInputRouter, type RouterAdapterRoot, stackOrder } from './input-router';
|
|
28
|
+
import type { PlaytestContext } from './playtest';
|
|
26
29
|
import { installRenderControlHarness, isRenderModeRequested } from './render-control';
|
|
27
|
-
import type { PlaytestContext } from './types';
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Register a three world for a freshly-mounted game onto the Game shell. Shared
|
|
31
|
-
* by `createGameRuntime` and every headless test harness
|
|
32
|
-
* (`test/game-root.test.ts`, `test/frame-order.test.ts`,
|
|
33
|
-
* `test/game-two-roots.test.ts`) so there is exactly one code path for this
|
|
34
|
-
* wiring. `physics`/`collisions`/`camera`/`frame` are populated from the mount's
|
|
35
|
-
* first-party `GameContext`/ `MountedSetupThreeRoot` when available, left
|
|
36
|
-
* `undefined` otherwise (an external adapter's mount has none of these
|
|
37
|
-
* first-party handles — `frame` in particular is what makes
|
|
38
|
-
* `GameInternal.runFrame` fall back to a single opaque
|
|
39
|
-
* `mounted.update` call per substep for such a world).
|
|
40
|
-
*/
|
|
41
|
-
function firstPartyExtras(
|
|
42
|
-
mounted: MountedRoot,
|
|
43
|
-
): Pick<Parameters<typeof createRootInstance>[0], 'physics' | 'collisions' | 'camera' | 'frame'> {
|
|
44
|
-
if (!isFirstPartyMounted(mounted)) return {};
|
|
45
|
-
return {
|
|
46
|
-
physics: mounted.ctx.physics,
|
|
47
|
-
collisions: mounted.ctx.collisions,
|
|
48
|
-
camera: mounted.ctx.camera,
|
|
49
|
-
frame: mounted.frame,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
30
|
|
|
53
31
|
/**
|
|
54
32
|
* Register a three world onto the Game shell. `opts.id` defaults to
|
|
@@ -59,21 +37,68 @@ export function registerThreeRoot(
|
|
|
59
37
|
game: GameInternal,
|
|
60
38
|
adapter: RootAdapter,
|
|
61
39
|
mounted: MountedThreeRoot,
|
|
62
|
-
opts?:
|
|
40
|
+
opts?: RegisterRootOptions,
|
|
63
41
|
): RootInstance {
|
|
42
|
+
const pausable = opts?.pausable ?? true;
|
|
64
43
|
const world = createRootInstance({
|
|
65
44
|
id: opts?.id ?? 'main',
|
|
66
45
|
kind: 'three',
|
|
67
|
-
pausable
|
|
46
|
+
pausable,
|
|
68
47
|
adapter,
|
|
69
48
|
mounted,
|
|
70
49
|
scene: mounted.scene as THREE.Scene,
|
|
71
|
-
|
|
50
|
+
binding: bindRoot(
|
|
51
|
+
game,
|
|
52
|
+
opts?.declaration,
|
|
53
|
+
{
|
|
54
|
+
surface: 'three',
|
|
55
|
+
adapter: adapter as RootAdapter<'three'>,
|
|
56
|
+
},
|
|
57
|
+
mounted,
|
|
58
|
+
pausable,
|
|
59
|
+
),
|
|
72
60
|
});
|
|
73
61
|
game.registerRoot(world);
|
|
74
62
|
return world;
|
|
75
63
|
}
|
|
76
64
|
|
|
65
|
+
/** What every `register<Surface>Root` takes beyond the mount itself. */
|
|
66
|
+
export interface RegisterRootOptions {
|
|
67
|
+
readonly id?: string | undefined;
|
|
68
|
+
readonly pausable?: boolean | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* The DECLARATION half of this root's binding, from the resolver that
|
|
71
|
+
* produced `adapter` (`adapter/binding.ts`). Absent for a bare harness
|
|
72
|
+
* registration or a `mountManifestRoots` caller who supplied an
|
|
73
|
+
* already-constructed adapter — see `RootInstance.binding` for why that
|
|
74
|
+
* legitimately yields `null` rather than a fabricated binding.
|
|
75
|
+
*/
|
|
76
|
+
readonly declaration?: RootDeclaration | undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Complete the root's binding at the first moment it CAN be completed: the
|
|
81
|
+
* declaration is what the resolver knew, `mounted`/`adapter`/`pausable` are
|
|
82
|
+
* what mounting produced, and the debug registry is the game's own. Nothing
|
|
83
|
+
* here loads or fabricates — see `createRootBinding`.
|
|
84
|
+
*/
|
|
85
|
+
function bindRoot(
|
|
86
|
+
game: GameInternal,
|
|
87
|
+
declaration: RootDeclaration | undefined,
|
|
88
|
+
adapter: SurfaceAdapter,
|
|
89
|
+
mounted: MountedRoot,
|
|
90
|
+
pausable: boolean,
|
|
91
|
+
): RootBinding | null {
|
|
92
|
+
if (!declaration) return null;
|
|
93
|
+
return createRootBinding({
|
|
94
|
+
...declaration,
|
|
95
|
+
adapter,
|
|
96
|
+
mounted,
|
|
97
|
+
pausable,
|
|
98
|
+
debugRegistry: getDebugRegistry(game),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
77
102
|
/**
|
|
78
103
|
* Register a canvas world onto the Game shell — the pixi analog of
|
|
79
104
|
* {@link registerThreeRoot}. `adapter` is the seam's own
|
|
@@ -85,16 +110,18 @@ export function registerPixiRoot(
|
|
|
85
110
|
game: GameInternal,
|
|
86
111
|
adapter: RootAdapter<'canvas'>,
|
|
87
112
|
mounted: MountedPixiRoot,
|
|
88
|
-
opts?:
|
|
113
|
+
opts?: RegisterRootOptions,
|
|
89
114
|
): RootInstance {
|
|
90
115
|
const id = opts?.id ?? 'main';
|
|
116
|
+
const pausable = opts?.pausable ?? true;
|
|
91
117
|
const world = createRootInstance({
|
|
92
118
|
id,
|
|
93
119
|
kind: 'canvas',
|
|
94
|
-
pausable
|
|
120
|
+
pausable,
|
|
95
121
|
adapter,
|
|
96
122
|
mounted,
|
|
97
123
|
stage: mounted.stage,
|
|
124
|
+
binding: bindRoot(game, opts?.declaration, { surface: 'canvas', adapter }, mounted, pausable),
|
|
98
125
|
});
|
|
99
126
|
game.registerRoot(world);
|
|
100
127
|
return world;
|
|
@@ -116,15 +143,17 @@ export function registerReactRoot(
|
|
|
116
143
|
adapter: RootAdapter<'dom'>,
|
|
117
144
|
mounted: MountedReactRoot,
|
|
118
145
|
container: HTMLElement,
|
|
119
|
-
opts?:
|
|
146
|
+
opts?: RegisterRootOptions,
|
|
120
147
|
): RootInstance {
|
|
148
|
+
const pausable = opts?.pausable ?? true;
|
|
121
149
|
const world = createRootInstance({
|
|
122
150
|
id: opts?.id ?? 'main',
|
|
123
151
|
kind: 'dom',
|
|
124
|
-
pausable
|
|
152
|
+
pausable,
|
|
125
153
|
adapter,
|
|
126
154
|
mounted,
|
|
127
155
|
container,
|
|
156
|
+
binding: bindRoot(game, opts?.declaration, { surface: 'dom', adapter }, mounted, pausable),
|
|
128
157
|
});
|
|
129
158
|
game.registerRoot(world);
|
|
130
159
|
return world;
|
|
@@ -156,6 +185,14 @@ export interface RootMountSpecBase {
|
|
|
156
185
|
* `input-router.ts`'s `resolveClaimingRoot`.
|
|
157
186
|
*/
|
|
158
187
|
readonly hitTest?: ((x: number, y: number) => boolean) | undefined;
|
|
188
|
+
/**
|
|
189
|
+
* The DECLARATION half of this root's binding, from whoever resolved
|
|
190
|
+
* `adapter` — see `adapter/binding.ts`'s {@link RootDeclaration}. Carried on
|
|
191
|
+
* the spec because the resolver knows it and the host is the only one who
|
|
192
|
+
* can complete the binding (it needs `mounted`, which does not exist until
|
|
193
|
+
* this spec is mounted).
|
|
194
|
+
*/
|
|
195
|
+
readonly declaration?: RootDeclaration | undefined;
|
|
159
196
|
}
|
|
160
197
|
|
|
161
198
|
export interface ThreeRootMountSpec extends RootMountSpecBase {
|
|
@@ -185,9 +222,8 @@ export interface PixiRootMountSpec extends RootMountSpecBase {
|
|
|
185
222
|
* opaque mount with nothing to tick).
|
|
186
223
|
*
|
|
187
224
|
* Deliberately carries NO `firstParty: true` brand: that brand specifically
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* GameContext at all, so branding it first-party would be a type lie.
|
|
225
|
+
* a react world has no physics or component runtime at all, so branding it
|
|
226
|
+
* first-party would be a type lie.
|
|
191
227
|
* `Game.registerRoot`'s "no state bridge" console warning explicitly exempts
|
|
192
228
|
* `kind: 'dom'` (§7.1-15): a react world has no `observe` BY DESIGN — that
|
|
193
229
|
* hook is scoped to `useRootObservation` (the ingested/foreign-world case),
|
|
@@ -223,7 +259,7 @@ export interface ReactRootAdapter extends RootAdapter<'dom'> {
|
|
|
223
259
|
mount(host: DomHostContext): Promise<MountedReactGame>;
|
|
224
260
|
}
|
|
225
261
|
|
|
226
|
-
/**
|
|
262
|
+
/** Same reasoning as {@link PixiRootMountSpec} above: the seam's
|
|
227
263
|
* `RootAdapter<'dom'>` is what a DOM root actually guarantees. */
|
|
228
264
|
export interface ReactRootMountSpec extends RootMountSpecBase {
|
|
229
265
|
readonly kind: 'dom';
|
|
@@ -249,9 +285,8 @@ export interface RootsRuntimeConfig {
|
|
|
249
285
|
/**
|
|
250
286
|
* Mirrors `ThreeHostContext.headless` (Node conformance tests — no GPU): skips
|
|
251
287
|
* real `WebGLRenderer` construction for every three world in this
|
|
252
|
-
* session (a stand-in renderer is used instead
|
|
253
|
-
* `
|
|
254
|
-
* internally). Pixijs roots are unaffected — Pixi already falls back to
|
|
288
|
+
* session (a stand-in renderer is used instead; adapters special-case
|
|
289
|
+
* `host.headless` themselves). Pixijs roots are unaffected — Pixi already falls back to
|
|
255
290
|
* a 2D canvas renderer with no GPU. Never set `true` in a real host.
|
|
256
291
|
*/
|
|
257
292
|
headless?: boolean | undefined;
|
|
@@ -301,11 +336,9 @@ export interface RootsRuntimeConfig {
|
|
|
301
336
|
/**
|
|
302
337
|
* Handle returned by createGameRuntime() for controlling the running game.
|
|
303
338
|
*
|
|
304
|
-
* This is the GENERIC host handle — it has no first-party concepts
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
* and narrowed to `MountedSetupThreeRoot`
|
|
308
|
-
* (the editor does this for HMR/physics-sync — those are inherently first-party).
|
|
339
|
+
* This is the GENERIC host handle — it has no first-party concepts. A
|
|
340
|
+
* root's own surface is reached through `game.defaultRoot.mounted`, typed
|
|
341
|
+
* as the mounted-root contract (`MountedRoot`).
|
|
309
342
|
*
|
|
310
343
|
* Root access is explicit through `game.roots`, `game.world(id)`, and
|
|
311
344
|
* `game.defaultRoot`; the session does not project a surface-specific alias.
|
|
@@ -341,9 +374,8 @@ export async function createGameRuntime(config: RootsRuntimeConfig): Promise<Gam
|
|
|
341
374
|
// ---------------------------------------------------------------------------
|
|
342
375
|
|
|
343
376
|
/** A stand-in `THREE.WebGLRenderer` for headless (`headless:true`) three
|
|
344
|
-
* roots
|
|
345
|
-
*
|
|
346
|
-
* mount never calls a render-phase method on `host.renderer` at all, so
|
|
377
|
+
* roots: a headless mount never calls a render-phase method on
|
|
378
|
+
* `host.renderer` at all, so
|
|
347
379
|
* this only needs to satisfy the handful of calls THIS file itself makes
|
|
348
380
|
* (`setPixelRatio`/`setClearColor`/`setSize` at mount, `dispose`/
|
|
349
381
|
* `forceContextLoss` at teardown) — never a real GL call. Node conformance
|
|
@@ -372,6 +404,45 @@ interface MountedAdapterRoot {
|
|
|
372
404
|
readonly renderer: THREE.WebGLRenderer | undefined;
|
|
373
405
|
}
|
|
374
406
|
|
|
407
|
+
/**
|
|
408
|
+
* Reclaim ONE mounted root: its adapter, the renderer this file constructed for
|
|
409
|
+
* it (including the WebGL context — a browser keeps only a handful alive, so
|
|
410
|
+
* `dispose()` alone is not enough), and its stacked surface element.
|
|
411
|
+
*
|
|
412
|
+
* ONE body, TWO callers, deliberately: the session's `fullCleanup` (a normal
|
|
413
|
+
* stop) and `mountAllRootSpecs`'s partial-mount ROLLBACK (a later root's mount
|
|
414
|
+
* rejected, so the roots that already mounted must not survive the rejection).
|
|
415
|
+
* Those two must never drift — a rollback that reclaims less than a stop is how
|
|
416
|
+
* a failed play leaves a live renderer and a lost GL context behind with no
|
|
417
|
+
* handle anywhere able to reach it.
|
|
418
|
+
*
|
|
419
|
+
* Throws only what the adapter's own `dispose()` throws; every caller wraps it
|
|
420
|
+
* per entry so one bad root cannot abort the rest of the teardown.
|
|
421
|
+
*/
|
|
422
|
+
function disposeMountedRoot(entry: MountedAdapterRoot, container: HTMLElement): void {
|
|
423
|
+
try {
|
|
424
|
+
entry.mounted.dispose();
|
|
425
|
+
} finally {
|
|
426
|
+
// Renderer + surface release run even when the adapter's dispose threw:
|
|
427
|
+
// the GL context is the scarce resource, and the caller still sees the
|
|
428
|
+
// error (this rethrows through the `finally`).
|
|
429
|
+
entry.renderer?.dispose();
|
|
430
|
+
entry.renderer?.forceContextLoss();
|
|
431
|
+
// Remove from the element's CURRENT parent, not the mount-time
|
|
432
|
+
// `container`: the editor's Game panel re-parents the live surfaces
|
|
433
|
+
// when its mount element swaps (fill <-> device preset, W2c), and
|
|
434
|
+
// `container.removeChild` would throw NotFoundError after such a move.
|
|
435
|
+
// `container` remains the fallback for hosts whose element stand-ins
|
|
436
|
+
// never wire `parentNode` (headless unit fixtures); an element already
|
|
437
|
+
// detached by such a host is a no-op via the catch.
|
|
438
|
+
try {
|
|
439
|
+
(entry.element.parentNode ?? container).removeChild(entry.element);
|
|
440
|
+
} catch {
|
|
441
|
+
/* already detached — nothing to remove */
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
375
446
|
interface OneRootResult {
|
|
376
447
|
readonly mountedEntry: MountedAdapterRoot;
|
|
377
448
|
readonly routerEntry: RouterAdapterRoot;
|
|
@@ -456,7 +527,11 @@ async function mountOneThreeRoot(
|
|
|
456
527
|
game,
|
|
457
528
|
};
|
|
458
529
|
const mounted = await spec.adapter.mount(host);
|
|
459
|
-
registerThreeRoot(game, spec.adapter, mounted, {
|
|
530
|
+
registerThreeRoot(game, spec.adapter, mounted, {
|
|
531
|
+
id: spec.id,
|
|
532
|
+
pausable: spec.pausable,
|
|
533
|
+
declaration: spec.declaration,
|
|
534
|
+
});
|
|
460
535
|
return {
|
|
461
536
|
mountedEntry: { id: spec.id, kind: 'three', element: canvas, mounted, renderer },
|
|
462
537
|
routerEntry: { id: spec.id, zOrder: spec.zOrder ?? 0, canvas, hitTest: spec.hitTest },
|
|
@@ -468,7 +543,7 @@ async function mountOneThreeRoot(
|
|
|
468
543
|
* `mountOneThreeRoot` above.
|
|
469
544
|
*
|
|
470
545
|
* Unlike `mountOneThreeRoot`, this needs no explicit `canvas.style.width`/
|
|
471
|
-
* `.height` re-assertion
|
|
546
|
+
* `.height` re-assertion. The canvas surface constructs its
|
|
472
547
|
* `Application` with `autoDensity: true`, which makes PIXI ITSELF re-stamp
|
|
473
548
|
* `canvas.style.width`/`.height` (real CSS px, matching the LOGICAL
|
|
474
549
|
* width/height passed to `resize()`) on every `app.renderer.resize()` call —
|
|
@@ -500,6 +575,7 @@ async function mountOnePixiRoot(
|
|
|
500
575
|
registerPixiRoot(game, spec.adapter, mounted, {
|
|
501
576
|
id: spec.id,
|
|
502
577
|
pausable: spec.pausable,
|
|
578
|
+
declaration: spec.declaration,
|
|
503
579
|
});
|
|
504
580
|
return {
|
|
505
581
|
mountedEntry: {
|
|
@@ -560,6 +636,7 @@ async function mountOneReactRoot(
|
|
|
560
636
|
registerReactRoot(game, spec.adapter, mounted, layer, {
|
|
561
637
|
id: spec.id,
|
|
562
638
|
pausable: spec.pausable,
|
|
639
|
+
declaration: spec.declaration,
|
|
563
640
|
});
|
|
564
641
|
return {
|
|
565
642
|
mountedEntry: {
|
|
@@ -579,8 +656,8 @@ async function mountOneReactRoot(
|
|
|
579
656
|
* per D5 §1, z-order/ties exactly matching
|
|
580
657
|
* `manifest/load.ts`'s sort, ONE `Game`, and registers every world onto it
|
|
581
658
|
* via `registerThreeRoot`/`registerPixiRoot`/`registerReactRoot` — the
|
|
582
|
-
* SAME wiring `test/
|
|
583
|
-
* three/
|
|
659
|
+
* SAME wiring `test/create-runtime-worlds.test.ts` proves for the
|
|
660
|
+
* three/canvas pair. Worlds MOUNT in `roots` ARRAY order ("manifest
|
|
584
661
|
* declaration order" — the frame/registration axis), independent of
|
|
585
662
|
* `zOrder` (the canvas-stacking/rendering axis) — the two orders can differ
|
|
586
663
|
* and both are honored correctly.
|
|
@@ -628,6 +705,10 @@ function installDefaultRootDevGlobals(game: GameInternal): () => void {
|
|
|
628
705
|
* `isBottom`, plus the surface lookup and bottom-id needed to derive them. */
|
|
629
706
|
interface MountAllRootsInputs extends Omit<OneRootContext, 'canvas' | 'isBottom'> {
|
|
630
707
|
readonly surfacesById: Map<string, HTMLElement>;
|
|
708
|
+
/** The host element the surfaces were stacked into — needed only by the
|
|
709
|
+
* partial-mount rollback, which reclaims them through the same
|
|
710
|
+
* {@link disposeMountedRoot} body a normal stop uses. */
|
|
711
|
+
readonly container: HTMLElement;
|
|
631
712
|
}
|
|
632
713
|
|
|
633
714
|
interface MountAllRootsResult {
|
|
@@ -650,36 +731,60 @@ async function mountAllRootSpecs(
|
|
|
650
731
|
bottomId: string | undefined,
|
|
651
732
|
inputs: MountAllRootsInputs,
|
|
652
733
|
): Promise<MountAllRootsResult> {
|
|
653
|
-
const { surfacesById, ...shared } = inputs;
|
|
734
|
+
const { surfacesById, container, ...shared } = inputs;
|
|
654
735
|
const mountedEntries: MountedAdapterRoot[] = [];
|
|
655
736
|
const routerEntries: RouterAdapterRoot[] = [];
|
|
656
737
|
|
|
657
|
-
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
738
|
+
try {
|
|
739
|
+
for (const spec of mountSpecs) {
|
|
740
|
+
const isBottom = spec.id === bottomId;
|
|
741
|
+
if (spec.kind === 'dom') {
|
|
742
|
+
const layer = surfacesById.get(spec.id)!;
|
|
743
|
+
const { mountedEntry } = await mountOneReactRoot(spec, shared.game, layer);
|
|
744
|
+
mountedEntries.push(mountedEntry);
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
const canvas = surfacesById.get(spec.id)! as HTMLCanvasElement;
|
|
748
|
+
const oneCtx: OneRootContext = { ...shared, canvas, isBottom };
|
|
749
|
+
let mountedEntry: MountedAdapterRoot;
|
|
750
|
+
let routerEntry: RouterAdapterRoot;
|
|
751
|
+
if (spec.kind === 'three') {
|
|
752
|
+
({ mountedEntry, routerEntry } = await mountOneThreeRoot(spec, oneCtx));
|
|
753
|
+
} else if (spec.kind === 'canvas') {
|
|
754
|
+
({ mountedEntry, routerEntry } = await mountOnePixiRoot(spec, oneCtx));
|
|
755
|
+
} else {
|
|
756
|
+
// Exhaustiveness guard (§7.4-2): 'react' was already handled by the
|
|
757
|
+
// early `continue` above, so only a hypothetical 4th `AdapterSurface` can
|
|
758
|
+
// reach here — fail loudly rather than silently defaulting. `spec`
|
|
759
|
+
// itself (not `spec.kind`) is what TS has narrowed to `never`, since
|
|
760
|
+
// `RootMountSpec` is a discriminated union at the object level.
|
|
761
|
+
assertNever(spec, 'create-runtime mount loop');
|
|
762
|
+
}
|
|
662
763
|
mountedEntries.push(mountedEntry);
|
|
663
|
-
|
|
764
|
+
routerEntries.push(routerEntry);
|
|
664
765
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
766
|
+
} catch (err) {
|
|
767
|
+
// PARTIAL MOUNT ROLLBACK. Roots mount sequentially, so a rejection from
|
|
768
|
+
// root N leaves roots 1..N-1 fully live — renderer, WebGL context, R3F
|
|
769
|
+
// tree, adapter registrations — and NOTHING ever gets a handle to them:
|
|
770
|
+
// this function throws instead of returning, so no `GameSession` (and
|
|
771
|
+
// therefore no `stop()`) is ever constructed. Reclaim them here, in
|
|
772
|
+
// REVERSE mount order (the inverse of the order they were built in), then
|
|
773
|
+
// rethrow the ORIGINAL error — the mount failure is what the caller must
|
|
774
|
+
// see, never a teardown error raised while cleaning up after it.
|
|
775
|
+
for (let i = mountedEntries.length - 1; i >= 0; i--) {
|
|
776
|
+
const entry = mountedEntries[i]!;
|
|
777
|
+
try {
|
|
778
|
+
disposeMountedRoot(entry, container);
|
|
779
|
+
} catch (disposeErr) {
|
|
780
|
+
// biome-ignore lint/suspicious/noConsole: a rollback failure must be visible; the original mount error is still what we rethrow
|
|
781
|
+
console.error(
|
|
782
|
+
`createGameRuntime: rolling back root "${entry.id}" after a mount failure threw:`,
|
|
783
|
+
disposeErr,
|
|
784
|
+
);
|
|
785
|
+
}
|
|
680
786
|
}
|
|
681
|
-
|
|
682
|
-
routerEntries.push(routerEntry);
|
|
787
|
+
throw err;
|
|
683
788
|
}
|
|
684
789
|
return { mountedEntries, routerEntries };
|
|
685
790
|
}
|
|
@@ -735,6 +840,37 @@ function installSessionRenderHarness(
|
|
|
735
840
|
};
|
|
736
841
|
}
|
|
737
842
|
|
|
843
|
+
/**
|
|
844
|
+
* Reclaim what `createRootsGameRuntime` built for ITSELF when the mount
|
|
845
|
+
* rejects. `mountAllRootSpecs` has already rolled back every root that DID
|
|
846
|
+
* mount, so what is left is the Game shell (holding whatever registrations
|
|
847
|
+
* those roots made into it) and the stacked surface elements of the roots that
|
|
848
|
+
* never got to mount. Neither is reachable afterwards: the mount throws instead
|
|
849
|
+
* of returning, so no `GameSession` — and therefore no `stop()` — ever exists.
|
|
850
|
+
*
|
|
851
|
+
* Never throws: the MOUNT error is what the caller must see, so a failure while
|
|
852
|
+
* cleaning up after it is reported and swallowed.
|
|
853
|
+
*/
|
|
854
|
+
function reclaimAfterMountFailure(
|
|
855
|
+
game: GameInternal,
|
|
856
|
+
surfaces: Iterable<HTMLElement>,
|
|
857
|
+
container: HTMLElement,
|
|
858
|
+
): void {
|
|
859
|
+
try {
|
|
860
|
+
game.dispose();
|
|
861
|
+
} catch (disposeErr) {
|
|
862
|
+
// biome-ignore lint/suspicious/noConsole: a failed rollback must be visible; the original mount error is what the caller gets
|
|
863
|
+
console.error('createGameRuntime: disposing the game after a mount failure threw:', disposeErr);
|
|
864
|
+
}
|
|
865
|
+
for (const surface of surfaces) {
|
|
866
|
+
try {
|
|
867
|
+
(surface.parentNode ?? container).removeChild(surface);
|
|
868
|
+
} catch {
|
|
869
|
+
/* already detached by the per-root rollback */
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
738
874
|
async function createRootsGameRuntime(config: RootsRuntimeConfig): Promise<GameSession> {
|
|
739
875
|
const { container, roots: specs, width, height, headless = false, seed, playtest } = config;
|
|
740
876
|
if (specs.length === 0) {
|
|
@@ -836,14 +972,14 @@ async function createRootsGameRuntime(config: RootsRuntimeConfig): Promise<GameS
|
|
|
836
972
|
// host's explicit `simulateSubsteps`/`renderOnce` calls — wall-clock time
|
|
837
973
|
// passing while a screenshot is taken must not move the world.
|
|
838
974
|
externalDrive: renderMode,
|
|
839
|
-
//
|
|
975
|
+
// Sim and presentation are wired to different callbacks here:
|
|
840
976
|
// `update` runs the gameplay phases once per consumed fixed substep with
|
|
841
977
|
// `preRender`/`render` withheld, and `render` runs those two once per real
|
|
842
978
|
// display frame with the interpolation alpha. Both closures are invoked
|
|
843
979
|
// only from the loop's own rAF arm, which `externalDrive` never arms — so
|
|
844
980
|
// a capture/offline-export page (`renderMode`) reaches neither, and its
|
|
845
981
|
// `simulateSubsteps` → `game.runFrame(fixedDt)` path stays frame-exact,
|
|
846
|
-
// rendering inside the substep
|
|
982
|
+
// rendering inside the substep.
|
|
847
983
|
update: (dt) => {
|
|
848
984
|
if (started) game.runFrame(dt, { skipRenderPhases: true });
|
|
849
985
|
},
|
|
@@ -858,16 +994,24 @@ async function createRootsGameRuntime(config: RootsRuntimeConfig): Promise<GameS
|
|
|
858
994
|
// `createRootsGameRuntime`'s own cyclomatic complexity down (E4) — same
|
|
859
995
|
// reason `mountOneThreeRoot`/`mountOnePixiRoot` are already split out
|
|
860
996
|
// below.
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
997
|
+
let mounted: MountAllRootsResult;
|
|
998
|
+
try {
|
|
999
|
+
mounted = await mountAllRootSpecs(mountSpecs, bottomId, {
|
|
1000
|
+
game,
|
|
1001
|
+
surfacesById,
|
|
1002
|
+
container,
|
|
1003
|
+
w,
|
|
1004
|
+
h,
|
|
1005
|
+
dpr,
|
|
1006
|
+
headless,
|
|
1007
|
+
assets,
|
|
1008
|
+
antialias: config.antialias,
|
|
1009
|
+
});
|
|
1010
|
+
} catch (err) {
|
|
1011
|
+
reclaimAfterMountFailure(game, surfacesById.values(), container);
|
|
1012
|
+
throw err;
|
|
1013
|
+
}
|
|
1014
|
+
const { mountedEntries, routerEntries } = mounted;
|
|
871
1015
|
|
|
872
1016
|
// --- Delegating input router (D5 §2a) ---
|
|
873
1017
|
const router = createInputRouter(container, routerEntries);
|
|
@@ -894,37 +1038,55 @@ async function createRootsGameRuntime(config: RootsRuntimeConfig): Promise<GameS
|
|
|
894
1038
|
});
|
|
895
1039
|
let stopping = false;
|
|
896
1040
|
|
|
1041
|
+
/**
|
|
1042
|
+
* Reclaim EVERYTHING this session owns, and let no single failure stop that.
|
|
1043
|
+
*
|
|
1044
|
+
* Teardown used to be one `try` over the whole body, so a throwing
|
|
1045
|
+
* `entry.mounted.dispose()` on root 1 skipped root 2's disposal, both
|
|
1046
|
+
* renderers' `forceContextLoss`, `game.dispose()` and the two global
|
|
1047
|
+
* retractions — while `stopComplete` still resolved and the editor still
|
|
1048
|
+
* reported the instance reclaimed. Every step is now isolated: a failure is
|
|
1049
|
+
* COLLECTED and reported, never allowed to abort the steps after it.
|
|
1050
|
+
*/
|
|
897
1051
|
function fullCleanup(): void {
|
|
898
1052
|
if (stopping) return;
|
|
899
1053
|
stopping = true;
|
|
1054
|
+
const failures: { readonly step: string; readonly error: unknown }[] = [];
|
|
1055
|
+
/** Run one teardown step; record its failure and keep going. */
|
|
1056
|
+
const step = (name: string, run: () => void): void => {
|
|
1057
|
+
try {
|
|
1058
|
+
run();
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
failures.push({ step: name, error });
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
900
1063
|
try {
|
|
901
|
-
loop.stop();
|
|
902
|
-
router.dispose();
|
|
1064
|
+
step('loop.stop', () => loop.stop());
|
|
1065
|
+
step('router.dispose', () => router.dispose());
|
|
903
1066
|
for (const entry of mountedEntries) {
|
|
904
|
-
entry.
|
|
905
|
-
entry.renderer?.dispose();
|
|
906
|
-
entry.renderer?.forceContextLoss();
|
|
907
|
-
// Remove from the element's CURRENT parent, not the mount-time
|
|
908
|
-
// `container`: the editor's Game panel re-parents the live surfaces
|
|
909
|
-
// when its mount element swaps (fill <-> device preset, W2c), and
|
|
910
|
-
// `container.removeChild` would throw NotFoundError after such a move.
|
|
911
|
-
// `container` remains the fallback for hosts whose element stand-ins
|
|
912
|
-
// never wire `parentNode` (headless unit fixtures); an element already
|
|
913
|
-
// detached by such a host is a no-op via the catch.
|
|
914
|
-
try {
|
|
915
|
-
(entry.element.parentNode ?? container).removeChild(entry.element);
|
|
916
|
-
} catch {
|
|
917
|
-
/* already detached — nothing to remove */
|
|
918
|
-
}
|
|
1067
|
+
step(`root "${entry.id}"`, () => disposeMountedRoot(entry, container));
|
|
919
1068
|
}
|
|
920
|
-
game.dispose();
|
|
921
|
-
retractDevGlobals();
|
|
922
|
-
retractRenderHarness?.();
|
|
1069
|
+
step('game.dispose', () => game.dispose());
|
|
1070
|
+
step('dev globals', () => retractDevGlobals());
|
|
1071
|
+
step('render harness', () => retractRenderHarness?.());
|
|
923
1072
|
} finally {
|
|
924
1073
|
void Promise.allSettled(
|
|
925
1074
|
mountedEntries.map((entry) => entry.mounted.disposeComplete ?? Promise.resolve()),
|
|
926
1075
|
).then(() => resolveStopComplete());
|
|
927
1076
|
}
|
|
1077
|
+
if (failures.length > 0) {
|
|
1078
|
+
// LOUD, and after everything else has been reclaimed. `stop()` is called
|
|
1079
|
+
// from hosts that must not be left half-torn-down by an early throw, so
|
|
1080
|
+
// the report comes last — but it IS a report: a silent partial teardown
|
|
1081
|
+
// is the failure this whole restructure exists to end. The editor's
|
|
1082
|
+
// `exitPlayMode` catches it and prints it to the editor console.
|
|
1083
|
+
throw new AggregateError(
|
|
1084
|
+
failures.map((f) => f.error),
|
|
1085
|
+
`Game session teardown failed in ${failures.length} step(s): ${failures
|
|
1086
|
+
.map((f) => f.step)
|
|
1087
|
+
.join(', ')}. Every other step still ran.`,
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
928
1090
|
}
|
|
929
1091
|
|
|
930
1092
|
return {
|