@vgai/engine 0.5.2 → 0.5.4
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 +18 -11
- package/package.json +10 -6
- package/schemas/engine-api.json +1 -68
- package/schemas/engine-api.md +1 -32
- package/schemas/engine-capabilities.json +30 -42
- package/schemas/{vgai-game.schema.json → vgai-project.schema.json} +52 -34
- package/src/adapter/adapter-surface.ts +5 -5
- package/src/adapter/authoring.ts +168 -226
- package/src/adapter/colyseus-networking-adapter.ts +44 -5
- package/src/adapter/first-party-systems.ts +156 -42
- package/src/adapter/host-context.ts +177 -46
- package/src/adapter/index.ts +47 -51
- package/src/adapter/ingest/game-contract.ts +2 -2
- package/src/adapter/ingest/scene-capture.ts +18 -19
- package/src/adapter/ingest/structural-ids.ts +127 -0
- package/src/adapter/ingest/upstream-pin.ts +9 -12
- package/src/adapter/loop-gate-report.ts +11 -11
- package/src/adapter/rapier-physics-adapter.ts +27 -9
- package/src/adapter/root-adapter.ts +217 -0
- package/src/adapter/{vgai-scene-game-adapter.ts → setup-three-root-adapter.ts} +173 -351
- package/src/adapter/system-adapter.ts +80 -63
- package/src/ai/navigation.ts +1 -1
- package/src/animation/animation-clock.ts +1 -1
- package/src/animation/camera-ownership.ts +1 -2
- package/src/animation/cubic-spline-interpolant.ts +132 -0
- package/src/animation/theatre-clock-binding.ts +2 -2
- package/src/animation/theatre-object-binding.ts +4 -4
- package/src/animation/xstate-animation-binding.ts +75 -5
- package/src/{scene/schema → asset-formats}/camera.ts +2 -5
- package/src/{scene/schema → asset-formats}/collider.ts +2 -5
- package/src/asset-formats/index.ts +54 -0
- package/src/{scene/schema → asset-formats}/instances.ts +10 -6
- package/src/{scene/schema → asset-formats}/light.ts +3 -6
- package/src/{scene/schema → asset-formats}/material.ts +4 -7
- package/src/{scene/schema → asset-formats}/mesh.ts +4 -7
- package/src/asset-formats/parse.ts +39 -0
- package/src/{scene/schema → asset-formats}/particles.ts +3 -6
- package/src/{scene/schema/environment.ts → asset-formats/render-env.ts} +23 -86
- package/src/{scene/schema → asset-formats}/tuples.ts +1 -1
- package/src/{scene/asset-loaders.ts → asset-loaders.ts} +8 -9
- package/src/asset-parse-error.ts +33 -0
- package/src/{scene/asset-registry.ts → asset-registry.ts} +1 -1
- package/src/assets.ts +1 -1
- package/src/audio/wav-encode.ts +9 -9
- package/src/canvas-react/engine-bridge.ts +59 -0
- package/src/canvas-react/index.ts +50 -0
- package/src/canvas-react/pixi-primitive.tsx +202 -0
- package/src/canvas-react/pixi-react-adapter.tsx +290 -0
- package/src/canvas-react/pixi-react-root-factory.tsx +88 -0
- package/src/canvas-react/world-context.ts +328 -0
- package/src/core/frame-pacing.ts +100 -0
- package/src/core/game-loop.ts +50 -28
- package/src/core/seeded-random.ts +7 -7
- package/src/core/sim-clock.ts +388 -0
- package/src/core/system-runner.ts +17 -63
- package/src/core/types.ts +34 -15
- package/src/data/data-asset.ts +3 -3
- package/src/data/data-check-core.ts +6 -7
- package/src/data/data-ref.ts +11 -11
- package/src/data/vite-plugin-data.ts +10 -10
- package/src/{scene/defaults.ts → defaults.ts} +18 -40
- package/src/dev/render-debug-adapter.ts +1 -1
- package/src/dev/webgl-frame-capture.ts +1 -1
- package/src/ecs/scene-index.ts +439 -0
- package/src/ecs/scene-query.ts +43 -0
- package/src/{scene → ecs}/user-data.ts +17 -36
- package/src/index.ts +7 -9
- package/src/input/input-manager.ts +29 -32
- package/src/input/input-types.ts +2 -2
- package/src/input/schema.ts +5 -5
- package/src/loader.ts +57 -0
- package/src/manifest/editor-port.ts +69 -0
- package/src/manifest/filename.ts +49 -0
- package/src/manifest/index.ts +8 -2
- package/src/manifest/load-file.ts +11 -0
- package/src/manifest/load.ts +65 -77
- package/src/manifest/locate.ts +55 -0
- package/src/manifest/schema.ts +400 -233
- package/src/{scene → physics}/collider-dimensions.ts +3 -3
- package/src/physics/physics-registry.ts +1 -1
- package/src/{world2d/authoring-2d.ts → pixi/authoring.ts} +24 -11
- package/src/pixi/index.ts +43 -0
- package/src/{world2d/ingest-iframe-2d.ts → pixi/ingest-iframe.ts} +9 -9
- package/src/{world2d/ingest2d.ts → pixi/ingest.ts} +28 -28
- package/src/{world2d/physics2d-registry.ts → pixi/physics-registry.ts} +1 -1
- package/src/{world2d/scene-capture-2d.ts → pixi/scene-capture.ts} +3 -3
- package/src/{world2d/system-adapters-2d.ts → pixi/system-adapters.ts} +2 -2
- package/src/react/unmanaged-root-detector.ts +26 -1
- package/src/react/use-data.ts +5 -5
- package/src/react/use-selection.tsx +15 -42
- package/src/react/{game-state.tsx → world-state.tsx} +44 -46
- package/src/render/auto-batcher.ts +1 -2
- package/src/{scene → render}/instance-mesh.ts +1 -1
- package/src/{scene → render}/light-camera-factory.ts +14 -13
- package/src/render/lod.ts +17 -0
- package/src/{scene → render}/material-factory.ts +8 -7
- package/src/{scene → render}/particles-factory.ts +62 -12
- package/src/render/render-batch-system.ts +14 -41
- package/src/render/render-features.ts +1 -1
- package/src/render/render-settings.ts +1 -2
- package/src/render/spark-renderer-lifecycle.ts +1 -1
- package/src/runtime/create-runtime.ts +352 -463
- package/src/runtime/debug-bridge.ts +148 -93
- package/src/runtime/debug-registry.ts +79 -54
- package/src/runtime/dev-layers.ts +40 -0
- package/src/runtime/frame-selector-cache.ts +4 -4
- package/src/runtime/game.ts +618 -369
- package/src/runtime/gameplay-rng-trap.ts +6 -7
- package/src/runtime/input-router.ts +11 -11
- package/src/runtime/mount-game.ts +54 -55
- package/src/runtime/mount-manifest.ts +154 -150
- package/src/runtime/presentation.ts +141 -0
- package/src/runtime/render-audio-control.ts +64 -53
- package/src/runtime/render-control.ts +45 -51
- package/src/runtime/render-seed.ts +3 -4
- package/src/runtime/state-bridge.ts +17 -18
- package/src/runtime/types.ts +94 -61
- package/src/setup/setup-renderer.ts +11 -6
- package/src/world3d-react/engine-bridge.ts +46 -33
- package/src/world3d-react/index.ts +31 -26
- package/src/world3d-react/r3f-adapter.tsx +211 -96
- package/src/world3d-react/r3f-root-factory.tsx +91 -0
- package/src/world3d-react/renderer-config.ts +137 -0
- package/src/world3d-react/world-context.ts +141 -111
- package/schemas/entity2d.schema.json +0 -468
- package/schemas/prefab.schema.json +0 -9992
- package/schemas/scn2d.schema.json +0 -494
- package/schemas/vscn.schema.json +0 -10851
- package/src/adapter/game-adapter.ts +0 -164
- package/src/adapter/ingest/overlay-applier.ts +0 -207
- package/src/adapter/ingest/overlay-apply.ts +0 -168
- package/src/adapter/ingest/overlay-file.ts +0 -126
- package/src/adapter/ingest/overlay-report.ts +0 -176
- package/src/animation/gsap-registration.ts +0 -184
- package/src/audio/audio-introspection.ts +0 -290
- package/src/audio/index.ts +0 -39
- package/src/audio/tone-clock-binding.ts +0 -98
- package/src/audio/tone-context.ts +0 -175
- package/src/audio/tone-offline-render.ts +0 -167
- package/src/ecs/component-manager.ts +0 -814
- package/src/ecs/game-component.ts +0 -260
- package/src/ecs/hmr-swap-report.ts +0 -65
- package/src/physics/trigger-dispatch.ts +0 -97
- package/src/react/root-adapter.tsx +0 -49
- package/src/scene/asset-paths.ts +0 -121
- package/src/scene/asset-ref-check.ts +0 -248
- package/src/scene/component-registry.ts +0 -51
- package/src/scene/parse.ts +0 -204
- package/src/scene/scene-apply.ts +0 -407
- package/src/scene/scene-diff-schema.ts +0 -115
- package/src/scene/scene-diff-types.ts +0 -29
- package/src/scene/scene-loader.ts +0 -1526
- package/src/scene/scene-query.ts +0 -63
- package/src/scene/scene-types.ts +0 -33
- package/src/scene/scene-version.ts +0 -40
- package/src/scene/schema/animation.ts +0 -46
- package/src/scene/schema/audio.ts +0 -25
- package/src/scene/schema/entity-ref.ts +0 -78
- package/src/scene/schema/entity.ts +0 -189
- package/src/scene/schema/index.ts +0 -51
- package/src/scene/schema/joint.ts +0 -26
- package/src/scene/schema/physics.ts +0 -49
- package/src/scene/schema/scene-file.ts +0 -292
- package/src/scene/schema/shadow.ts +0 -24
- package/src/scene/schema/spline.ts +0 -21
- package/src/world2d/asset-paths2d.ts +0 -44
- package/src/world2d/capture-to-scene2d.ts +0 -52
- package/src/world2d/collision-2d.ts +0 -99
- package/src/world2d/entity2d-asset.ts +0 -22
- package/src/world2d/index.ts +0 -91
- package/src/world2d/physics2d-transform.ts +0 -173
- package/src/world2d/physics2d-units.ts +0 -10
- package/src/world2d/pixi-game-adapter.ts +0 -439
- package/src/world2d/pixi-surface.ts +0 -78
- package/src/world2d/scene2d-identity.ts +0 -49
- package/src/world2d/scene2d-loader.ts +0 -433
- package/src/world2d/schema/entity2d.ts +0 -163
- package/src/world2d/schema/physics2d.ts +0 -64
- package/src/world2d/schema/sprite.ts +0 -99
- package/src/world2d/schema/tilemap.ts +0 -39
- package/src/world2d/schema/tuples2d.ts +0 -25
- package/src/world2d/transform-writer-2d.ts +0 -42
- package/src/world2d/types.ts +0 -74
- package/src/world3d-react/behavior.tsx +0 -146
- /package/src/{scene → render}/mesh-shadow.ts +0 -0
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* SetupThreeRootAdapter — the first-party implementer of {@link RootAdapter}
|
|
3
|
+
* that mounts ONE three root from an imperative `setup(ctx)` function.
|
|
3
4
|
*
|
|
4
|
-
* This is where
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* {@link
|
|
8
|
-
* {@link
|
|
9
|
-
* same interface an unmodified external game implements (Phase B).
|
|
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.
|
|
10
18
|
*
|
|
11
19
|
* `GameSetupFn` / `GameContext` are imported ONLY here (and in the example
|
|
12
20
|
* `fromSetup` wrappers) — never by the host. That confinement is the inversion.
|
|
@@ -16,14 +24,15 @@ import RAPIER from '@dimforge/rapier3d-compat';
|
|
|
16
24
|
import type { SparkRenderer } from '@sparkjsdev/spark';
|
|
17
25
|
import type { EffectComposer } from 'postprocessing';
|
|
18
26
|
import * as THREE from 'three';
|
|
27
|
+
import { clearAssetCaches } from '../asset-loaders';
|
|
19
28
|
import {
|
|
20
29
|
createSeededRandom,
|
|
21
30
|
DEFAULT_SEEDED_RANDOM_SEED,
|
|
22
31
|
getSeededRandom,
|
|
23
32
|
} from '../core/seeded-random';
|
|
33
|
+
import { createSimClock, getSimClock, type SimClockInternal } from '../core/sim-clock';
|
|
24
34
|
import { createSystemRunner } from '../core/system-runner';
|
|
25
35
|
import { createDebugDraw } from '../dev/debug-draw';
|
|
26
|
-
import { log } from '../dev/logger';
|
|
27
36
|
import {
|
|
28
37
|
createRenderDebugAdapter,
|
|
29
38
|
frameCaptureContextFor,
|
|
@@ -32,15 +41,14 @@ import {
|
|
|
32
41
|
import { collectRenderMemory } from '../dev/render-memory';
|
|
33
42
|
import { createWebGLFrameCapture } from '../dev/webgl-frame-capture';
|
|
34
43
|
import { createWebGLGpuTimer } from '../dev/webgl-gpu-timer';
|
|
35
|
-
import {
|
|
44
|
+
import { createSceneIndex } from '../ecs/scene-index';
|
|
45
|
+
import { getUserData, hasUserData, setUserData } from '../ecs/user-data';
|
|
36
46
|
import { InputManager } from '../input/input-manager';
|
|
37
47
|
import { setAssetPrefix } from '../loader';
|
|
38
48
|
import { createCollisionSystem } from '../physics/collision-system';
|
|
39
49
|
import { createPhysicsRegistry } from '../physics/physics-registry';
|
|
40
50
|
import { createTransformWriter } from '../physics/transform-writer';
|
|
41
|
-
import {
|
|
42
|
-
import { RenderBatchSystem } from '../render/render-batch-system';
|
|
43
|
-
import { type RenderScope, resolveRenderSettings } from '../render/render-settings';
|
|
51
|
+
import { updateSceneLODs } from '../render/lod';
|
|
44
52
|
import {
|
|
45
53
|
disposeSparkRendererWhenIdle,
|
|
46
54
|
SPARK_DISCOVERY_INTERVAL_MS,
|
|
@@ -53,76 +61,42 @@ import {
|
|
|
53
61
|
type DebugRegistry,
|
|
54
62
|
getDebugRegistry,
|
|
55
63
|
} from '../runtime/debug-registry';
|
|
56
|
-
import type
|
|
64
|
+
import { disposeDebrisSubtree, type RootFrameHooks } from '../runtime/game';
|
|
57
65
|
import type { EditorPreview, GameCleanup, GameContext, GameSetupFn } from '../runtime/types';
|
|
58
|
-
import type { ComponentRegistry } from '../scene/component-registry';
|
|
59
|
-
import {
|
|
60
|
-
clearAssetCaches,
|
|
61
|
-
loadScene,
|
|
62
|
-
loadSceneFromData,
|
|
63
|
-
type SceneInstance,
|
|
64
|
-
updateSceneLODs,
|
|
65
|
-
} from '../scene/scene-loader';
|
|
66
|
-
import type { SceneFile } from '../scene/scene-types';
|
|
67
|
-
import { hasUserData, setUserData } from '../scene/user-data';
|
|
68
66
|
import { type AudioContext as GameAudio, setupAudio } from '../setup/setup-audio';
|
|
69
67
|
import { setupParticles } from '../setup/setup-particles';
|
|
70
68
|
import { setupPhysics, updatePhysicsDebug } from '../setup/setup-physics';
|
|
71
|
-
import {
|
|
72
|
-
import {
|
|
73
|
-
|
|
74
|
-
createInputManagerAdapter,
|
|
75
|
-
createNavigationAdapter,
|
|
76
|
-
createVgaiAssetAdapter,
|
|
77
|
-
} from './first-party-systems';
|
|
78
|
-
import type { GameAdapter, MountedGame } from './game-adapter';
|
|
79
|
-
import type { HostContext } from './host-context';
|
|
69
|
+
import { createSceneView } from '../setup/setup-renderer';
|
|
70
|
+
import { createAudioSystemAdapter, releaseAudioMeters } from './first-party-systems';
|
|
71
|
+
import type { ThreeHostContext } from './host-context';
|
|
80
72
|
import { createRapierPhysicsAdapter } from './rapier-physics-adapter';
|
|
73
|
+
import type { MountedThreeRoot, RootAdapter } from './root-adapter';
|
|
81
74
|
import type { SystemAdapters } from './system-adapter';
|
|
82
75
|
|
|
83
|
-
/** How a {@link
|
|
84
|
-
export interface
|
|
76
|
+
/** How a {@link SetupThreeRootAdapter} builds its game (the former RuntimeConfig). */
|
|
77
|
+
export interface SetupThreeRootConfig {
|
|
85
78
|
/** Game setup function — registers systems and loads scenes. */
|
|
86
79
|
setup?: GameSetupFn | undefined;
|
|
87
80
|
/** Editor context — passed to setup() when launched from the editor. */
|
|
88
81
|
editorPreview?: EditorPreview | undefined;
|
|
89
|
-
/** Path to a .vscn.json scene file. Loaded if no setup is provided. */
|
|
90
|
-
scenePath?: string | undefined;
|
|
91
|
-
/** In-memory scene data. Loaded if no setup and no scenePath is provided. */
|
|
92
|
-
sceneData?: SceneFile | undefined;
|
|
93
|
-
/**
|
|
94
|
-
* Component name -> GameComponent class map, threaded into the scene
|
|
95
|
-
* loader's `SceneLoadContext.componentRegistry` for both the `scenePath`
|
|
96
|
-
* and `sceneData` branches (never for `setup` — a custom setup already
|
|
97
|
-
* owns its own registry, e.g. `template/src/scripts/main.ts`'s generic
|
|
98
|
-
* fallback). Without this, a scene's `components:` entries throw
|
|
99
|
-
* ("Component ... not found in registry") or, pre-T7.2-closure, were
|
|
100
|
-
* simply unreachable through these two config options at all (the T7.2
|
|
101
|
-
* finding `docs/CLI-ON-FOLDER-DESIGN.md` §1D calls out — see
|
|
102
|
-
* `packages/editor/src/adapter-resolver.ts`, the only production caller
|
|
103
|
-
* that sets this today).
|
|
104
|
-
*/
|
|
105
|
-
componentRegistry?: ComponentRegistry | undefined;
|
|
106
82
|
/** Input map JSON path. Defaults to 'inputmaps/default.inputmap.json'. */
|
|
107
83
|
inputMapPath?: string | undefined;
|
|
108
84
|
/** URL prefix for relative asset paths. Defaults to '/'. */
|
|
109
85
|
assetPrefix?: string | undefined;
|
|
110
|
-
/** Stable adapter id (for registry/conformance). Defaults to '
|
|
86
|
+
/** Stable adapter id (for registry/conformance). Defaults to 'setup-three'. */
|
|
111
87
|
id?: string | undefined;
|
|
112
88
|
}
|
|
113
89
|
|
|
114
90
|
/**
|
|
115
|
-
* The first-party `
|
|
91
|
+
* The first-party `MountedThreeRoot`, with concrete extras the editor host uses for
|
|
116
92
|
* first-party features (HMR, physics sync) that are NOT part of the neutral
|
|
117
|
-
* interface. The generic host only touches the `
|
|
93
|
+
* interface. The generic host only touches the `MountedThreeRoot` surface.
|
|
118
94
|
*/
|
|
119
|
-
export interface
|
|
95
|
+
export interface MountedSetupThreeRoot extends MountedThreeRoot {
|
|
120
96
|
/**
|
|
121
97
|
* First-party brand (checklist item 1 / T7.3 lookahead): a plain `'ctx' in
|
|
122
|
-
* mounted` structural check misfires
|
|
123
|
-
*
|
|
124
|
-
* `World2DContext`, not a `GameContext`, and has none of
|
|
125
|
-
* `queryByComponent`/`physics`/`collisions`/`camera`. This literal-`true`
|
|
98
|
+
* mounted` structural check misfires against any foreign mount that happens
|
|
99
|
+
* to carry an unrelated `ctx` key. This literal-`true`
|
|
126
100
|
* property is what `isFirstPartyMounted` (`runtime/game.ts`) actually
|
|
127
101
|
* checks; `game.ts` reads it via a structural `{ firstParty?: unknown }`
|
|
128
102
|
* probe so it never needs a value import of this module (the existing
|
|
@@ -136,16 +110,16 @@ export interface VgaiMountedGame extends MountedGame {
|
|
|
136
110
|
/** Warm-restart: dispose the current game, re-run a new setup on the same ctx. */
|
|
137
111
|
hotReload(newSetup: GameSetupFn, editorPreview?: EditorPreview): Promise<void>;
|
|
138
112
|
/**
|
|
139
|
-
* Phase-partitioned frame entry point (T7.1 slice 2
|
|
140
|
-
* `
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
113
|
+
* Phase-partitioned frame entry point (T7.1 slice 2) — wired onto this
|
|
114
|
+
* world's `RootInstance` by `registerThreeRoot` (`create-runtime.ts`).
|
|
115
|
+
* `Game.runFrame` is the CANONICAL driver going forward: it calls
|
|
116
|
+
* `frame.runPhase` once per (phase, substep) for every world, then
|
|
117
|
+
* `frame.endFrame` once per substep after all roots finish all phases.
|
|
118
|
+
* `update` (below) remains the legacy/direct entry point to the exact same
|
|
119
|
+
* `SystemRunner` — direct callers (editor HMR, adapter conformance tests)
|
|
120
|
+
* keep using it unchanged.
|
|
147
121
|
*/
|
|
148
|
-
readonly frame:
|
|
122
|
+
readonly frame: RootFrameHooks;
|
|
149
123
|
}
|
|
150
124
|
|
|
151
125
|
/** Minimal stand-in for the GPU composer when mounting headlessly. */
|
|
@@ -162,28 +136,25 @@ function headlessComposer(renderer: THREE.WebGLRenderer): EffectComposer {
|
|
|
162
136
|
}
|
|
163
137
|
|
|
164
138
|
/**
|
|
165
|
-
* Wrap a first-party `setup` function as a {@link
|
|
139
|
+
* Wrap a first-party `setup` function as a {@link RootAdapter}. This is how every
|
|
166
140
|
* first-party example becomes an implementer of the same interface an external
|
|
167
141
|
* game implements — with no change to the setup's body.
|
|
168
142
|
*/
|
|
169
|
-
export function fromSetup(id: string, setup: GameSetupFn):
|
|
170
|
-
return new
|
|
143
|
+
export function fromSetup(id: string, setup: GameSetupFn): RootAdapter {
|
|
144
|
+
return new SetupThreeRootAdapter({ id, setup });
|
|
171
145
|
}
|
|
172
146
|
|
|
173
|
-
export class
|
|
147
|
+
export class SetupThreeRootAdapter implements RootAdapter {
|
|
174
148
|
readonly id: string;
|
|
175
|
-
constructor(private readonly config:
|
|
176
|
-
this.id = config.id ?? '
|
|
149
|
+
constructor(private readonly config: SetupThreeRootConfig = {}) {
|
|
150
|
+
this.id = config.id ?? 'setup-three';
|
|
177
151
|
}
|
|
178
152
|
|
|
179
153
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one cohesive first-party bootstrap (moved verbatim from create-runtime); splitting it would obscure the ordering contract
|
|
180
|
-
async mount(host:
|
|
154
|
+
async mount(host: ThreeHostContext): Promise<MountedSetupThreeRoot> {
|
|
181
155
|
const {
|
|
182
156
|
setup,
|
|
183
157
|
editorPreview,
|
|
184
|
-
scenePath,
|
|
185
|
-
sceneData,
|
|
186
|
-
componentRegistry,
|
|
187
158
|
inputMapPath = '/inputmaps/default.inputmap.json',
|
|
188
159
|
assetPrefix = '/',
|
|
189
160
|
} = this.config;
|
|
@@ -268,7 +239,7 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
268
239
|
let viewportShadingMode: ViewportShadingMode = 'solid';
|
|
269
240
|
|
|
270
241
|
// --- System runner (engine-level systems) ---
|
|
271
|
-
const systems = createSystemRunner(host.game?.profiler.systemObserver, '
|
|
242
|
+
const systems = createSystemRunner(host.game?.profiler.systemObserver, 'three');
|
|
272
243
|
// D15/T-D15.3/.5: pass the LIVE shared game tick (not a locally-counted
|
|
273
244
|
// one) into every `poll()` call — `debugRegistry` is declared further
|
|
274
245
|
// down in this same `mount()` (it needs `ctx`/`systemAdapters` to exist
|
|
@@ -384,6 +355,22 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
384
355
|
// late registrations are visible to its panels at read time.
|
|
385
356
|
const systemAdapters: SystemAdapters = {};
|
|
386
357
|
|
|
358
|
+
// P3 — `ctx.clock`. Same "ONE per Game root, shared across every world"
|
|
359
|
+
// reach-in as `ctx.debug`/`ctx.random` below: `getSimClock(host.game)`
|
|
360
|
+
// returns the clock `createGame` filed, and `runFrameImpl` is what flushes
|
|
361
|
+
// it. A bare mount with no Game shell (headless harness, foreign host) gets
|
|
362
|
+
// a private, mount-local clock so the field is always a real value — but
|
|
363
|
+
// nothing flushes that one, because a mount with no Game has no fixed loop
|
|
364
|
+
// to bind timers to and inventing a wall-clock frontier here is exactly the
|
|
365
|
+
// hazard `core/sim-clock.ts` exists to avoid. Its `disposeAfter` is
|
|
366
|
+
// therefore unreachable; the disposer is still the real one.
|
|
367
|
+
const gameClock: SimClockInternal | null = host.game ? getSimClock(host.game) : null;
|
|
368
|
+
const clock: SimClockInternal =
|
|
369
|
+
gameClock ?? createSimClock({ dispose: (obj) => disposeDebrisSubtree(obj, [ctx]) });
|
|
370
|
+
/** Only a mount-local fallback is THIS mount's to dispose — see the
|
|
371
|
+
* `clock.dispose` teardown step below for why the game-scoped one is not. */
|
|
372
|
+
const ownsClock = gameClock === null;
|
|
373
|
+
|
|
387
374
|
// --- GameContext (the first-party runtime surface) ---
|
|
388
375
|
const ctx: GameContext = {
|
|
389
376
|
scene,
|
|
@@ -399,30 +386,33 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
399
386
|
debugDraw,
|
|
400
387
|
assets,
|
|
401
388
|
systems,
|
|
402
|
-
|
|
389
|
+
clock,
|
|
390
|
+
// The live scene index for THIS root (P2 `observe`). Created before
|
|
391
|
+
// setup() runs so a game's own objects are indexed as they are added —
|
|
392
|
+
// it subscribes to three's `childadded`/`childremoved` on the scene, so
|
|
393
|
+
// everything the game builds arrives through it. Disposed in
|
|
394
|
+
// disposeGame() below (a warm restart keeps it: same `scene`, same ctx).
|
|
395
|
+
sceneIndex: createSceneIndex(scene),
|
|
403
396
|
registerSystemAdapter: (kind, adapter) => {
|
|
404
397
|
systemAdapters[kind] = adapter;
|
|
398
|
+
host.game?.notifySystemAdaptersChanged();
|
|
405
399
|
},
|
|
406
400
|
};
|
|
407
401
|
// Game root (T7.1 slice 1): only present once the host has constructed a
|
|
408
402
|
// Game shell (createGameRuntime does this before mount; headless/foreign
|
|
409
403
|
// hosts may omit it — ctx.game/ctx.roots simply stay undefined, which is
|
|
410
404
|
// the documented zero-break behavior). `ctx.roots` is assigned the SAME
|
|
411
|
-
// live array `host.game` mutates via `
|
|
405
|
+
// live array `host.game` mutates via `registerRoot` (not a copy) — the
|
|
412
406
|
// default world is registered AFTER mount returns, and this reference
|
|
413
407
|
// must observe that later push.
|
|
414
408
|
if (host.game) {
|
|
415
409
|
ctx.game = host.game;
|
|
416
410
|
ctx.roots = host.game.roots;
|
|
411
|
+
ctx.playtest = host.game.playtest ?? null;
|
|
417
412
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
);
|
|
422
|
-
|
|
423
|
-
// Everything registered above (engine systems + the ComponentManager's
|
|
424
|
-
// per-phase tick, just wired in) is "engine" and must survive a warm
|
|
425
|
-
// restart. Everything a game (setup()/scenePath/sceneData, below) adds
|
|
413
|
+
// Everything registered above (the engine systems) is "engine" and must
|
|
414
|
+
// survive a warm
|
|
415
|
+
// restart. Everything a game's setup() (below) adds
|
|
426
416
|
// from here on is "game" content that `hotReload` bulk-removes via
|
|
427
417
|
// `systems.removeAllNonEngine()` on every restart (T1.7).
|
|
428
418
|
systems.markEngineBoundary();
|
|
@@ -433,26 +423,44 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
433
423
|
// always wins over the engine's first-party entry (see
|
|
434
424
|
// registerSystemAdapter's docstring in runtime/types.ts). `navigation` is
|
|
435
425
|
// ALSO first-party now (R8 item 7): `createNavigationAdapter` wraps the
|
|
436
|
-
// real `NavMeshManager` a scenePath/sceneData scene load may produce (see
|
|
437
|
-
// `applySceneNavigation` below) — it is wired once that scene instance
|
|
438
426
|
// resolves, not here (no NavMeshManager exists yet at this point in the
|
|
439
427
|
// mount). `networking` remains the one game-owned capability with no
|
|
440
428
|
// first-party implementer; it simply starts absent here.
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
429
|
+
// P-4: the physics seam is keyed by node id, so the first-party mount
|
|
430
|
+
// supplies the first-party `id → Object3D` map. `userData.entityId` is
|
|
431
|
+
// THIS stack's identity convention (the scene loader stamps it, and the
|
|
432
|
+
// editor's `objectMap`/`hierarchy.object3D` agree on it), which is exactly
|
|
433
|
+
// why the lookup belongs here and not inside `rapier-physics-adapter.ts`.
|
|
434
|
+
// Memoized because the gizmo/inspector ask repeatedly for one selection;
|
|
435
|
+
// a miss (or an object since detached) re-walks once and re-caches.
|
|
436
|
+
const nodeObjectCache = new Map<string, THREE.Object3D>();
|
|
437
|
+
const resolveNodeObject = (nodeId: string): THREE.Object3D | null => {
|
|
438
|
+
const cached = nodeObjectCache.get(nodeId);
|
|
439
|
+
if (cached?.parent) return cached;
|
|
440
|
+
let found: THREE.Object3D | null = null;
|
|
441
|
+
scene.traverse((o) => {
|
|
442
|
+
if (!found && getUserData(o, 'entityId') === nodeId) found = o;
|
|
443
|
+
});
|
|
444
|
+
if (found) nodeObjectCache.set(nodeId, found);
|
|
445
|
+
else nodeObjectCache.delete(nodeId);
|
|
446
|
+
return found;
|
|
447
|
+
};
|
|
448
|
+
systemAdapters.physics = createRapierPhysicsAdapter(
|
|
449
|
+
physicsRegistry,
|
|
450
|
+
physics,
|
|
451
|
+
resolveNodeObject,
|
|
452
|
+
);
|
|
444
453
|
// D10/T7.6: the audio seam `Game.play.pause()` silences on pause. Works
|
|
445
454
|
// against the headless stand-in too (`headlessAudio()`'s plain
|
|
446
455
|
// `masterGain.gain` object) — this world's `pause()` mutes it harmlessly.
|
|
447
456
|
systemAdapters.audio = createAudioSystemAdapter(audio);
|
|
448
|
-
// Debug/synthetic-player seam (T1.1,
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
// as `ctx.game`/`ctx.roots` above.
|
|
457
|
+
// Debug/synthetic-player seam (T1.1): ONE registry per Game root, shared
|
|
458
|
+
// across every world that mounts onto it — `getDebugRegistry(host.game)`
|
|
459
|
+
// reaches the SAME accumulator `createGame` filed, so a second/third
|
|
460
|
+
// world's `ctx.debug` feeds it too (see debug-registry.ts's module doc
|
|
461
|
+
// for the provenance/id note). A mount with no Game shell at all (bare
|
|
462
|
+
// test harnesses predating T7.1) gets a private, mount-local registry
|
|
463
|
+
// instead — same absence precedent as `ctx.game`/`ctx.roots` above.
|
|
456
464
|
const debugRegistry: DebugRegistry =
|
|
457
465
|
(host.game ? getDebugRegistry(host.game) : null) ??
|
|
458
466
|
createDebugRegistry({ getTick: () => 0, getSimT: () => 0 });
|
|
@@ -462,7 +470,7 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
462
470
|
// below so a warm restart / stop never mistakes it for game content. Only
|
|
463
471
|
// present when a real WebGL2 context backed the mount (renderDebugWiring).
|
|
464
472
|
if (renderDebugWiring) systemAdapters.renderDebug = renderDebugWiring.adapter;
|
|
465
|
-
ctx.debug = debugRegistry.
|
|
473
|
+
ctx.debug = debugRegistry.forRoot(this.id);
|
|
466
474
|
// T1.2: the InputManager is a first-party per-world handle the registry
|
|
467
475
|
// has no other way to reach — wired as a lazy supplier (not a direct
|
|
468
476
|
// read here), scoped to THIS world's id, so the built-in `input.actions`
|
|
@@ -555,7 +563,6 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
555
563
|
c.traverse((n) => setUserData(n, 'engineInternal', true));
|
|
556
564
|
}
|
|
557
565
|
let currentCleanup: GameCleanup | null = null;
|
|
558
|
-
let batchSystem: RenderBatchSystem | null = null;
|
|
559
566
|
let sparkRenderer: SparkRenderer | null = null;
|
|
560
567
|
let sparkRendererLoading = false;
|
|
561
568
|
let sparkRendererFailed = false;
|
|
@@ -564,109 +571,21 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
564
571
|
// Start at the threshold so the first rendered frame discovers eager splats.
|
|
565
572
|
let sparkDiscoveryElapsedMs = SPARK_DISCOVERY_INTERVAL_MS;
|
|
566
573
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
// authored setting even when the scene has no post-processing effects.
|
|
576
|
-
applySceneRenderPipeline(composer, renderer, scene, camera, inst.environment);
|
|
577
|
-
// transparent batching: collapse static/mover meshes sharing a signature
|
|
578
|
-
if (settings['autoBatch']) {
|
|
579
|
-
batchSystem = new RenderBatchSystem(scene, settings);
|
|
580
|
-
batchSystem.build();
|
|
581
|
-
ctx.systems.add('preRender', () => batchSystem?.update());
|
|
582
|
-
if (import.meta.env?.DEV)
|
|
583
|
-
(window as unknown as { __vgaiBatch?: unknown }).__vgaiBatch = batchSystem;
|
|
584
|
-
}
|
|
585
|
-
if (inst.splatCount > 0 && !sparkRenderer) {
|
|
586
|
-
const { SparkRenderer } = await import('@sparkjsdev/spark');
|
|
587
|
-
sparkRenderer = new SparkRenderer({ renderer, enableLod: false });
|
|
588
|
-
sparkRenderer.traverse((node) => setUserData(node, 'engineInternal', true));
|
|
589
|
-
}
|
|
590
|
-
};
|
|
591
|
-
|
|
592
|
-
// Wire the (first-party) navigation system adapter to whichever
|
|
593
|
-
// NavMeshManager the CURRENT scene instance carries — reassigned on every
|
|
594
|
-
// scene load, and explicitly cleared (not just left stale) when the new
|
|
595
|
-
// scene has none, so `mounted.systems.navigation` never lingers pointing
|
|
596
|
-
// at a manager a later teardown step disposes (R8 item 7).
|
|
597
|
-
const applySceneNavigation = (inst: SceneInstance): void => {
|
|
598
|
-
if (inst.navMesh) {
|
|
599
|
-
systemAdapters.navigation = createNavigationAdapter(inst.navMesh);
|
|
600
|
-
} else {
|
|
601
|
-
delete systemAdapters.navigation;
|
|
602
|
-
}
|
|
603
|
-
};
|
|
604
|
-
|
|
605
|
-
const loadFromData = async (data: SceneFile): Promise<SceneInstance> => {
|
|
606
|
-
const inst = await loadSceneFromData(data, {
|
|
607
|
-
scene,
|
|
608
|
-
rapierWorld: ctx.rapierWorld,
|
|
609
|
-
rapier: ctx.rapier,
|
|
610
|
-
physics: ctx.physics,
|
|
611
|
-
audioListener: ctx.audio.listener,
|
|
612
|
-
...(componentRegistry ? { componentRegistry } : {}),
|
|
613
|
-
componentManager: ctx.components,
|
|
614
|
-
particleRenderer: ctx.particles.batchedRenderer,
|
|
615
|
-
...(headless ? {} : { renderer }),
|
|
616
|
-
});
|
|
617
|
-
await applySceneRendering(inst);
|
|
618
|
-
applySceneNavigation(inst);
|
|
619
|
-
ctx.systems.add('postPhysics', (dt) => inst.update(dt));
|
|
620
|
-
return inst;
|
|
621
|
-
};
|
|
622
|
-
|
|
623
|
-
if (setup) {
|
|
624
|
-
if (headless) throw new Error('VgaiSceneGameAdapter: setup-based games require a GPU host');
|
|
625
|
-
currentCleanup = await setup(ctx, editorPreview);
|
|
626
|
-
} else if (scenePath) {
|
|
627
|
-
// The editor viewport is only the fallback/initial play viewpoint. Apply
|
|
628
|
-
// it BEFORE capturing the load baseline so authored scene cameras and
|
|
629
|
-
// camera GameComponents can override it during/after scene load.
|
|
630
|
-
applyEditorCamera(editorPreview, camera);
|
|
631
|
-
// #139: captured immediately BEFORE load so adoptSceneCamera can tell
|
|
632
|
-
// whether a component (e.g. SceneCamera.init) framed the camera.
|
|
633
|
-
const preLoadPose = captureCameraPose(camera);
|
|
634
|
-
const inst = await loadScene(scenePath, {
|
|
635
|
-
scene,
|
|
636
|
-
rapierWorld: ctx.rapierWorld,
|
|
637
|
-
rapier: ctx.rapier,
|
|
638
|
-
physics: ctx.physics,
|
|
639
|
-
audioListener: ctx.audio.listener,
|
|
640
|
-
...(componentRegistry ? { componentRegistry } : {}),
|
|
641
|
-
componentManager: ctx.components,
|
|
642
|
-
particleRenderer: ctx.particles.batchedRenderer,
|
|
643
|
-
...(headless ? {} : { renderer }),
|
|
644
|
-
});
|
|
645
|
-
adoptSceneCamera(inst, camera, preLoadPose);
|
|
646
|
-
await applySceneRendering(inst);
|
|
647
|
-
applySceneNavigation(inst);
|
|
648
|
-
ctx.systems.add('postPhysics', (dt) => inst.update(dt));
|
|
649
|
-
// A pre-baked navmesh (sibling .navmesh file — see scene-loader.ts) holds
|
|
650
|
-
// live recast-navigation WASM handles that leak unless destroyed
|
|
651
|
-
// explicitly; dispose it alongside the scene instance on teardown.
|
|
652
|
-
currentCleanup = {
|
|
653
|
-
dispose: () => {
|
|
654
|
-
inst.navMesh?.dispose(scene);
|
|
655
|
-
inst.dispose();
|
|
656
|
-
},
|
|
657
|
-
};
|
|
658
|
-
} else if (sceneData) {
|
|
659
|
-
applyEditorCamera(editorPreview, camera);
|
|
660
|
-
const preLoadPose = captureCameraPose(camera);
|
|
661
|
-
const inst = await loadFromData(sceneData);
|
|
662
|
-
adoptSceneCamera(inst, camera, preLoadPose);
|
|
663
|
-
currentCleanup = {
|
|
664
|
-
dispose: () => {
|
|
665
|
-
inst.navMesh?.dispose(scene);
|
|
666
|
-
inst.dispose();
|
|
667
|
-
},
|
|
668
|
-
};
|
|
574
|
+
if (!setup) {
|
|
575
|
+
throw new Error(
|
|
576
|
+
'SetupThreeRootAdapter: no `setup` supplied. The `.vscn.json` scene branches ' +
|
|
577
|
+
'(`scenePath` / `sceneData`) were removed with the format; a first-party three root ' +
|
|
578
|
+
'is either an imperative ' +
|
|
579
|
+
'`setup(ctx)` (wrap it with `fromSetup(id, setup)`) or a TSX/R3F world root ' +
|
|
580
|
+
'(`entry` pointing at a .tsx module — mounted by the R3F adapter, not this one).',
|
|
581
|
+
);
|
|
669
582
|
}
|
|
583
|
+
// Headless is legal here: `createSceneView` was skipped above, the composer
|
|
584
|
+
// is a no-op stand-in, and `setup` gets a real `THREE.Scene`/camera. It used
|
|
585
|
+
// to throw because headless existed only for the `.vscn` `sceneData` branch;
|
|
586
|
+
// with the format gone (WO-8) `setup` is the only mount path, and headless
|
|
587
|
+
// unit mounts are what exercise the Game/roots/frame machinery without WebGL.
|
|
588
|
+
currentCleanup = await setup(ctx, editorPreview);
|
|
670
589
|
|
|
671
590
|
// Warm the GPU program cache before the first visible frame. A mount gets
|
|
672
591
|
// a fresh WebGL context, so without this every scene material compiles
|
|
@@ -697,40 +616,16 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
697
616
|
// adapter.
|
|
698
617
|
let disposed = false;
|
|
699
618
|
|
|
700
|
-
//
|
|
701
|
-
//
|
|
702
|
-
//
|
|
703
|
-
// mount-critical chunk (the Colyseus dynamic-import idiom); the editor
|
|
704
|
-
// panel polls, so a few-ms-late attachment is invisible. If the game
|
|
705
|
-
// replaced `systems.audio` with its OWN adapter during setup(), the
|
|
706
|
-
// first-party augmentation is skipped — the game's adapter is the truth
|
|
707
|
-
// the editor sees.
|
|
708
|
-
let disposeAudioIntrospection: (() => void) | null = null;
|
|
709
|
-
const firstPartyAudioAdapter = systemAdapters.audio;
|
|
710
|
-
if (firstPartyAudioAdapter) {
|
|
711
|
-
void import('../audio/audio-introspection').then(
|
|
712
|
-
(m) => {
|
|
713
|
-
if (disposed || systemAdapters.audio !== firstPartyAudioAdapter) return;
|
|
714
|
-
disposeAudioIntrospection = m.attachAudioIntrospection(firstPartyAudioAdapter, audio);
|
|
715
|
-
},
|
|
716
|
-
(err) => {
|
|
717
|
-
log.audio.warn('VgaiSceneGameAdapter: audio introspection unavailable', {
|
|
718
|
-
error: String(err),
|
|
719
|
-
});
|
|
720
|
-
},
|
|
721
|
-
);
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
// Run one teardown step in isolation: a throwing step (e.g. a
|
|
725
|
-
// GameComponent's dispose() misbehaving, or a WASM free() panicking) must
|
|
726
|
-
// not abort the remaining steps — otherwise a single bad component leaks
|
|
619
|
+
// Run one teardown step in isolation: a throwing step (e.g. a game's own
|
|
620
|
+
// cleanup misbehaving, or a WASM free() panicking) must
|
|
621
|
+
// not abort the remaining steps — otherwise a single bad step leaks
|
|
727
622
|
// the Rapier world and skips input.dispose() with no way to retry (a
|
|
728
623
|
// second call is a no-op once `disposed` is set). Logs and continues.
|
|
729
624
|
const safeStep = (label: string, fn: () => void): void => {
|
|
730
625
|
try {
|
|
731
626
|
fn();
|
|
732
627
|
} catch (err) {
|
|
733
|
-
console.error(`
|
|
628
|
+
console.error(`SetupThreeRootAdapter: teardown step "${label}" threw (continuing)`, err);
|
|
734
629
|
}
|
|
735
630
|
};
|
|
736
631
|
|
|
@@ -741,10 +636,6 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
741
636
|
if (disposed) return;
|
|
742
637
|
disposed = true;
|
|
743
638
|
|
|
744
|
-
safeStep('batchSystem.teardown', () => {
|
|
745
|
-
batchSystem?.teardown(); // re-attach detached sources before scene teardown
|
|
746
|
-
batchSystem = null;
|
|
747
|
-
});
|
|
748
639
|
safeStep('currentCleanup.dispose', () => {
|
|
749
640
|
currentCleanup?.dispose();
|
|
750
641
|
currentCleanup = null;
|
|
@@ -769,12 +660,29 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
769
660
|
);
|
|
770
661
|
}
|
|
771
662
|
|
|
772
|
-
//
|
|
773
|
-
//
|
|
774
|
-
// (
|
|
775
|
-
//
|
|
776
|
-
//
|
|
777
|
-
|
|
663
|
+
// P3 — the MOUNT-LOCAL clock only, and only when this mount created one
|
|
664
|
+
// (a bare mount with no Game shell). The game-scoped clock is disposed by
|
|
665
|
+
// `GameInternal.dispose()`, which `create-runtime.ts` runs after every
|
|
666
|
+
// root has torn down — because disposing ONE world's mount is a supported
|
|
667
|
+
// way to end a sub-session while the Game keeps running, and destroying a
|
|
668
|
+
// shared clock there froze `now()` and killed the sibling worlds' timers.
|
|
669
|
+
// The asymmetry is the tell: `seededRandom` and `debugRegistry` are
|
|
670
|
+
// game-scoped too, and this teardown destroys neither (it strips only its
|
|
671
|
+
// OWN slice of the registry, a few steps below).
|
|
672
|
+
//
|
|
673
|
+
// Sim timers still go BEFORE the Rapier world they would touch: a pending
|
|
674
|
+
// `disposeAfter`/`after` fires into a live Rapier world, and once that is
|
|
675
|
+
// gone it would operate on freed WASM handles.
|
|
676
|
+
// Disposing cancels every timer and rejects every pending `delay` with an
|
|
677
|
+
// `AbortError` (which awaiting game code is documented to tolerate,
|
|
678
|
+
// exactly like an aborted `fetch`).
|
|
679
|
+
if (ownsClock) safeStep('clock.dispose', () => clock.dispose());
|
|
680
|
+
|
|
681
|
+
// Before the scene is emptied below: dropping the listeners
|
|
682
|
+
// first means the teardown's ~N `scene.remove()` calls do no index
|
|
683
|
+
// bookkeeping, and — the failure mode this exists to prevent — no
|
|
684
|
+
// `childadded` listener is left behind on a disposed scene.
|
|
685
|
+
safeStep('sceneIndex.dispose', () => ctx.sceneIndex.dispose());
|
|
778
686
|
|
|
779
687
|
safeStep('remove rigid bodies', () => {
|
|
780
688
|
const handles: number[] = [];
|
|
@@ -833,12 +741,18 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
833
741
|
host.game ? debugRegistry.strip(this.id) : debugRegistry.strip(),
|
|
834
742
|
);
|
|
835
743
|
|
|
836
|
-
//
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
744
|
+
// Release analyser taps BEFORE the audio teardown below. A consumer
|
|
745
|
+
// (the editor's meter poll) should dispose its own handle, but Stop must
|
|
746
|
+
// never leak taps regardless.
|
|
747
|
+
//
|
|
748
|
+
// This used to ride on the Tone introspection module's disposer, which
|
|
749
|
+
// meant a game that never touched Tone leaked them — the module was
|
|
750
|
+
// never loaded, so nothing ever called this. `acquireMeters` belongs to
|
|
751
|
+
// the base adapter (535d248dc), so its release does too, and the world
|
|
752
|
+
// that created the adapter is what disposes it.
|
|
753
|
+
safeStep('audio meters release', () => {
|
|
754
|
+
const audioAdapter = systemAdapters.audio;
|
|
755
|
+
if (audioAdapter) releaseAudioMeters(audioAdapter);
|
|
842
756
|
});
|
|
843
757
|
|
|
844
758
|
if (!headless) {
|
|
@@ -862,35 +776,27 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
862
776
|
newEditorPreview?: EditorPreview,
|
|
863
777
|
): Promise<void> => {
|
|
864
778
|
if (disposed) {
|
|
865
|
-
throw new Error('
|
|
779
|
+
throw new Error('SetupThreeRootAdapter: cannot hotReload after dispose()/stop()');
|
|
866
780
|
}
|
|
867
781
|
|
|
868
782
|
// Full parity with disposeGame for the parts a warm restart rebuilds:
|
|
869
|
-
//
|
|
783
|
+
// game cleanup, component batch teardown (in that
|
|
870
784
|
// ordering, BEFORE bodies are pulled from the world — see disposeGame),
|
|
871
785
|
// scene-UI dispose (React root unmount), and bulk system removal. Each
|
|
872
786
|
// step isolated the same way as disposeGame — one throwing step (e.g. a
|
|
873
787
|
// component dispose) must not abort the rest of the warm restart.
|
|
874
|
-
safeStep('batchSystem.teardown', () => {
|
|
875
|
-
batchSystem?.teardown(); // re-attach detached sources before scene teardown
|
|
876
|
-
batchSystem = null;
|
|
877
|
-
});
|
|
878
788
|
safeStep('currentCleanup.dispose', () => {
|
|
879
789
|
currentCleanup?.dispose();
|
|
880
790
|
currentCleanup = null;
|
|
881
791
|
});
|
|
882
|
-
//
|
|
883
|
-
//
|
|
884
|
-
//
|
|
885
|
-
// adapter's own scenePath/sceneData mount, once, at initial mount) — so
|
|
886
|
-
// clear the stale wrapper here. A `newSetup` that wants navigation may
|
|
792
|
+
// Clear any navigation adapter a previous `setup` registered: `hotReload`'s
|
|
793
|
+
// `newSetup` (below) never auto-wires a replacement, so leaving the old
|
|
794
|
+
// wrapper would point at a disposed manager. A `newSetup` that wants navigation may
|
|
887
795
|
// register its own via `ctx.registerSystemAdapter('navigation', ...)`.
|
|
888
796
|
safeStep('clear navigation adapter', () => {
|
|
889
797
|
delete systemAdapters.navigation;
|
|
890
798
|
});
|
|
891
799
|
|
|
892
|
-
safeStep('components.clear', () => ctx.components.clear());
|
|
893
|
-
|
|
894
800
|
safeStep('remove rigid bodies', () => {
|
|
895
801
|
const handles: number[] = [];
|
|
896
802
|
physics.rapierWorld.forEachRigidBody((b) => handles.push(b.handle));
|
|
@@ -954,7 +860,7 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
954
860
|
};
|
|
955
861
|
|
|
956
862
|
return {
|
|
957
|
-
kind: '
|
|
863
|
+
kind: 'three',
|
|
958
864
|
scene,
|
|
959
865
|
camera,
|
|
960
866
|
firstParty: true,
|
|
@@ -986,92 +892,8 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
986
892
|
}
|
|
987
893
|
}
|
|
988
894
|
|
|
989
|
-
// --- Camera precedence helpers (moved from create-runtime, unchanged) ---
|
|
990
|
-
|
|
991
|
-
function applyEditorCamera(
|
|
992
|
-
editor: EditorPreview | undefined,
|
|
993
|
-
runtimeCamera: THREE.PerspectiveCamera,
|
|
994
|
-
): boolean {
|
|
995
|
-
if (!editor?.viewportCamera) return false;
|
|
996
|
-
runtimeCamera.position.fromArray(editor.viewportCamera.position);
|
|
997
|
-
runtimeCamera.quaternion.fromArray(editor.viewportCamera.quaternion);
|
|
998
|
-
return true;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
/** The pose `adoptSceneCamera` compares against to detect that a component
|
|
1002
|
-
* already framed the runtime camera during scene load (#139). Capture with
|
|
1003
|
-
* `captureCameraPose(camera)` immediately BEFORE `loadScene`. */
|
|
1004
|
-
export interface CameraPose {
|
|
1005
|
-
position: THREE.Vector3;
|
|
1006
|
-
quaternion: THREE.Quaternion;
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
export function captureCameraPose(camera: THREE.PerspectiveCamera): CameraPose {
|
|
1010
|
-
return { position: camera.position.clone(), quaternion: camera.quaternion.clone() };
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
export function adoptSceneCamera(
|
|
1014
|
-
sceneInstance: SceneInstance,
|
|
1015
|
-
runtimeCamera: THREE.PerspectiveCamera,
|
|
1016
|
-
preLoadPose?: CameraPose,
|
|
1017
|
-
): boolean {
|
|
1018
|
-
const sceneCam = sceneInstance.cameras[0];
|
|
1019
|
-
if (!sceneCam) return false;
|
|
1020
|
-
// #133 (blind run #4): `camera.type: 'orthographic'` builds a real
|
|
1021
|
-
// OrthographicCamera in the scene graph (`createCamera`), but the RUNTIME
|
|
1022
|
-
// renders through a hardcoded PerspectiveCamera — the authored projection
|
|
1023
|
-
// silently never reached the screen. Until ortho support is ranked
|
|
1024
|
-
// (implement-vs-reject, owner call on #133), say so loudly instead of
|
|
1025
|
-
// letting a coplanar game render in unexplained perspective.
|
|
1026
|
-
if ((sceneCam as THREE.Camera & { isOrthographicCamera?: boolean }).isOrthographicCamera) {
|
|
1027
|
-
console.warn(
|
|
1028
|
-
"adoptSceneCamera (#133): this scene's camera is ORTHOGRAPHIC, but the runtime camera is " +
|
|
1029
|
-
'perspective-only — the orthographic projection is NOT applied (only the pose is). ' +
|
|
1030
|
-
'Use type: "perspective" (a long-lens fake: far position + small fov) until #133 lands.',
|
|
1031
|
-
);
|
|
1032
|
-
}
|
|
1033
|
-
// #139 (blind run #5): if a GameComponent already positioned/oriented the
|
|
1034
|
-
// runtime camera during scene load (`SceneCamera.init`'s
|
|
1035
|
-
// `ctx.camera.position.set` + `lookAt` is the blessed shape), the camera
|
|
1036
|
-
// ENTITY's transform must not stomp it — an unauthored `transform.rotation`
|
|
1037
|
-
// is identity ("stare down −Z"), which silently blanked an entire correct
|
|
1038
|
-
// game while every state-based probe spec passed. Components win the pose;
|
|
1039
|
-
// the entity still contributes the LENS (fov/near/far below), which
|
|
1040
|
-
// `SceneCamera` never sets.
|
|
1041
|
-
const componentFramedCamera =
|
|
1042
|
-
preLoadPose !== undefined &&
|
|
1043
|
-
(!runtimeCamera.position.equals(preLoadPose.position) ||
|
|
1044
|
-
!runtimeCamera.quaternion.equals(preLoadPose.quaternion));
|
|
1045
|
-
const parent = sceneCam.parent;
|
|
1046
|
-
if (parent && !componentFramedCamera) {
|
|
1047
|
-
parent.updateMatrixWorld(true);
|
|
1048
|
-
runtimeCamera.position.copy(parent.getWorldPosition(new THREE.Vector3()));
|
|
1049
|
-
const worldQuat = parent.getWorldQuaternion(new THREE.Quaternion());
|
|
1050
|
-
runtimeCamera.quaternion.copy(worldQuat);
|
|
1051
|
-
// Identity orientation on an adopted scene camera is almost never intent
|
|
1052
|
-
// (it means "look at the horizon down −Z", not "look at my scene") — the
|
|
1053
|
-
// exact silent-blank-viewport footgun above. Adopt it anyway (authored
|
|
1054
|
-
// data is truth) but say so loudly, with the two blessed remedies.
|
|
1055
|
-
if (Math.abs(1 - Math.abs(worldQuat.w)) < 1e-6) {
|
|
1056
|
-
console.warn(
|
|
1057
|
-
'adoptSceneCamera (#139): the scene camera entity has no rotation — the runtime camera ' +
|
|
1058
|
-
'will stare horizontally down −Z and may show nothing. Either attach the SceneCamera ' +
|
|
1059
|
-
'component (position + lookAt, no quaternion math) or author transform.rotation on ' +
|
|
1060
|
-
'the camera entity.',
|
|
1061
|
-
);
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
if (sceneCam instanceof THREE.PerspectiveCamera) {
|
|
1065
|
-
runtimeCamera.fov = sceneCam.fov;
|
|
1066
|
-
runtimeCamera.near = sceneCam.near;
|
|
1067
|
-
runtimeCamera.far = sceneCam.far;
|
|
1068
|
-
runtimeCamera.updateProjectionMatrix();
|
|
1069
|
-
}
|
|
1070
|
-
return true;
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
895
|
/** Headless input stand-in — `InputManager` attaches `window` listeners in its
|
|
1074
|
-
* constructor, which Node lacks.
|
|
896
|
+
* constructor, which Node lacks. A headless mount never reads input. */
|
|
1075
897
|
function headlessInput(): InputManager {
|
|
1076
898
|
return {
|
|
1077
899
|
poll: () => {},
|
|
@@ -1083,8 +905,8 @@ function headlessInput(): InputManager {
|
|
|
1083
905
|
} as unknown as InputManager;
|
|
1084
906
|
}
|
|
1085
907
|
|
|
1086
|
-
/** Headless audio stand-in (no Web Audio). Only the fields the
|
|
1087
|
-
* touch are present
|
|
908
|
+
/** Headless audio stand-in (no Web Audio). Only the fields the mount/cleanup
|
|
909
|
+
* touch are present. */
|
|
1088
910
|
function headlessAudio(): GameAudio {
|
|
1089
911
|
return {
|
|
1090
912
|
listener: undefined as unknown as THREE.AudioListener,
|