@vgai/engine 0.5.15 → 0.5.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/adapter-module.d.ts +229 -33
- package/dist/adapter/adapter-module.d.ts.map +1 -1
- package/dist/adapter/adapter-module.js +222 -50
- package/dist/adapter/authoring-seam-contract.d.ts +530 -0
- package/dist/adapter/authoring-seam-contract.d.ts.map +1 -0
- package/dist/adapter/authoring-seam-contract.js +162 -0
- package/dist/adapter/authoring.d.ts +237 -50
- package/dist/adapter/authoring.d.ts.map +1 -1
- package/dist/adapter/authoring.js +2 -1
- package/dist/adapter/entrypoint-selection-source.d.ts +35 -0
- package/dist/adapter/entrypoint-selection-source.d.ts.map +1 -0
- package/dist/adapter/entrypoint-selection-source.js +151 -0
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts +7 -3
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -1
- package/dist/adapter/finders/scenes-from-entrypoint-selection.js +145 -27
- package/dist/adapter/first-party-systems.d.ts +13 -3
- package/dist/adapter/first-party-systems.d.ts.map +1 -1
- package/dist/adapter/first-party-systems.js +10 -3
- package/dist/adapter/index.d.ts +7 -2
- package/dist/adapter/index.d.ts.map +1 -1
- package/dist/adapter/index.js +4 -0
- package/dist/adapter/ingest/contract-system-adapters.d.ts +1 -1
- package/dist/adapter/ingest/contract-system-adapters.d.ts.map +1 -1
- package/dist/adapter/ingest/contract-system-adapters.js +13 -25
- package/dist/adapter/ingest/game-contract-seams.d.ts +214 -0
- package/dist/adapter/ingest/game-contract-seams.d.ts.map +1 -0
- package/dist/adapter/ingest/game-contract-seams.js +62 -0
- package/dist/adapter/ingest/game-contract.d.ts +5 -3
- package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
- package/dist/adapter/ingest/game-contract.js +4 -1
- package/dist/adapter/ingest/merge-debug-adapters.d.ts +52 -0
- package/dist/adapter/ingest/merge-debug-adapters.d.ts.map +1 -0
- package/dist/adapter/ingest/merge-debug-adapters.js +141 -0
- package/dist/adapter/ingest/observation-debug-adapter.d.ts +56 -0
- package/dist/adapter/ingest/observation-debug-adapter.d.ts.map +1 -0
- package/dist/adapter/ingest/observation-debug-adapter.js +131 -0
- package/dist/adapter/ingest/scene-capture.d.ts +38 -13
- package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
- package/dist/adapter/ingest/scene-capture.js +101 -24
- package/dist/adapter/ingest/visible-capture-window.d.ts +22 -5
- package/dist/adapter/ingest/visible-capture-window.d.ts.map +1 -1
- package/dist/adapter/ingest/visible-capture-window.js +46 -7
- package/dist/adapter/native-debug-module.d.ts +43 -0
- package/dist/adapter/native-debug-module.d.ts.map +1 -0
- package/dist/adapter/native-debug-module.js +230 -0
- package/dist/adapter/object3d-authoring-subject.d.ts +32 -0
- package/dist/adapter/object3d-authoring-subject.d.ts.map +1 -0
- package/dist/adapter/object3d-authoring-subject.js +25 -0
- package/dist/adapter/root-adapter.d.ts +25 -56
- package/dist/adapter/root-adapter.d.ts.map +1 -1
- package/dist/adapter/root-adapter.js +4 -20
- package/dist/adapter/root-seam-contract.d.ts +129 -0
- package/dist/adapter/root-seam-contract.d.ts.map +1 -0
- package/dist/adapter/root-seam-contract.js +37 -0
- package/dist/adapter/seam-evidence.d.ts +118 -0
- package/dist/adapter/seam-evidence.d.ts.map +1 -0
- package/dist/adapter/seam-evidence.js +231 -0
- package/dist/adapter/setup-three-root-adapter.d.ts +2 -3
- package/dist/adapter/setup-three-root-adapter.d.ts.map +1 -1
- package/dist/adapter/setup-three-root-adapter.js +11 -12
- package/dist/adapter/system-adapter.d.ts +6 -1
- package/dist/adapter/system-adapter.d.ts.map +1 -1
- package/dist/adapter/system-seam-contract.d.ts +354 -0
- package/dist/adapter/system-seam-contract.d.ts.map +1 -0
- package/dist/adapter/system-seam-contract.js +95 -0
- package/dist/animation/xstate-animation-binding.d.ts +6 -0
- package/dist/animation/xstate-animation-binding.d.ts.map +1 -1
- package/dist/animation/xstate-animation-binding.js +142 -4
- package/dist/asset-formats/collider.d.ts +1 -1
- package/dist/asset-formats/mesh.d.ts +2 -2
- package/dist/asset-formats/particles.d.ts +1 -1
- package/dist/canvas-react/engine-bridge.d.ts +6 -6
- package/dist/canvas-react/engine-bridge.d.ts.map +1 -1
- package/dist/canvas-react/engine-bridge.js +2 -2
- package/dist/canvas-react/index.d.ts +2 -2
- package/dist/canvas-react/index.d.ts.map +1 -1
- package/dist/canvas-react/index.js +1 -1
- package/dist/canvas-react/pixi-react-adapter.d.ts +15 -9
- package/dist/canvas-react/pixi-react-adapter.d.ts.map +1 -1
- package/dist/canvas-react/pixi-react-adapter.js +129 -64
- package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -1
- package/dist/canvas-react/pixi-react-root-factory.js +4 -0
- package/dist/canvas-react/world-context.d.ts +22 -12
- package/dist/canvas-react/world-context.d.ts.map +1 -1
- package/dist/canvas-react/world-context.js +12 -51
- package/dist/data/curve.d.ts +1 -1
- package/dist/data/curve.js +2 -2
- package/dist/dev/instruments.d.ts +4 -4
- package/dist/dev/instruments.js +4 -4
- package/dist/dev/register-render-vitals.d.ts +2 -2
- package/dist/dev/register-render-vitals.js +2 -2
- package/dist/dev/static-batch-advisor.d.ts +1 -1
- package/dist/dev/static-batch-advisor.js +2 -2
- package/dist/ecs/user-data.d.ts +5 -0
- package/dist/ecs/user-data.d.ts.map +1 -1
- package/dist/ecs/user-data.js +3 -0
- package/dist/input/input-manager.d.ts +64 -0
- package/dist/input/input-manager.d.ts.map +1 -1
- package/dist/input/input-manager.js +149 -0
- package/dist/manifest/load.d.ts +0 -3
- package/dist/manifest/load.d.ts.map +1 -1
- package/dist/manifest/load.js +0 -4
- package/dist/manifest/schema.d.ts +9 -11
- package/dist/manifest/schema.d.ts.map +1 -1
- package/dist/manifest/schema.js +2 -23
- package/dist/pixi/authoring.d.ts +33 -0
- package/dist/pixi/authoring.d.ts.map +1 -1
- package/dist/pixi/authoring.js +301 -2
- package/dist/pixi/ingest.d.ts +2 -1
- package/dist/pixi/ingest.d.ts.map +1 -1
- package/dist/pixi/ingest.js +4 -2
- package/dist/pixi/scene-capture.d.ts +9 -1
- package/dist/pixi/scene-capture.d.ts.map +1 -1
- package/dist/pixi/scene-capture.js +9 -6
- package/dist/react/world-state.d.ts +12 -0
- package/dist/react/world-state.d.ts.map +1 -1
- package/dist/react/world-state.js +14 -0
- package/dist/render/particles-factory.d.ts +13 -6
- package/dist/render/particles-factory.d.ts.map +1 -1
- package/dist/render/particles-factory.js +6 -3
- package/dist/render/soft-particle-depth.d.ts +3 -3
- package/dist/render/soft-particle-depth.d.ts.map +1 -1
- package/dist/render/soft-particle-depth.js +8 -13
- package/dist/runtime/create-runtime.d.ts +25 -62
- package/dist/runtime/create-runtime.d.ts.map +1 -1
- package/dist/runtime/create-runtime.js +58 -105
- package/dist/runtime/dev-build.d.ts +37 -0
- package/dist/runtime/dev-build.d.ts.map +1 -0
- package/dist/runtime/dev-build.js +49 -0
- package/dist/runtime/game.d.ts +38 -72
- package/dist/runtime/game.d.ts.map +1 -1
- package/dist/runtime/game.js +40 -55
- package/dist/runtime/mount-game.d.ts.map +1 -1
- package/dist/runtime/mount-game.js +12 -1
- package/dist/runtime/mount-manifest.d.ts +0 -13
- package/dist/runtime/mount-manifest.d.ts.map +1 -1
- package/dist/runtime/mount-manifest.js +5 -25
- package/dist/runtime/types.d.ts +18 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/setup/setup-renderer.d.ts +3 -4
- package/dist/setup/setup-renderer.d.ts.map +1 -1
- package/dist/setup/setup-renderer.js +3 -4
- package/dist/world3d-react/engine-bridge.d.ts +1 -1
- package/dist/world3d-react/engine-bridge.d.ts.map +1 -1
- package/dist/world3d-react/engine-bridge.js +29 -4
- package/dist/world3d-react/index.d.ts +1 -1
- package/dist/world3d-react/index.js +1 -1
- package/dist/world3d-react/r3f-adapter.d.ts +7 -0
- package/dist/world3d-react/r3f-adapter.d.ts.map +1 -1
- package/dist/world3d-react/r3f-adapter.js +132 -28
- package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
- package/dist/world3d-react/r3f-root-factory.js +14 -0
- package/dist/world3d-react/rapier-physics-bridge.d.ts +4 -4
- package/dist/world3d-react/rapier-physics-bridge.js +3 -3
- package/dist/world3d-react/world-context.d.ts +52 -3
- package/dist/world3d-react/world-context.d.ts.map +1 -1
- package/dist/world3d-react/world-context.js +94 -64
- package/dist-config/config.js.map +1 -1
- package/package.json +1 -1
- package/schemas/engine-capabilities.json +8 -8
- package/schemas/vgai-project.schema.json +1 -6
- package/src/adapter/adapter-module.ts +405 -72
- package/src/adapter/authoring-seam-contract.ts +211 -0
- package/src/adapter/authoring.ts +242 -50
- package/src/adapter/entrypoint-selection-source.ts +159 -0
- package/src/adapter/finders/scenes-from-entrypoint-selection.ts +162 -36
- package/src/adapter/first-party-systems.ts +17 -3
- package/src/adapter/index.ts +51 -1
- package/src/adapter/ingest/contract-system-adapters.ts +26 -30
- package/src/adapter/ingest/game-contract-seams.ts +85 -0
- package/src/adapter/ingest/game-contract.ts +9 -4
- package/src/adapter/ingest/merge-debug-adapters.ts +193 -0
- package/src/adapter/ingest/observation-debug-adapter.ts +158 -0
- package/src/adapter/ingest/scene-capture.ts +126 -23
- package/src/adapter/ingest/visible-capture-window.ts +63 -9
- package/src/adapter/native-debug-module.ts +307 -0
- package/src/adapter/object3d-authoring-subject.ts +50 -0
- package/src/adapter/root-adapter.ts +25 -56
- package/src/adapter/root-seam-contract.ts +57 -0
- package/src/adapter/seam-evidence.ts +336 -0
- package/src/adapter/setup-three-root-adapter.ts +13 -15
- package/src/adapter/system-adapter.ts +6 -1
- package/src/adapter/system-seam-contract.ts +121 -0
- package/src/animation/xstate-animation-binding.ts +151 -4
- package/src/canvas-react/engine-bridge.ts +6 -6
- package/src/canvas-react/index.ts +2 -1
- package/src/canvas-react/pixi-react-adapter.tsx +143 -77
- package/src/canvas-react/pixi-react-root-factory.tsx +4 -0
- package/src/canvas-react/world-context.ts +26 -101
- package/src/data/curve.ts +2 -2
- package/src/dev/instruments.ts +4 -4
- package/src/dev/register-render-vitals.ts +2 -2
- package/src/dev/static-batch-advisor.ts +2 -2
- package/src/ecs/user-data.ts +5 -0
- package/src/input/input-manager.ts +145 -0
- package/src/manifest/load.ts +0 -7
- package/src/manifest/schema.ts +2 -25
- package/src/pixi/authoring.ts +329 -2
- package/src/pixi/ingest.ts +9 -2
- package/src/pixi/scene-capture.ts +20 -5
- package/src/react/world-state.tsx +15 -0
- package/src/render/particles-factory.ts +16 -6
- package/src/render/soft-particle-depth.ts +8 -13
- package/src/runtime/create-runtime.ts +79 -158
- package/src/runtime/dev-build.ts +49 -0
- package/src/runtime/game.ts +72 -113
- package/src/runtime/mount-game.ts +19 -1
- package/src/runtime/mount-manifest.ts +5 -37
- package/src/runtime/types.ts +22 -0
- package/src/setup/setup-renderer.ts +3 -4
- package/src/world3d-react/engine-bridge.ts +29 -4
- package/src/world3d-react/index.ts +1 -1
- package/src/world3d-react/r3f-adapter.tsx +140 -31
- package/src/world3d-react/r3f-root-factory.tsx +14 -0
- package/src/world3d-react/rapier-physics-bridge.tsx +4 -4
- package/src/world3d-react/world-context.ts +111 -87
- package/dist/runtime/dev-layers.d.ts +0 -34
- package/dist/runtime/dev-layers.d.ts.map +0 -1
- package/dist/runtime/dev-layers.js +0 -46
- package/src/runtime/dev-layers.ts +0 -46
package/src/runtime/game.ts
CHANGED
|
@@ -1,25 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Game root
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* instead of driving its own loop, and in T7.3 slice 2 with a
|
|
7
|
-
* construction-time stage check (symmetric with the three/scene check).
|
|
8
|
-
* T6.2 slice 1 adds real `'react'`-kind
|
|
9
|
-
* `RootInstance` support (`container`, a real `reactRoot()`) — the DOM-root
|
|
10
|
-
* world surface `runtime/create-runtime.ts`'s roots path now mounts, with
|
|
11
|
-
* `physics`/`collisions`/`camera`/`frame` `undefined` for them exactly
|
|
12
|
-
* like an opaque/foreign mount.
|
|
13
|
-
*
|
|
14
|
-
* (D6), particularly §4 (the frame algorithm) and §8 stage 2, for the full
|
|
15
|
-
* target shape. This file holds the Game/ RootInstance shell, the NEW
|
|
16
|
-
* game-scoped `SystemRunner`, and the host-facing `runFrame` frame executor.
|
|
17
|
-
* The public surface is NOT frozen yet — do not add these exports to a
|
|
18
|
-
* barrel/index; only the in-scope runtime/adapter files import this module
|
|
19
|
-
* directly. The `gpu` per-world cache is the one remaining slice-1 omission.
|
|
2
|
+
* The surface-neutral Game root and its declaration-ordered root registry.
|
|
3
|
+
* Three, Canvas, and DOM roots share one game-scoped loop, input manager,
|
|
4
|
+
* system runner, state bridge, debug registry, clock, and seeded RNG. Each
|
|
5
|
+
* RootInstance retains its own mounted surface and optional capabilities.
|
|
20
6
|
*/
|
|
21
7
|
|
|
22
|
-
// TYPE-ONLY pixi import
|
|
8
|
+
// TYPE-ONLY pixi import: `game.ts` must never value-import
|
|
23
9
|
// `pixi.js` — a value import would pull the Pixi runtime into 3D-only
|
|
24
10
|
// bundles that never touch a canvas root. Every pixi-typed field below is erased
|
|
25
11
|
// at compile time; nothing here constructs or calls into Pixi.
|
|
@@ -48,7 +34,6 @@ import type { PhysicsRegistry } from '../physics/physics-registry';
|
|
|
48
34
|
// TYPE-ONLY (same rule as the pixi import above): this lives under `pixi/`,
|
|
49
35
|
// but `game.ts` only ever names its TYPE.
|
|
50
36
|
import type { Physics2DRegistry } from '../pixi/physics-registry';
|
|
51
|
-
import type { AudioContext as GameAudio } from '../setup/setup-audio';
|
|
52
37
|
import {
|
|
53
38
|
createDebugRegistry,
|
|
54
39
|
DebugError,
|
|
@@ -89,10 +74,7 @@ const DISPLAY_RATE_PHASES: readonly SystemPhaseName[] = PHASE_ORDER.filter(
|
|
|
89
74
|
* `runFrame`. `GameLoop.timeScale` (unaffected by this) remains the orthogonal
|
|
90
75
|
* "speed up/slow down" axis — pausing never touches it, so the host's
|
|
91
76
|
* accumulator/rAF loop keeps ticking at its normal cadence, which is what makes
|
|
92
|
-
* "paused still renders" possible
|
|
93
|
-
* implementation set `loop.timeScale = 0`, which starves the accumulator — and,
|
|
94
|
-
* before WO-P13, stopped rendering with it; that was the bug this control
|
|
95
|
-
* surface replaces, not a compatible behavior to keep).
|
|
77
|
+
* "paused still renders" possible.
|
|
96
78
|
*/
|
|
97
79
|
export interface PlayState {
|
|
98
80
|
/** Whether the game is currently paused (game-level — see the per-world
|
|
@@ -131,11 +113,9 @@ export interface PlayState {
|
|
|
131
113
|
|
|
132
114
|
/**
|
|
133
115
|
* The kinds of render surface a world can be. `'three'` and `'canvas'`
|
|
134
|
-
* roots are
|
|
135
|
-
* here so the type is already shaped for T7.4 and no caller has to widen a
|
|
136
|
-
* union later.
|
|
116
|
+
* roots are canvas-backed; `'dom'` roots own a DOM layer.
|
|
137
117
|
*
|
|
138
|
-
* Re-exported from `adapter/adapter-surface.ts`
|
|
118
|
+
* Re-exported from `adapter/adapter-surface.ts` so
|
|
139
119
|
* `adapter/root-adapter.ts`'s kind-tagged `MountedRoot` types can name it
|
|
140
120
|
* without an import cycle back to this file. This re-export keeps every
|
|
141
121
|
* existing `import type { AdapterSurface } from '../runtime/game'` call site
|
|
@@ -144,7 +124,7 @@ export interface PlayState {
|
|
|
144
124
|
export type AdapterSurface = AdapterSurfaceLeaf;
|
|
145
125
|
|
|
146
126
|
/**
|
|
147
|
-
* A world's per-phase frame hooks
|
|
127
|
+
* A world's per-phase frame hooks.
|
|
148
128
|
* Populated on a `RootInstance` only for first-party mounts — an opaque/
|
|
149
129
|
* foreign mount has no phase-partitioned entry point, so it stays
|
|
150
130
|
* `undefined` and the Game's frame executor (`GameInternal.runFrame`) falls
|
|
@@ -153,7 +133,7 @@ export type AdapterSurface = AdapterSurfaceLeaf;
|
|
|
153
133
|
export interface RootFrameHooks {
|
|
154
134
|
/** Run this world's engine systems + component ticks + world-bound game
|
|
155
135
|
* systems for one phase. For a first-party world this delegates to the
|
|
156
|
-
*
|
|
136
|
+
* same `SystemRunner.runPhase` as its direct `mounted.update` entry. */
|
|
157
137
|
runPhase(phase: SystemPhaseName, dt: number): void;
|
|
158
138
|
/** Run once per substep, after ALL phases have run for ALL roots this
|
|
159
139
|
* substep (mirrors where `mounted.update`'s post-`systems.run` work sat
|
|
@@ -164,14 +144,12 @@ export interface RootFrameHooks {
|
|
|
164
144
|
|
|
165
145
|
/**
|
|
166
146
|
* A single world instance: the unit of adaptation.
|
|
167
|
-
* Slice-1 subset — `gpu` (per-world GPU resource cache, §6.2) is omitted
|
|
168
|
-
* until the slice that builds it.
|
|
169
147
|
*/
|
|
170
148
|
export interface RootInstance {
|
|
171
|
-
/** Manifest id
|
|
149
|
+
/** Manifest root id. */
|
|
172
150
|
readonly id: string;
|
|
173
151
|
readonly kind: AdapterSurface;
|
|
174
|
-
/** Per-world play/pause semantics
|
|
152
|
+
/** Per-world play/pause semantics. */
|
|
175
153
|
readonly pausable: boolean;
|
|
176
154
|
/** The RootAdapter that produced `mounted` — first-party or external.
|
|
177
155
|
* Deliberately narrower than `RootAdapter<K>` (T7.5): this field is only
|
|
@@ -195,7 +173,7 @@ export interface RootInstance {
|
|
|
195
173
|
* descriptively for a non-pixijs world, or a canvas world built without
|
|
196
174
|
* a `stage` (see `RootInstanceInit.stage`). */
|
|
197
175
|
pixiStage(): PIXI.Container;
|
|
198
|
-
/** Kind-narrowed accessor for
|
|
176
|
+
/** Kind-narrowed accessor for DOM roots: returns the
|
|
199
177
|
* DOM-root layer `<div>` the host mounted this world's react tree into
|
|
200
178
|
* (the SAME element passed as `container` to `createRootInstance` —
|
|
201
179
|
* identity matters, mirroring `threeScene()`/`pixiStage()`'s "same
|
|
@@ -214,7 +192,7 @@ export interface RootInstance {
|
|
|
214
192
|
readonly physics2d?: Physics2DRegistry | undefined;
|
|
215
193
|
/** Kind-typed via `mounted` in T7.5; `unknown` here deliberately. */
|
|
216
194
|
readonly camera?: unknown;
|
|
217
|
-
/** Phase-partitioned frame entry point
|
|
195
|
+
/** Phase-partitioned frame entry point — present only for
|
|
218
196
|
* first-party mounts. `undefined` for an opaque/foreign mount, which
|
|
219
197
|
* `GameInternal.runFrame` drives via its single `mounted.update` call
|
|
220
198
|
* instead (unless it `drivesOwnLoop`, in which case it isn't ticked at
|
|
@@ -399,8 +377,8 @@ export function createRootInstance(init: RootInstanceInit): RootInstance {
|
|
|
399
377
|
/**
|
|
400
378
|
* Type guard for whether a `MountedThreeRoot` is a first-party
|
|
401
379
|
* `SetupThreeRootAdapter` mount (has a live `GameContext` at `.ctx`). Used to
|
|
402
|
-
* decide whether a world's `physics`/`collisions`/`camera`
|
|
403
|
-
*
|
|
380
|
+
* decide whether a world's `physics`/`collisions`/`camera` can be populated
|
|
381
|
+
* from it — an external
|
|
404
382
|
* adapter's mount has none of these first-party handles.
|
|
405
383
|
*
|
|
406
384
|
* Checks the `firstParty: true` brand (checklist item 1), NOT `'ctx' in
|
|
@@ -505,13 +483,9 @@ function debrisOwnersOf(world: RootInstance): DebrisOwner[] {
|
|
|
505
483
|
}
|
|
506
484
|
|
|
507
485
|
/**
|
|
508
|
-
* The Game root. Owns the
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
* additions: they delegate to the default world's first-party mount so
|
|
512
|
-
* existing single-world call sites keep working; hoisting them to true
|
|
513
|
-
* Game ownership (the target shape) is a later slice's
|
|
514
|
-
* work, not this one's.
|
|
486
|
+
* The Game root. Owns the loop, raw-asset cache, world registry, input,
|
|
487
|
+
* and game-scoped `SystemRunner`. Surface-specific capabilities remain on
|
|
488
|
+
* their mounted roots or in the aggregated `SystemAdapters` contract.
|
|
515
489
|
*/
|
|
516
490
|
export interface Game {
|
|
517
491
|
readonly loop: GameLoop;
|
|
@@ -521,18 +495,16 @@ export interface Game {
|
|
|
521
495
|
/** Per-game diagnostic store. Disabled by default; the editor enables it on demand. */
|
|
522
496
|
readonly profiler: PerformanceProfiler;
|
|
523
497
|
/**
|
|
524
|
-
* The game-scoped `SystemRunner
|
|
525
|
-
*
|
|
498
|
+
* The game-scoped `SystemRunner`, separate from any world's own runner.
|
|
499
|
+
* Within each phase,
|
|
526
500
|
* `GameInternal.runFrame` runs THIS runner's `runPhase` first, before any
|
|
527
501
|
* world's engine systems/component ticks/world-bound game systems (e.g.
|
|
528
502
|
* `ctx.systems.add`, which stays world-bound to the default world — see
|
|
529
503
|
* `runtime/types.ts`). Empty for every existing game (nothing registers
|
|
530
|
-
* against it
|
|
531
|
-
* unchanged by its presence.
|
|
504
|
+
* against it), so an empty runner has no frame cost beyond dispatch.
|
|
532
505
|
*/
|
|
533
506
|
readonly systems: SystemRunner;
|
|
534
|
-
/** Declaration-ordered.
|
|
535
|
-
* three world) — this is the SAME array reference `registerRoot`
|
|
507
|
+
/** Declaration-ordered. This is the same array reference `registerRoot`
|
|
536
508
|
* mutates, not a snapshot, so holders (e.g. `GameContext.roots`) observe
|
|
537
509
|
* later registrations. */
|
|
538
510
|
readonly roots: ReadonlyArray<RootInstance>;
|
|
@@ -568,9 +540,6 @@ export interface Game {
|
|
|
568
540
|
subscribeSystemAdapters?(listener: () => void): () => void;
|
|
569
541
|
/** The one game-owned `InputManager`, shared by every first-party root. */
|
|
570
542
|
readonly input: InputManager;
|
|
571
|
-
/** Delegates to the default world's first-party audio context. Throws
|
|
572
|
-
* when the default world is not a first-party mount. */
|
|
573
|
-
readonly audio: GameAudio;
|
|
574
543
|
/**
|
|
575
544
|
* Frame-versioned state bridge (T7.4 slice 1). Bumped once per completed
|
|
576
545
|
* `runFrame`, after all phases of all roots and all `endFrame` hooks (see
|
|
@@ -653,28 +622,18 @@ export interface GameInternal extends Game {
|
|
|
653
622
|
* else: world.mounted.update?.(dt) // opaque world fallback
|
|
654
623
|
* ```
|
|
655
624
|
*
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
*
|
|
659
|
-
* `update`
|
|
660
|
-
* `drivesOwnLoop` world is never ticked here at all — matching its
|
|
661
|
-
* exclusion from the legacy `!mountedRef.drivesOwnLoop` guard. An opaque
|
|
662
|
-
* host-driven world (no `frame`) gets exactly one `update(dt)` call per
|
|
663
|
-
* substep, after the phase loop — unchanged cadence from today.
|
|
625
|
+
* A single first-party world and its direct `mounted.update(dt)` entry run
|
|
626
|
+
* the same `SystemRunner` and `postFrame` work. A `drivesOwnLoop` world is
|
|
627
|
+
* never ticked here. An opaque host-driven world (no `frame`) gets exactly
|
|
628
|
+
* one `update(dt)` call per substep, after the phase loop.
|
|
664
629
|
*
|
|
665
630
|
* D10/T7.6 play-state addendum: when `Game.play.paused` is true, every
|
|
666
631
|
* `pausable` (and non-`drivesOwnLoop`) world skips every phase EXCEPT
|
|
667
632
|
* `render` (still called, every substep, with `dt` forced to `0`) and skips
|
|
668
633
|
* its `endFrame`/opaque-`update` call entirely — a `pausable: false` world
|
|
669
|
-
* is completely unaffected. `
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
* none) but is no longer how `Game.play.step()` works (§7.1-2 fix,
|
|
673
|
-
* probe4: the old `ignorePause` full-frame re-run double-ticked every
|
|
674
|
-
* already-running `pausable: false` world, since D10's ordinary loop never
|
|
675
|
-
* stops ticking them). `Game.play.step()` now drives this function via the
|
|
676
|
-
* internal-only `onlyFrozen` mode instead (see `runFrameImpl` — not part of
|
|
677
|
-
* this public, host-facing signature): it ticks EXACTLY the currently
|
|
634
|
+
* is completely unaffected. `Game.play.step()` drives this function via the
|
|
635
|
+
* internal-only `onlyFrozen` mode (see `runFrameImpl` — not part of this
|
|
636
|
+
* public, host-facing signature): it ticks exactly the currently
|
|
678
637
|
* frozen set (host-driven, `pausable`, and `paused`) through every phase +
|
|
679
638
|
* `endFrame` with the real `dt` (not the render-phase's forced `0`), and
|
|
680
639
|
* touches no other world at all — a natural no-op while not paused, since
|
|
@@ -690,7 +649,7 @@ export interface GameInternal extends Game {
|
|
|
690
649
|
* calls `runFrame(fixedDt)` with no opts) is frame-exact and unchanged.
|
|
691
650
|
* `runTicks` sets it per tick for its `render: 'none' | 'last'` modes.
|
|
692
651
|
*/
|
|
693
|
-
runFrame(dt: number, opts?: {
|
|
652
|
+
runFrame(dt: number, opts?: { skipRenderPhases?: boolean }): void;
|
|
694
653
|
/**
|
|
695
654
|
* WO-P13 — run ONE display frame's presentation pass: the registered
|
|
696
655
|
* `onRenderStep` callbacks, then the `preRender` and `render` phases across
|
|
@@ -775,20 +734,18 @@ export interface GameInternal extends Game {
|
|
|
775
734
|
* the conventional `/inputmaps/default.inputmap.json` (or the adapter's
|
|
776
735
|
* configured `inputMapPath`); runtime additions go through
|
|
777
736
|
* `ctx.input.registerAction` instead.
|
|
737
|
+
*
|
|
738
|
+
* `{ optional: true }` is the CONVENTIONAL-PATH probe a root uses when the
|
|
739
|
+
* project never named a map: an absent file resolves quietly (a brand-new
|
|
740
|
+
* scaffold declares no actions yet), pins nothing — so a sibling root may
|
|
741
|
+
* still declare its own map — and yields to a map another root already
|
|
742
|
+
* owns. A map that exists but fails to parse is as loud either way.
|
|
778
743
|
*/
|
|
779
|
-
loadInputMap(path: string): Promise<void>;
|
|
744
|
+
loadInputMap(path: string, options?: { optional?: boolean }): Promise<void>;
|
|
780
745
|
/** Release game-owned resources after every mounted root has disposed. */
|
|
781
746
|
dispose(): void;
|
|
782
747
|
}
|
|
783
748
|
|
|
784
|
-
function describeMismatch(handle: 'input' | 'audio', world: RootInstance): string {
|
|
785
|
-
return (
|
|
786
|
-
`Game.${handle}: default world "${world.id}" (kind: ${world.kind}) is not a first-party ` +
|
|
787
|
-
"mount — hoisting these to true Game ownership is a later slice's work; available only " +
|
|
788
|
-
'via a first-party default world today'
|
|
789
|
-
);
|
|
790
|
-
}
|
|
791
|
-
|
|
792
749
|
/**
|
|
793
750
|
* Construct the (host-internal) Game shell. Callers: `createGameRuntime`
|
|
794
751
|
* builds this BEFORE mounting its one adapter, then registers the default
|
|
@@ -879,7 +836,7 @@ export function createGame(opts: {
|
|
|
879
836
|
getTick: () => tick,
|
|
880
837
|
getSimT: () => simT,
|
|
881
838
|
// D15/T-D15.5 — "the manifest's first/default world" for the debug
|
|
882
|
-
// registry's world-addressed input-target
|
|
839
|
+
// registry's world-addressed input-target surface. Every
|
|
883
840
|
// first-party root now registers the SAME game-owned InputManager, but the
|
|
884
841
|
// stable default id still keeps explicit/implicit debug routing coherent.
|
|
885
842
|
// Use the SAME "first three world, else first world" rule
|
|
@@ -905,6 +862,10 @@ export function createGame(opts: {
|
|
|
905
862
|
input.setDebugEmit((event, detail) => debugRegistry.forRoot('(game)').emit(event, detail));
|
|
906
863
|
let inputMapPath: string | null = null;
|
|
907
864
|
let inputMapLoad: Promise<void> | null = null;
|
|
865
|
+
/** In-flight/settled conventional-path probes (`loadInputMap(path, {optional})`),
|
|
866
|
+
* so two roots probing the same path share one fetch. Separate from
|
|
867
|
+
* `inputMapLoad` because a probe that found nothing declares nothing. */
|
|
868
|
+
const inputMapProbes = new Map<string, Promise<void>>();
|
|
908
869
|
|
|
909
870
|
function requireDefaultRoot(): RootInstance {
|
|
910
871
|
if (roots.length === 0) {
|
|
@@ -913,14 +874,6 @@ export function createGame(opts: {
|
|
|
913
874
|
return roots.find((w) => w.kind === 'three') ?? roots[0]!;
|
|
914
875
|
}
|
|
915
876
|
|
|
916
|
-
function requireFirstPartyCtx(handle: 'input' | 'audio') {
|
|
917
|
-
const world = requireDefaultRoot();
|
|
918
|
-
if (!isFirstPartyMounted(world.mounted)) {
|
|
919
|
-
throw new Error(describeMismatch(handle, world));
|
|
920
|
-
}
|
|
921
|
-
return world.mounted.ctx;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
877
|
// --- Game.systemAdapters aggregation (§7.1-3, probe1) --------------------
|
|
925
878
|
// Warn-once-per-colliding-key state, scoped to this Game instance (a fresh
|
|
926
879
|
// Game gets a fresh warn history) — deliberately NOT reset by anything
|
|
@@ -1023,7 +976,7 @@ export function createGame(opts: {
|
|
|
1023
976
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: the frame algorithm (now with D10's per-world pause gate + the onlyFrozen step()-only mode) is one cohesive nested loop over phases/roots — splitting it would obscure the ordering contract documented on GameInternal.runFrame
|
|
1024
977
|
function runFrameImpl(
|
|
1025
978
|
dt: number,
|
|
1026
|
-
frameOpts?: {
|
|
979
|
+
frameOpts?: { onlyFrozen?: boolean; skipRenderPhases?: boolean },
|
|
1027
980
|
): void {
|
|
1028
981
|
// D15 (T-D15.3) — brackets the ENTIRE frame body (every phase, every
|
|
1029
982
|
// world, both the `onlyFrozen` and normal branches below converge on the
|
|
@@ -1033,7 +986,6 @@ export function createGame(opts: {
|
|
|
1033
986
|
// declare `determinism.seededRandom`).
|
|
1034
987
|
if (rngTrapEnabled) rngTrap.enable();
|
|
1035
988
|
profiler.beginFrame();
|
|
1036
|
-
const ignorePause = frameOpts?.ignorePause ?? false;
|
|
1037
989
|
// D15/T-D15.4 + WO-P13: the render-phase skip. Two callers set it, for the
|
|
1038
990
|
// same reason — this substep is not the thing that paints. `runTicks`'s
|
|
1039
991
|
// `render: 'none'|'last'` fast-forward sets it on every tick it doesn't
|
|
@@ -1048,9 +1000,6 @@ export function createGame(opts: {
|
|
|
1048
1000
|
// one and only caller. It ticks EXACTLY the currently-frozen set (host-driven,
|
|
1049
1001
|
// `pausable`, and `paused`) through every phase + `endFrame`, with the REAL
|
|
1050
1002
|
// `dt` (not the render-phase's forced `0`), and touches no other world at all
|
|
1051
|
-
// — §7.1-2 fix, probe4: the previous `ignorePause` full-frame re-run ticked
|
|
1052
|
-
// EVERY host-driven world, double-ticking every already-running
|
|
1053
|
-
// `pausable: false` world (D10's ordinary loop never stops ticking them).
|
|
1054
1003
|
const onlyFrozen = frameOpts?.onlyFrozen ?? false;
|
|
1055
1004
|
// Checklist item 7: snapshot the world count ONCE at entry and iterate
|
|
1056
1005
|
// by index in both loops below. A world registered mid-frame (e.g. from
|
|
@@ -1068,10 +1017,9 @@ export function createGame(opts: {
|
|
|
1068
1017
|
break;
|
|
1069
1018
|
}
|
|
1070
1019
|
} else if (
|
|
1071
|
-
|
|
1072
|
-
(world.mounted.drivesOwnLoop
|
|
1020
|
+
world.mounted.drivesOwnLoop
|
|
1073
1021
|
? !paused || !world.pausable || !world.mounted.setPaused
|
|
1074
|
-
: !paused || !world.pausable
|
|
1022
|
+
: !paused || !world.pausable
|
|
1075
1023
|
) {
|
|
1076
1024
|
inputFrameActive = true;
|
|
1077
1025
|
break;
|
|
@@ -1169,13 +1117,12 @@ export function createGame(opts: {
|
|
|
1169
1117
|
for (let i = 0; i < n; i++) {
|
|
1170
1118
|
const world = roots[i]!;
|
|
1171
1119
|
if (world.mounted.drivesOwnLoop) continue;
|
|
1172
|
-
// D10/T7.6: a `pausable` world under an active
|
|
1120
|
+
// D10/T7.6: a `pausable` world under an active pause
|
|
1173
1121
|
// skips every phase except `render` — its render still runs, every
|
|
1174
1122
|
// substep, but with `dt` forced to `0` (deterministic: no
|
|
1175
1123
|
// time-based render effect silently keeps animating a "frozen"
|
|
1176
|
-
// scene). A `pausable: false` world
|
|
1177
|
-
|
|
1178
|
-
const frozen = !ignorePause && paused && world.pausable;
|
|
1124
|
+
// scene). A `pausable: false` world is unaffected.
|
|
1125
|
+
const frozen = paused && world.pausable;
|
|
1179
1126
|
if (frozen && phase !== SystemPhase.RENDER) continue;
|
|
1180
1127
|
const phaseDt = frozen ? 0 : dt;
|
|
1181
1128
|
// Checklist item 2: isolate each world's per-phase work — one
|
|
@@ -1199,7 +1146,7 @@ export function createGame(opts: {
|
|
|
1199
1146
|
if (world.mounted.drivesOwnLoop) continue;
|
|
1200
1147
|
// A fully-frozen world gets no `endFrame`/opaque-`update` call either —
|
|
1201
1148
|
// there is nothing to "end the frame" of when nothing ran this substep.
|
|
1202
|
-
const frozen =
|
|
1149
|
+
const frozen = paused && world.pausable;
|
|
1203
1150
|
if (frozen) continue;
|
|
1204
1151
|
advanced = true;
|
|
1205
1152
|
if (world.frame) {
|
|
@@ -1346,9 +1293,6 @@ export function createGame(opts: {
|
|
|
1346
1293
|
for (const listener of systemAdapterListeners) listener();
|
|
1347
1294
|
},
|
|
1348
1295
|
input,
|
|
1349
|
-
get audio() {
|
|
1350
|
-
return requireFirstPartyCtx('audio').audio;
|
|
1351
|
-
},
|
|
1352
1296
|
state: stateBridge,
|
|
1353
1297
|
play: {
|
|
1354
1298
|
get paused() {
|
|
@@ -1387,11 +1331,7 @@ export function createGame(opts: {
|
|
|
1387
1331
|
}
|
|
1388
1332
|
}
|
|
1389
1333
|
}
|
|
1390
|
-
//
|
|
1391
|
-
// paused) set — not `{ ignorePause: true }`, which re-ran a FULL
|
|
1392
|
-
// extra frame for every host-driven world (including already-running
|
|
1393
|
-
// `pausable: false` ones — a double-tick, since D10's ordinary loop
|
|
1394
|
-
// never stops ticking them). See `runFrameImpl`'s `onlyFrozen` mode.
|
|
1334
|
+
// Tick exactly the frozen (host-driven, pausable, paused) set.
|
|
1395
1335
|
runFrameImpl(dt, { onlyFrozen: true });
|
|
1396
1336
|
},
|
|
1397
1337
|
},
|
|
@@ -1486,7 +1426,7 @@ export function createGame(opts: {
|
|
|
1486
1426
|
);
|
|
1487
1427
|
}
|
|
1488
1428
|
},
|
|
1489
|
-
runFrame(dt: number, frameOpts?: {
|
|
1429
|
+
runFrame(dt: number, frameOpts?: { skipRenderPhases?: boolean }): void {
|
|
1490
1430
|
runFrameImpl(dt, frameOpts);
|
|
1491
1431
|
},
|
|
1492
1432
|
runRenderFrame(alpha: number, displayDt: number): void {
|
|
@@ -1538,7 +1478,26 @@ export function createGame(opts: {
|
|
|
1538
1478
|
runFrameImpl(fixedDt, { skipRenderPhases });
|
|
1539
1479
|
}
|
|
1540
1480
|
},
|
|
1541
|
-
loadInputMap(path: string): Promise<void> {
|
|
1481
|
+
loadInputMap(path: string, options?: { optional?: boolean }): Promise<void> {
|
|
1482
|
+
if (options?.optional === true) {
|
|
1483
|
+
// The conventional-path probe. It never becomes the game's declared
|
|
1484
|
+
// map unless a file was actually there, so a root that DOES name a
|
|
1485
|
+
// map (a sibling canvas root, a later mount) is never pre-empted by a
|
|
1486
|
+
// probe that found nothing — the competing-path throw below would
|
|
1487
|
+
// otherwise fire on a project that declared exactly one map.
|
|
1488
|
+
if (inputMapPath) return inputMapLoad ?? Promise.resolve();
|
|
1489
|
+
let probe = inputMapProbes.get(path);
|
|
1490
|
+
if (!probe) {
|
|
1491
|
+
probe = input.loadMapIfPresent(path).then((loaded) => {
|
|
1492
|
+
if (loaded && !inputMapPath) {
|
|
1493
|
+
inputMapPath = path;
|
|
1494
|
+
inputMapLoad = Promise.resolve();
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1497
|
+
inputMapProbes.set(path, probe);
|
|
1498
|
+
}
|
|
1499
|
+
return probe;
|
|
1500
|
+
}
|
|
1542
1501
|
if (inputMapPath && inputMapPath !== path) {
|
|
1543
1502
|
throw new Error(
|
|
1544
1503
|
`Game.loadInputMap: input map is already "${inputMapPath}"; root requested "${path}". ` +
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
// even the 'three' convenience export is a plain function the CALLER
|
|
18
18
|
// registers, not something this module wires up automatically.
|
|
19
19
|
|
|
20
|
+
import {
|
|
21
|
+
installNativeDebugBindings,
|
|
22
|
+
type NativeDebugBinding,
|
|
23
|
+
nativeDebugBindingFromEntryModule,
|
|
24
|
+
} from '../adapter/native-debug-module';
|
|
20
25
|
import type { RootAdapter } from '../adapter/root-adapter';
|
|
21
26
|
import type { ResolvedAdapterRoot } from '../manifest/load';
|
|
22
27
|
import type { GameSession } from './create-runtime';
|
|
@@ -209,6 +214,15 @@ function worldIsSelfSufficient(_world: ResolvedAdapterRoot): boolean {
|
|
|
209
214
|
return false;
|
|
210
215
|
}
|
|
211
216
|
|
|
217
|
+
function nativeDebugForEntry(
|
|
218
|
+
world: ResolvedAdapterRoot,
|
|
219
|
+
entryModule: unknown,
|
|
220
|
+
): NativeDebugBinding[] {
|
|
221
|
+
if (entryModule === undefined || world.adapter.type !== 'builtin') return [];
|
|
222
|
+
const binding = nativeDebugBindingFromEntryModule(world.id, entryModule);
|
|
223
|
+
return binding ? [binding] : [];
|
|
224
|
+
}
|
|
225
|
+
|
|
212
226
|
/**
|
|
213
227
|
* Mount every world declared by a `vgai.project.json` manifest onto `host
|
|
214
228
|
* .container`, resolving each world's kind through the `registerAdapter`
|
|
@@ -240,6 +254,7 @@ export async function mountGameFromManifest(
|
|
|
240
254
|
assertExportedOrInEditor();
|
|
241
255
|
const manifest = resolveManifest(manifestInput);
|
|
242
256
|
const entries: Record<string, MountEntry> = { ...opts.entries };
|
|
257
|
+
const nativeDebug: NativeDebugBinding[] = [];
|
|
243
258
|
|
|
244
259
|
for (const world of manifest.roots) {
|
|
245
260
|
if (entries[world.id] !== undefined) continue; // explicit entry always wins
|
|
@@ -250,6 +265,7 @@ export async function mountGameFromManifest(
|
|
|
250
265
|
world.entry !== undefined && host.loadEntryModule
|
|
251
266
|
? await host.loadEntryModule(world.entry)
|
|
252
267
|
: undefined;
|
|
268
|
+
nativeDebug.push(...nativeDebugForEntry(world, entryModule));
|
|
253
269
|
entries[world.id] = await factory(world, { host, entryModule });
|
|
254
270
|
continue;
|
|
255
271
|
}
|
|
@@ -265,7 +281,7 @@ export async function mountGameFromManifest(
|
|
|
265
281
|
);
|
|
266
282
|
}
|
|
267
283
|
|
|
268
|
-
|
|
284
|
+
const session = await mountManifestRoots({
|
|
269
285
|
manifest,
|
|
270
286
|
container: host.container,
|
|
271
287
|
entries,
|
|
@@ -273,4 +289,6 @@ export async function mountGameFromManifest(
|
|
|
273
289
|
height: host.height,
|
|
274
290
|
headless: host.headless,
|
|
275
291
|
});
|
|
292
|
+
installNativeDebugBindings(session.game, nativeDebug);
|
|
293
|
+
return session;
|
|
276
294
|
}
|
|
@@ -20,8 +20,7 @@
|
|
|
20
20
|
// `entries[id].adapter` (the caller's own module graph built it). Threejs is
|
|
21
21
|
// different: `three`/
|
|
22
22
|
// `SetupThreeRootAdapter` are already unconditional dependencies of every
|
|
23
|
-
// caller of `createGameRuntime
|
|
24
|
-
// immediately below in this directory), so wrapping a `setup` with
|
|
23
|
+
// caller of `createGameRuntime`, so wrapping a `setup` with
|
|
25
24
|
// `fromSetup` here adds no NEW bundle weight — only pixi/react are avoided.
|
|
26
25
|
|
|
27
26
|
import { assertNever } from '../adapter/adapter-surface';
|
|
@@ -37,7 +36,6 @@ import {
|
|
|
37
36
|
import { createGameRuntime, type GameSession, type RootMountSpec } from './create-runtime';
|
|
38
37
|
import { type DebugBridgeWindowTarget, maybeInstallDebugBridge } from './debug-bridge';
|
|
39
38
|
import { getDebugRegistry } from './debug-registry';
|
|
40
|
-
import { devLayersEnabled } from './dev-layers';
|
|
41
39
|
import { getGameplayRngTrapControl } from './gameplay-rng-trap';
|
|
42
40
|
import { RENDER_SEED_QUERY_PARAM } from './render-seed';
|
|
43
41
|
import type { GameSetupFn, PlaytestContext } from './types';
|
|
@@ -157,15 +155,6 @@ export interface MountManifestOptions {
|
|
|
157
155
|
readonly seed?: number | undefined;
|
|
158
156
|
/** Host identity for a private play run or coordinated Team Test. */
|
|
159
157
|
readonly playtest?: PlaytestContext | null | undefined;
|
|
160
|
-
/**
|
|
161
|
-
* The explicit per-mount answer to "are dev layers enabled" — forwarded
|
|
162
|
-
* verbatim to `devLayersEnabled` (`runtime/dev-layers.ts`), the ONE owner of
|
|
163
|
-
* that question. Omit it and the build decides (dev build ⇒ on; production
|
|
164
|
-
* build ⇒ off unless `VITE_VGAI_DEV_LAYERS=true`). A root declaring
|
|
165
|
-
* `dev: true` is simply not mounted when the answer is `false`, so a shipped
|
|
166
|
-
* build carries no dev GUI at all.
|
|
167
|
-
*/
|
|
168
|
-
readonly devLayers?: boolean | undefined;
|
|
169
158
|
/** Where to read `?vgai-seed=` from for the boot-time seed reader. Same
|
|
170
159
|
* override precedent as `debugBridge.url` (defaults to `window.location`
|
|
171
160
|
* when a real `window` exists; a headless caller with no override gets no
|
|
@@ -275,8 +264,9 @@ function resolveThreeAdapter(
|
|
|
275
264
|
if (world.adapter.type === 'ingest') {
|
|
276
265
|
throw new Error(
|
|
277
266
|
`mountManifestRoots: world "${world.id}" (three) declares an { ingest } adapter — ` +
|
|
278
|
-
"ingest roots require the editor's dev-server-backed mount machinery (an EditorStore " +
|
|
279
|
-
|
|
267
|
+
"captured ingest roots require the editor's dev-server-backed mount machinery (an EditorStore " +
|
|
268
|
+
'plus in-realm scene capture, see resolveIngestDescriptor + ' +
|
|
269
|
+
'mountThreeIngestRootFromManifest) and are ' +
|
|
280
270
|
'not supported by mountManifestRoots (no porting aids, no hosted ingest routes).',
|
|
281
271
|
);
|
|
282
272
|
}
|
|
@@ -346,8 +336,6 @@ function buildRootMountSpec(
|
|
|
346
336
|
id: world.id,
|
|
347
337
|
zOrder: world.zOrder,
|
|
348
338
|
pausable: world.pausable,
|
|
349
|
-
dev: world.dev,
|
|
350
|
-
loop: world.loop,
|
|
351
339
|
};
|
|
352
340
|
|
|
353
341
|
// `entry`'s `kind` is guaranteed to agree with `world.kind` past the guard
|
|
@@ -391,10 +379,6 @@ function buildRootMountSpec(
|
|
|
391
379
|
* ingest }` adapter with no caller-supplied adapter, or an empty manifest all
|
|
392
380
|
* throw a named `Error` identifying the world id, its kind, and what was
|
|
393
381
|
* expected — never a silent skip or a partially-mounted session.
|
|
394
|
-
*
|
|
395
|
-
* The ONE root that IS skipped silently is a `dev: true` dev layer when dev
|
|
396
|
-
* layers are disabled (`devLayersEnabled`) — that skip is the feature: it is
|
|
397
|
-
* what makes a dev GUI safe to leave in the game.
|
|
398
382
|
*/
|
|
399
383
|
export async function mountManifestRoots(opts: MountManifestOptions): Promise<GameSession> {
|
|
400
384
|
const manifest = resolveManifest(opts.manifest);
|
|
@@ -403,23 +387,7 @@ export async function mountManifestRoots(opts: MountManifestOptions): Promise<Ga
|
|
|
403
387
|
}
|
|
404
388
|
const entries = opts.entries ?? {};
|
|
405
389
|
|
|
406
|
-
|
|
407
|
-
// only when dev layers are enabled, so a production build carries no dev
|
|
408
|
-
// GUI. The answer comes from `devLayersEnabled` and nowhere else — see that
|
|
409
|
-
// function's header for why there is exactly one owner. Dropping the root
|
|
410
|
-
// here (rather than mounting it hidden) is deliberate: nothing of the dev
|
|
411
|
-
// layer is constructed, so its entry module's cost and its DOM never exist
|
|
412
|
-
// in a shipped build.
|
|
413
|
-
const devEnabled = devLayersEnabled(opts.devLayers);
|
|
414
|
-
const mountable = devEnabled ? manifest.roots : manifest.roots.filter((world) => !world.dev);
|
|
415
|
-
if (mountable.length === 0) {
|
|
416
|
-
throw new Error(
|
|
417
|
-
'mountManifestRoots: every root in this manifest is a dev layer (`dev: true`) and dev ' +
|
|
418
|
-
'layers are disabled for this build — there is no game to mount.',
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const roots: RootMountSpec[] = mountable.map((world) =>
|
|
390
|
+
const roots: RootMountSpec[] = manifest.roots.map((world) =>
|
|
423
391
|
buildRootMountSpec(world, entries[world.id]),
|
|
424
392
|
);
|
|
425
393
|
|
package/src/runtime/types.ts
CHANGED
|
@@ -259,6 +259,28 @@ export interface GameContext {
|
|
|
259
259
|
playtest?: PlaytestContext | null | undefined;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Services shared by component-authored roots, independent of their render
|
|
264
|
+
* surface. This is deliberately an explicit pick from {@link GameContext}:
|
|
265
|
+
* adding a subsystem to the imperative Three setup context never silently
|
|
266
|
+
* exposes it to R3F or Canvas components.
|
|
267
|
+
*
|
|
268
|
+
* `input` is nullable because it belongs to the optional `Game` host. Normal
|
|
269
|
+
* vgai project mounts always provide it; bare harnesses and foreign hosts do
|
|
270
|
+
* not. Component code therefore sees the absence in its type instead of a
|
|
271
|
+
* getter that first throws from inside a frame callback.
|
|
272
|
+
*
|
|
273
|
+
* `clock` is surface-specific and intentionally absent here. The imperative
|
|
274
|
+
* and R3F clocks can dispose `THREE.Object3D` debris; a Canvas context narrows
|
|
275
|
+
* that Three-only operation away.
|
|
276
|
+
*/
|
|
277
|
+
export type ComponentContextBase = Pick<
|
|
278
|
+
GameContext,
|
|
279
|
+
'systems' | 'debug' | 'random' | 'game' | 'roots' | 'playtest' | 'registerSystemAdapter'
|
|
280
|
+
> & {
|
|
281
|
+
readonly input: InputManager | null;
|
|
282
|
+
};
|
|
283
|
+
|
|
262
284
|
/** Return value from a game setup function. */
|
|
263
285
|
export interface GameCleanup {
|
|
264
286
|
dispose: () => void;
|
|
@@ -79,10 +79,9 @@ export function applyRendererSettings(
|
|
|
79
79
|
* reload-only `antialias` option at construction and the live shadow/resolution
|
|
80
80
|
* settings (via {@link applyRendererSettings}). Absent → registry defaults.
|
|
81
81
|
*
|
|
82
|
-
* `opts` is
|
|
83
|
-
*
|
|
84
|
-
* `
|
|
85
|
-
* passes it. The roots path passes `alpha:true` for every stacked canvas
|
|
82
|
+
* When `opts` is omitted, construction leaves `alpha` and
|
|
83
|
+
* `preserveDrawingBuffer` at their renderer defaults. The host passes
|
|
84
|
+
* `alpha:true` for every stacked canvas
|
|
86
85
|
* above the bottom one (so its clear-alpha-0 shows the layer below through
|
|
87
86
|
* it) and `preserveDrawingBuffer:true` for every stacked canvas (the
|
|
88
87
|
* recorded capture-tier cost, paid once here rather than re-derived later).
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { createContext, useContext } from 'react';
|
|
18
|
+
import { useOptionalGame } from '../react/world-state';
|
|
18
19
|
import type { R3FGameContext } from './world-context';
|
|
19
20
|
|
|
20
21
|
export interface EngineBridgeValue {
|
|
@@ -39,7 +40,7 @@ export const EngineBridge = createContext<EngineBridgeValue | null>(null);
|
|
|
39
40
|
* function Player({ speed = 4.5 }) {
|
|
40
41
|
* const { input } = useGameContext();
|
|
41
42
|
* const ref = useRef<THREE.Group>(null);
|
|
42
|
-
* useFrame((_, dt) => { if (input
|
|
43
|
+
* useFrame((_, dt) => { if (input?.isPressed('move_forward')) … });
|
|
43
44
|
* return <group ref={ref}><PlayerCharacter /></group>;
|
|
44
45
|
* }
|
|
45
46
|
* ```
|
|
@@ -50,11 +51,35 @@ export const EngineBridge = createContext<EngineBridgeValue | null>(null);
|
|
|
50
51
|
*/
|
|
51
52
|
export function useGameContext(): R3FGameContext {
|
|
52
53
|
const bridge = useContext(EngineBridge);
|
|
54
|
+
// Read unconditionally (rules of hooks) — used only to shape the throw below.
|
|
55
|
+
const hostedByGame = useOptionalGame() !== null;
|
|
53
56
|
if (!bridge?.ctx) {
|
|
57
|
+
// NAME THE REAL CAUSE. This throw used to say "check that the component is
|
|
58
|
+
// rendered inside your world entry" — advice a scaffolded game has already
|
|
59
|
+
// followed, because the shape `vgai create` produces (`export default
|
|
60
|
+
// function World()`) mounts as a COMPONENT ROOT and receives no vgai
|
|
61
|
+
// runtime context at all: `r3f-root-factory.tsx` passes
|
|
62
|
+
// `engineRuntime: false` deliberately, so this hook can never resolve
|
|
63
|
+
// there however the component is nested. A world whose first render calls
|
|
64
|
+
// it dies with "fiber crashed before its first commit" and a fix
|
|
65
|
+
// instruction that cannot work. The `Game` in scope tells the two cases
|
|
66
|
+
// apart: present means a vgai-mounted world, absent means no root at all.
|
|
54
67
|
throw new Error(
|
|
55
|
-
|
|
56
|
-
'
|
|
57
|
-
|
|
68
|
+
hostedByGame
|
|
69
|
+
? 'useGameContext(): this world mounts as a COMPONENT ROOT (the default-exported ' +
|
|
70
|
+
'shape `vgai create` scaffolds) and receives no vgai runtime context — by design, ' +
|
|
71
|
+
'so nesting cannot fix it. Game-scoped services are on the Game handle instead: ' +
|
|
72
|
+
'`useGame().input` (from @vgai/engine/react/world-state) is the game-owned ' +
|
|
73
|
+
"InputManager the host already loaded this project's input map into and the same " +
|
|
74
|
+
'one `game.input.hold/tap` drives; `useDebugProvider`/`useDebugCommand`/' +
|
|
75
|
+
'`useWorldState` are the state/command doors. If this world genuinely needs the ' +
|
|
76
|
+
'imperative runtime (`ctx.systems`, adapter options like `inputMapPath`), export ' +
|
|
77
|
+
'`adapter = createR3FAdapter({ id, content: <World/> })` from the entry instead of ' +
|
|
78
|
+
'default-exporting the component.'
|
|
79
|
+
: 'useGameContext(): no engine root context and no Game in scope — this component is ' +
|
|
80
|
+
'rendering outside any mounted vgai root (a standalone <Canvas>, a story, or a ' +
|
|
81
|
+
"bare test render). Mount it through your project's three root, or read services " +
|
|
82
|
+
'the component root way (native hooks and project-owned state).',
|
|
58
83
|
);
|
|
59
84
|
}
|
|
60
85
|
return bridge.ctx;
|