@vgai/engine 0.5.16 → 0.5.18
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 +1 -16
- package/dist/adapter/adapter-module.d.ts.map +1 -1
- package/dist/adapter/adapter-module.js +1 -11
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts +0 -2
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -1
- package/dist/adapter/finders/scenes-from-entrypoint-selection.js +3 -6
- package/dist/adapter/renderer-config.d.ts +83 -0
- package/dist/adapter/renderer-config.d.ts.map +1 -0
- package/dist/adapter/renderer-config.js +121 -0
- package/dist/adapter/root-adapter.d.ts +6 -2
- package/dist/adapter/root-adapter.d.ts.map +1 -1
- package/dist/adapter/root-seam-contract.d.ts +5 -0
- package/dist/adapter/root-seam-contract.d.ts.map +1 -1
- package/dist/adapter/root-seam-contract.js +1 -0
- package/dist/adapter/setup-three-root-adapter.js +9 -9
- package/dist/canvas-react/pixi-react-adapter.d.ts.map +1 -1
- package/dist/canvas-react/pixi-react-adapter.js +92 -41
- 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/input/input-manager.d.ts +15 -0
- package/dist/input/input-manager.d.ts.map +1 -1
- package/dist/input/input-manager.js +22 -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 +0 -2
- package/dist/manifest/schema.d.ts.map +1 -1
- package/dist/manifest/schema.js +3 -24
- package/dist/pixi/authoring.d.ts +33 -0
- package/dist/pixi/authoring.d.ts.map +1 -1
- package/dist/pixi/authoring.js +86 -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/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 +2 -16
- package/dist/runtime/create-runtime.d.ts.map +1 -1
- package/dist/runtime/create-runtime.js +36 -38
- 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 +9 -1
- package/dist/runtime/game.d.ts.map +1 -1
- package/dist/runtime/game.js +25 -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 +2 -21
- package/dist/world3d-react/engine-bridge.d.ts.map +1 -1
- package/dist/world3d-react/engine-bridge.js +28 -3
- package/dist/world3d-react/index.d.ts +2 -2
- package/dist/world3d-react/index.d.ts.map +1 -1
- package/dist/world3d-react/index.js +2 -2
- package/dist/world3d-react/r3f-adapter.d.ts +2 -2
- package/dist/world3d-react/r3f-adapter.d.ts.map +1 -1
- package/dist/world3d-react/r3f-adapter.js +90 -15
- package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
- package/dist/world3d-react/r3f-root-factory.js +12 -2
- 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/renderer-config.d.ts +7 -69
- package/dist/world3d-react/renderer-config.d.ts.map +1 -1
- package/dist/world3d-react/renderer-config.js +7 -108
- package/dist/world3d-react/world-context.d.ts +43 -0
- package/dist/world3d-react/world-context.d.ts.map +1 -1
- package/dist/world3d-react/world-context.js +89 -46
- package/dist-config/config.js.map +1 -1
- package/package.json +1 -1
- package/schemas/engine-capabilities.json +3 -3
- package/schemas/vgai-project.schema.json +2 -7
- package/src/adapter/adapter-module.ts +1 -25
- package/src/adapter/finders/scenes-from-entrypoint-selection.ts +3 -8
- package/src/adapter/renderer-config.ts +178 -0
- package/src/adapter/root-adapter.ts +6 -2
- package/src/adapter/root-seam-contract.ts +1 -0
- package/src/adapter/setup-three-root-adapter.ts +9 -9
- package/src/canvas-react/pixi-react-adapter.tsx +98 -53
- 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/input/input-manager.ts +23 -0
- package/src/manifest/load.ts +0 -7
- package/src/manifest/schema.ts +3 -26
- package/src/pixi/authoring.ts +98 -6
- package/src/react/world-state.tsx +15 -0
- package/src/render/soft-particle-depth.ts +8 -13
- package/src/runtime/create-runtime.ts +38 -52
- package/src/runtime/dev-build.ts +49 -0
- package/src/runtime/game.ts +31 -2
- package/src/runtime/mount-manifest.ts +2 -33
- package/src/world3d-react/engine-bridge.ts +28 -3
- package/src/world3d-react/index.ts +5 -5
- package/src/world3d-react/r3f-adapter.tsx +89 -20
- package/src/world3d-react/r3f-root-factory.tsx +12 -2
- package/src/world3d-react/rapier-physics-bridge.tsx +4 -4
- package/src/world3d-react/renderer-config.ts +7 -164
- package/src/world3d-react/world-context.ts +100 -51
- 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
|
@@ -149,22 +149,6 @@ export interface RootMountSpecBase {
|
|
|
149
149
|
readonly zOrder?: number | undefined;
|
|
150
150
|
/** Whether play-mode pause/step applies to this world (D10). Defaults to `true`. */
|
|
151
151
|
readonly pausable?: boolean | undefined;
|
|
152
|
-
/**
|
|
153
|
-
* `vgai.project.json`'s `dev: true` — this root is a DEV LAYER (the game's
|
|
154
|
-
* own dev GUI), not shipped game content. Two things happen here: the
|
|
155
|
-
* surface is stacked ABOVE every non-dev root (a dev layer sits topmost),
|
|
156
|
-
* and it is marked `data-vgai-dev-layer="true"` so the play compositor can
|
|
157
|
-
* leave it out of a capture (`composite-screenshot.ts`). Whether a dev root
|
|
158
|
-
* reaches this function at all is the CALLER's gate —
|
|
159
|
-
* `mountManifestRoots` asks `devLayersEnabled()` before building the spec.
|
|
160
|
-
*
|
|
161
|
-
* It steals no input from the game while closed for a reason that predates
|
|
162
|
-
* this flag and needs no new mechanism: a dev layer is a `dom` root, DOM
|
|
163
|
-
* layers are `pointer-events:none` (see `mountOneReactRoot`) with the
|
|
164
|
-
* mounted tree opting individual elements back in, and the input router
|
|
165
|
-
* never sees DOM roots at all.
|
|
166
|
-
*/
|
|
167
|
-
readonly dev?: boolean | undefined;
|
|
168
152
|
/**
|
|
169
153
|
* Optional claim predicate for the delegating input router (D5 §2a), over
|
|
170
154
|
* a point RELATIVE TO THE CONTAINER. Absent means: this world claims only
|
|
@@ -328,6 +312,8 @@ export interface RootsRuntimeConfig {
|
|
|
328
312
|
*/
|
|
329
313
|
export interface GameSession {
|
|
330
314
|
stop(): void;
|
|
315
|
+
/** Resolves after every root has finished the cleanup initiated by stop(). */
|
|
316
|
+
readonly stopComplete: Promise<void>;
|
|
331
317
|
pause(): void;
|
|
332
318
|
resume(): void;
|
|
333
319
|
step(): void;
|
|
@@ -776,22 +762,12 @@ async function createRootsGameRuntime(config: RootsRuntimeConfig): Promise<GameS
|
|
|
776
762
|
// DOM-root layer shares this SAME stacking pass even though it is a `<div>`, not a
|
|
777
763
|
// canvas, and carries no `hitTest` (the router never sees react entries at
|
|
778
764
|
// all — see the dispatch loop below). ---
|
|
779
|
-
// Dev layers sit TOPMOST, above every non-dev root, whatever zOrder either
|
|
780
|
-
// declares: a dev GUI the game can paint over is not a dev GUI. Expressed as
|
|
781
|
-
// two independent `stackOrder` passes concatenated rather than a magic
|
|
782
|
-
// zOrder value, so `stackOrder`'s one rule (ascending zOrder, ties -> array
|
|
783
|
-
// order) still governs WITHIN each band and nothing has to reason about what
|
|
784
|
-
// number is "high enough".
|
|
785
765
|
const claimEntries = mountSpecs.map((spec) => ({
|
|
786
766
|
id: spec.id,
|
|
787
767
|
zOrder: spec.zOrder ?? 0,
|
|
788
768
|
hitTest: spec.hitTest,
|
|
789
|
-
dev: spec.dev === true,
|
|
790
769
|
}));
|
|
791
|
-
const stacked =
|
|
792
|
-
...stackOrder(claimEntries.filter((e) => !e.dev)),
|
|
793
|
-
...stackOrder(claimEntries.filter((e) => e.dev)),
|
|
794
|
-
];
|
|
770
|
+
const stacked = stackOrder(claimEntries);
|
|
795
771
|
const bottomId = stacked[0]?.id;
|
|
796
772
|
const kindById = new Map(mountSpecs.map((spec) => [spec.id, spec.kind] as const));
|
|
797
773
|
|
|
@@ -807,11 +783,6 @@ async function createRootsGameRuntime(config: RootsRuntimeConfig): Promise<GameS
|
|
|
807
783
|
// surface.ts`, which is the one door the capture/staleness/screenshot
|
|
808
784
|
// sites now ask (ARCHITECTURE-CORE §The editor protocol, zero inference).
|
|
809
785
|
surface.dataset['vgaiRootId'] = entry.id;
|
|
810
|
-
// The capture's marker (`composite-screenshot.ts` reads exactly this
|
|
811
|
-
// attribute): a dev layer is excluded from a play capture unless the
|
|
812
|
-
// capture asks for it, because a contaminated "does the game look right"
|
|
813
|
-
// frame is worthless.
|
|
814
|
-
if (entry.dev) surface.dataset['vgaiDevLayer'] = 'true';
|
|
815
786
|
if (!isReact) {
|
|
816
787
|
const canvas = surface as unknown as HTMLCanvasElement;
|
|
817
788
|
canvas.width = w;
|
|
@@ -917,33 +888,48 @@ async function createRootsGameRuntime(config: RootsRuntimeConfig): Promise<GameS
|
|
|
917
888
|
? installSessionRenderHarness(game, renderModeLocation, config.renderControl?.target)
|
|
918
889
|
: undefined;
|
|
919
890
|
|
|
891
|
+
let resolveStopComplete!: () => void;
|
|
892
|
+
const stopComplete = new Promise<void>((resolve) => {
|
|
893
|
+
resolveStopComplete = resolve;
|
|
894
|
+
});
|
|
895
|
+
let stopping = false;
|
|
896
|
+
|
|
920
897
|
function fullCleanup(): void {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
entry
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
898
|
+
if (stopping) return;
|
|
899
|
+
stopping = true;
|
|
900
|
+
try {
|
|
901
|
+
loop.stop();
|
|
902
|
+
router.dispose();
|
|
903
|
+
for (const entry of mountedEntries) {
|
|
904
|
+
entry.mounted.dispose();
|
|
905
|
+
entry.renderer?.dispose();
|
|
906
|
+
entry.renderer?.forceContextLoss();
|
|
907
|
+
// Remove from the element's CURRENT parent, not the mount-time
|
|
908
|
+
// `container`: the editor's Game panel re-parents the live surfaces
|
|
909
|
+
// when its mount element swaps (fill <-> device preset, W2c), and
|
|
910
|
+
// `container.removeChild` would throw NotFoundError after such a move.
|
|
911
|
+
// `container` remains the fallback for hosts whose element stand-ins
|
|
912
|
+
// never wire `parentNode` (headless unit fixtures); an element already
|
|
913
|
+
// detached by such a host is a no-op via the catch.
|
|
914
|
+
try {
|
|
915
|
+
(entry.element.parentNode ?? container).removeChild(entry.element);
|
|
916
|
+
} catch {
|
|
917
|
+
/* already detached — nothing to remove */
|
|
918
|
+
}
|
|
938
919
|
}
|
|
920
|
+
game.dispose();
|
|
921
|
+
retractDevGlobals();
|
|
922
|
+
retractRenderHarness?.();
|
|
923
|
+
} finally {
|
|
924
|
+
void Promise.allSettled(
|
|
925
|
+
mountedEntries.map((entry) => entry.mounted.disposeComplete ?? Promise.resolve()),
|
|
926
|
+
).then(() => resolveStopComplete());
|
|
939
927
|
}
|
|
940
|
-
game.dispose();
|
|
941
|
-
retractDevGlobals();
|
|
942
|
-
retractRenderHarness?.();
|
|
943
928
|
}
|
|
944
929
|
|
|
945
930
|
return {
|
|
946
931
|
stop: fullCleanup,
|
|
932
|
+
stopComplete,
|
|
947
933
|
// `Game.play` owns per-world pause, loop-gate, and audio-gate semantics.
|
|
948
934
|
pause() {
|
|
949
935
|
game.play.pause();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE one owner of "is this a development context". Every reader that has to
|
|
3
|
+
* answer that question calls {@link devBuildEnabled}; there is deliberately NO
|
|
4
|
+
* second source of truth — not a module-level cached boolean, not a
|
|
5
|
+
* `globalThis` flag, not a per-host copy of the `import.meta.env` read below.
|
|
6
|
+
* Instrumentation that ships to players because two places disagreed about
|
|
7
|
+
* what "dev" means is exactly the failure this single owner exists to make
|
|
8
|
+
* impossible.
|
|
9
|
+
*
|
|
10
|
+
* Ownership, stated in one place (the build rule):
|
|
11
|
+
* - OWNER: this function. It resolves the answer; nothing else derives it.
|
|
12
|
+
* - SHARERS: the two three-root adapters (`adapter/setup-three-root-adapter.ts`
|
|
13
|
+
* and `world3d-react/r3f-adapter.tsx`), which seed live render vitals only
|
|
14
|
+
* under it, and the `static-batch` capability's mutation watch and `Frozen`
|
|
15
|
+
* warning. Any future dev-only instrument calls this too, with its own
|
|
16
|
+
* `override`.
|
|
17
|
+
* - TEARDOWN: none. This is a pure predicate over build config and one
|
|
18
|
+
* caller-supplied argument — it owns no resource, allocates nothing, and
|
|
19
|
+
* has no lifecycle to end.
|
|
20
|
+
*
|
|
21
|
+
* The three inputs, highest precedence first:
|
|
22
|
+
* 1. `override` — the explicit per-call answer. A headless test, a capture
|
|
23
|
+
* harness, or a host that knows better passes `true`/`false` and gets
|
|
24
|
+
* exactly that. Passing `undefined` (or omitting it) means "decide for
|
|
25
|
+
* me" and falls through. The editor's own preview mount is the worked
|
|
26
|
+
* case: it is a dev session by definition even when the editor SPA it
|
|
27
|
+
* runs inside is a production build.
|
|
28
|
+
* 2. A dev build — `import.meta.env.DEV`. The ordinary local/editor case.
|
|
29
|
+
* 3. A production build's EXPLICIT opt-in — `VITE_VGAI_DEV_BUILD=true`.
|
|
30
|
+
* Instrumenting a production bundle is a real, legitimate choice (an
|
|
31
|
+
* internal playtest build, a QA build), and it must be an opt-in someone
|
|
32
|
+
* had to type, never something a default drifts into.
|
|
33
|
+
*
|
|
34
|
+
* Anything else — a production build with no opt-in — is `false`.
|
|
35
|
+
*/
|
|
36
|
+
export function devBuildEnabled(override?: boolean | undefined): boolean {
|
|
37
|
+
if (override !== undefined) return override;
|
|
38
|
+
// `import.meta` is cast whole, not just its `.env`: this module is reachable
|
|
39
|
+
// from programs whose tsconfig does not pull in `vite/client` (the session
|
|
40
|
+
// client's, for one, which reaches the three adapter transitively), and there
|
|
41
|
+
// `ImportMeta` has no declared `env` at all. The cast keeps the single owner
|
|
42
|
+
// of the dev answer importable from ANY program rather than forcing every
|
|
43
|
+
// downstream tsconfig to adopt Vite's ambient types.
|
|
44
|
+
const env = (import.meta as unknown as { env?: unknown }).env as
|
|
45
|
+
| { DEV?: boolean | undefined; VITE_VGAI_DEV_BUILD?: string | undefined }
|
|
46
|
+
| undefined;
|
|
47
|
+
if (env?.DEV === true) return true;
|
|
48
|
+
return env?.VITE_VGAI_DEV_BUILD === 'true';
|
|
49
|
+
}
|
package/src/runtime/game.ts
CHANGED
|
@@ -734,8 +734,14 @@ export interface GameInternal extends Game {
|
|
|
734
734
|
* the conventional `/inputmaps/default.inputmap.json` (or the adapter's
|
|
735
735
|
* configured `inputMapPath`); runtime additions go through
|
|
736
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.
|
|
737
743
|
*/
|
|
738
|
-
loadInputMap(path: string): Promise<void>;
|
|
744
|
+
loadInputMap(path: string, options?: { optional?: boolean }): Promise<void>;
|
|
739
745
|
/** Release game-owned resources after every mounted root has disposed. */
|
|
740
746
|
dispose(): void;
|
|
741
747
|
}
|
|
@@ -856,6 +862,10 @@ export function createGame(opts: {
|
|
|
856
862
|
input.setDebugEmit((event, detail) => debugRegistry.forRoot('(game)').emit(event, detail));
|
|
857
863
|
let inputMapPath: string | null = null;
|
|
858
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>>();
|
|
859
869
|
|
|
860
870
|
function requireDefaultRoot(): RootInstance {
|
|
861
871
|
if (roots.length === 0) {
|
|
@@ -1468,7 +1478,26 @@ export function createGame(opts: {
|
|
|
1468
1478
|
runFrameImpl(fixedDt, { skipRenderPhases });
|
|
1469
1479
|
}
|
|
1470
1480
|
},
|
|
1471
|
-
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
|
+
}
|
|
1472
1501
|
if (inputMapPath && inputMapPath !== path) {
|
|
1473
1502
|
throw new Error(
|
|
1474
1503
|
`Game.loadInputMap: input map is already "${inputMapPath}"; root requested "${path}". ` +
|
|
@@ -36,7 +36,6 @@ import {
|
|
|
36
36
|
import { createGameRuntime, type GameSession, type RootMountSpec } from './create-runtime';
|
|
37
37
|
import { type DebugBridgeWindowTarget, maybeInstallDebugBridge } from './debug-bridge';
|
|
38
38
|
import { getDebugRegistry } from './debug-registry';
|
|
39
|
-
import { devLayersEnabled } from './dev-layers';
|
|
40
39
|
import { getGameplayRngTrapControl } from './gameplay-rng-trap';
|
|
41
40
|
import { RENDER_SEED_QUERY_PARAM } from './render-seed';
|
|
42
41
|
import type { GameSetupFn, PlaytestContext } from './types';
|
|
@@ -156,15 +155,6 @@ export interface MountManifestOptions {
|
|
|
156
155
|
readonly seed?: number | undefined;
|
|
157
156
|
/** Host identity for a private play run or coordinated Team Test. */
|
|
158
157
|
readonly playtest?: PlaytestContext | null | undefined;
|
|
159
|
-
/**
|
|
160
|
-
* The explicit per-mount answer to "are dev layers enabled" — forwarded
|
|
161
|
-
* verbatim to `devLayersEnabled` (`runtime/dev-layers.ts`), the ONE owner of
|
|
162
|
-
* that question. Omit it and the build decides (dev build ⇒ on; production
|
|
163
|
-
* build ⇒ off unless `VITE_VGAI_DEV_LAYERS=true`). A root declaring
|
|
164
|
-
* `dev: true` is simply not mounted when the answer is `false`, so a shipped
|
|
165
|
-
* build carries no dev GUI at all.
|
|
166
|
-
*/
|
|
167
|
-
readonly devLayers?: boolean | undefined;
|
|
168
158
|
/** Where to read `?vgai-seed=` from for the boot-time seed reader. Same
|
|
169
159
|
* override precedent as `debugBridge.url` (defaults to `window.location`
|
|
170
160
|
* when a real `window` exists; a headless caller with no override gets no
|
|
@@ -346,7 +336,6 @@ function buildRootMountSpec(
|
|
|
346
336
|
id: world.id,
|
|
347
337
|
zOrder: world.zOrder,
|
|
348
338
|
pausable: world.pausable,
|
|
349
|
-
dev: world.dev,
|
|
350
339
|
};
|
|
351
340
|
|
|
352
341
|
// `entry`'s `kind` is guaranteed to agree with `world.kind` past the guard
|
|
@@ -390,10 +379,6 @@ function buildRootMountSpec(
|
|
|
390
379
|
* ingest }` adapter with no caller-supplied adapter, or an empty manifest all
|
|
391
380
|
* throw a named `Error` identifying the world id, its kind, and what was
|
|
392
381
|
* expected — never a silent skip or a partially-mounted session.
|
|
393
|
-
*
|
|
394
|
-
* The ONE root that IS skipped silently is a `dev: true` dev layer when dev
|
|
395
|
-
* layers are disabled (`devLayersEnabled`) — that skip is the feature: it is
|
|
396
|
-
* what makes a dev GUI safe to leave in the game.
|
|
397
382
|
*/
|
|
398
383
|
export async function mountManifestRoots(opts: MountManifestOptions): Promise<GameSession> {
|
|
399
384
|
const manifest = resolveManifest(opts.manifest);
|
|
@@ -402,23 +387,7 @@ export async function mountManifestRoots(opts: MountManifestOptions): Promise<Ga
|
|
|
402
387
|
}
|
|
403
388
|
const entries = opts.entries ?? {};
|
|
404
389
|
|
|
405
|
-
|
|
406
|
-
// only when dev layers are enabled, so a production build carries no dev
|
|
407
|
-
// GUI. The answer comes from `devLayersEnabled` and nowhere else — see that
|
|
408
|
-
// function's header for why there is exactly one owner. Dropping the root
|
|
409
|
-
// here (rather than mounting it hidden) is deliberate: nothing of the dev
|
|
410
|
-
// layer is constructed, so its entry module's cost and its DOM never exist
|
|
411
|
-
// in a shipped build.
|
|
412
|
-
const devEnabled = devLayersEnabled(opts.devLayers);
|
|
413
|
-
const mountable = devEnabled ? manifest.roots : manifest.roots.filter((world) => !world.dev);
|
|
414
|
-
if (mountable.length === 0) {
|
|
415
|
-
throw new Error(
|
|
416
|
-
'mountManifestRoots: every root in this manifest is a dev layer (`dev: true`) and dev ' +
|
|
417
|
-
'layers are disabled for this build — there is no game to mount.',
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const roots: RootMountSpec[] = mountable.map((world) =>
|
|
390
|
+
const roots: RootMountSpec[] = manifest.roots.map((world) =>
|
|
422
391
|
buildRootMountSpec(world, entries[world.id]),
|
|
423
392
|
);
|
|
424
393
|
|
|
@@ -443,7 +412,7 @@ export async function mountManifestRoots(opts: MountManifestOptions): Promise<Ga
|
|
|
443
412
|
headless: opts.headless,
|
|
444
413
|
// `rendering.antialias` reaches the WebGL context at CONSTRUCTION and can be honoured
|
|
445
414
|
// nowhere else — see the manifest schema's own `rendering` block and
|
|
446
|
-
// `
|
|
415
|
+
// `adapter/renderer-config.ts`'s header for why it is not a world-level declaration.
|
|
447
416
|
...(manifest.rendering === undefined ? {} : { antialias: manifest.rendering.antialias }),
|
|
448
417
|
seed: resolvedSeed,
|
|
449
418
|
playtest: opts.playtest,
|
|
@@ -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 {
|
|
@@ -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;
|
|
@@ -33,22 +33,22 @@
|
|
|
33
33
|
* Deliberately NOT re-exported here: `<RapierPhysicsBridge>`
|
|
34
34
|
* (`@engine/world3d-react/rapier-physics-bridge`), which registers an R3F
|
|
35
35
|
* world's `@react-three/rapier` physics as the game's
|
|
36
|
-
* `SystemAdapters.physics` — so the
|
|
36
|
+
* `SystemAdapters.physics` — so the colliders instrument and the
|
|
37
37
|
* editor's transform coordination can see it. It imports
|
|
38
38
|
* `@react-three/rapier`, and only a world that already depends on that library
|
|
39
39
|
* should pull it into its graph; a deep import keeps this barrel's peer
|
|
40
40
|
* contract to `react`/`@react-three/fiber`/`three` alone.
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
|
-
export { EngineBridge, type EngineBridgeValue, useGameContext } from './engine-bridge';
|
|
44
|
-
export { type CreateR3FAdapterOptions, createR3FAdapter } from './r3f-adapter';
|
|
45
|
-
export { r3fRootFactory, resolveR3FEntryAdapter } from './r3f-root-factory';
|
|
46
43
|
export {
|
|
47
44
|
applyWorldRendererConfig,
|
|
48
45
|
type WorldOutputColorSpace,
|
|
49
46
|
type WorldRendererConfig,
|
|
50
47
|
type WorldToneMapping,
|
|
51
|
-
} from '
|
|
48
|
+
} from '../adapter/renderer-config';
|
|
49
|
+
export { EngineBridge, type EngineBridgeValue, useGameContext } from './engine-bridge';
|
|
50
|
+
export { type CreateR3FAdapterOptions, createR3FAdapter } from './r3f-adapter';
|
|
51
|
+
export { r3fRootFactory, resolveR3FEntryAdapter } from './r3f-root-factory';
|
|
52
52
|
export {
|
|
53
53
|
createR3FRootContext,
|
|
54
54
|
DEFAULT_INPUT_MAP_PATH,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from '@react-three/fiber';
|
|
31
31
|
import { createElement, Fragment, type ReactNode, useEffect, useMemo } from 'react';
|
|
32
32
|
import type { MountedThreeRoot, RootAdapter, ThreeHostContext } from '../adapter';
|
|
33
|
+
import { applyWorldRendererConfig, type WorldRendererConfig } from '../adapter/renderer-config';
|
|
33
34
|
import type { SystemAdapters } from '../adapter/system-adapter';
|
|
34
35
|
import { type RenderVitalsRegistration, registerRenderVitals } from '../dev/register-render-vitals';
|
|
35
36
|
import {
|
|
@@ -43,10 +44,9 @@ import { createWebGLFrameCapture } from '../dev/webgl-frame-capture';
|
|
|
43
44
|
import { WorldProvider } from '../react/world-state';
|
|
44
45
|
import { createSoftParticleDepthPass } from '../render/soft-particle-depth';
|
|
45
46
|
import { getDebugRegistry } from '../runtime/debug-registry';
|
|
46
|
-
import {
|
|
47
|
+
import { devBuildEnabled } from '../runtime/dev-build';
|
|
47
48
|
import { EngineBridge, type EngineBridgeValue } from './engine-bridge';
|
|
48
|
-
import {
|
|
49
|
-
import { createR3FRootContext } from './world-context';
|
|
49
|
+
import { createR3FRootContext, DEFAULT_INPUT_MAP_PATH, wireGameInputSeams } from './world-context';
|
|
50
50
|
|
|
51
51
|
/** The slice of `WebGLRenderer.info` the vitals reporter reads. Declared
|
|
52
52
|
* structurally rather than imported from `three`, per this module's own
|
|
@@ -77,6 +77,11 @@ interface RendererCounters {
|
|
|
77
77
|
* sort is stable) — the tie-break among priority-0 callbacks is insertion
|
|
78
78
|
* order.
|
|
79
79
|
*
|
|
80
|
+
* It is mounted only when THIS host carries a real WebGL context. The editor's
|
|
81
|
+
* design-time host deliberately reconciles the scene without one because the
|
|
82
|
+
* editor viewport owns the real renderer and camera; that viewport runs the
|
|
83
|
+
* same generic depth pass immediately before it presents the adopted scene.
|
|
84
|
+
*
|
|
80
85
|
* Costs nothing while no soft-particle system is armed — see
|
|
81
86
|
* `../render/soft-particle-depth.ts` for what "armed" means and what the pass
|
|
82
87
|
* spends when it is.
|
|
@@ -112,7 +117,7 @@ export interface CreateR3FAdapterOptions {
|
|
|
112
117
|
/** The colour pipeline this world was AUTHORED for, applied to the host's renderer for the life
|
|
113
118
|
* of the mount and restored on dispose. Omit it (every world here does) to keep the host's own
|
|
114
119
|
* defaults; declare it when the world's colours were picked against a different engine's
|
|
115
|
-
* pipeline — see
|
|
120
|
+
* pipeline — see `../adapter/renderer-config.ts`. */
|
|
116
121
|
readonly renderer?: WorldRendererConfig | undefined;
|
|
117
122
|
}
|
|
118
123
|
|
|
@@ -185,10 +190,33 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): RootAdapter
|
|
|
185
190
|
// `ctx` rides the bridge so ordinary components can reach engine
|
|
186
191
|
// services through `useGameContext()`.
|
|
187
192
|
const bridge: EngineBridgeValue | null = runtime ? { ctx: runtime.ctx } : null;
|
|
193
|
+
const gameDebugRegistry = host.game ? getDebugRegistry(host.game) : null;
|
|
194
|
+
// THE HOST STILL WIRES THIS GAME'S INPUT WHEN THE WORLD TAKES NO CONTEXT.
|
|
195
|
+
//
|
|
196
|
+
// A default-exported world mounts with `engineRuntime: false` — no vgai
|
|
197
|
+
// runtime context enters its React tree, by design. That says nothing
|
|
198
|
+
// about the GAME-scoped input the host owns from the outside: the
|
|
199
|
+
// project's declared map, and this root's `game.input.*` seams on the
|
|
200
|
+
// debug registry. Skipping those made a fresh scaffold's bot/autoplay/
|
|
201
|
+
// `vgai eval` input doors throw `DEBUG_INPUT_UNAVAILABLE` and left the
|
|
202
|
+
// declared actions unloaded — in the ONE world shape `vgai create`
|
|
203
|
+
// produces. Same host-vs-context split as `WorldProvider` below.
|
|
204
|
+
//
|
|
205
|
+
// `inputMapPath` unset (what the default-export factory passes) means
|
|
206
|
+
// "the conventional path, if this project ships one" — quiet when it
|
|
207
|
+
// does not, since a brand-new project declares no actions yet.
|
|
208
|
+
const hostInputReady =
|
|
209
|
+
runtime || !gameDebugRegistry
|
|
210
|
+
? null
|
|
211
|
+
: wireGameInputSeams(host, gameDebugRegistry, {
|
|
212
|
+
id,
|
|
213
|
+
inputMapPath: inputMapPath ?? DEFAULT_INPUT_MAP_PATH,
|
|
214
|
+
optionalInputMap: inputMapPath === undefined,
|
|
215
|
+
});
|
|
188
216
|
// Actions must exist before any component's init()/first frame reads
|
|
189
217
|
// them — wait for the (never-rejecting) input-map load before the first
|
|
190
218
|
// commit.
|
|
191
|
-
await runtime?.inputMapReady;
|
|
219
|
+
await (runtime?.inputMapReady ?? hostInputReady);
|
|
192
220
|
|
|
193
221
|
// The world's own colour pipeline. Applied AFTER `root.configure()` below — see the comment
|
|
194
222
|
// at its call site for why "before" is the version that silently does nothing.
|
|
@@ -320,24 +348,44 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): RootAdapter
|
|
|
320
348
|
// until this landed. `host.game` is genuinely absent in bare/headless
|
|
321
349
|
// hosts, so the provider is conditional and those hooks stay the inert
|
|
322
350
|
// no-ops they already document themselves to be.
|
|
323
|
-
|
|
351
|
+
//
|
|
352
|
+
// READ FROM `host.game`, NEVER FROM `runtime` — the two are not the same
|
|
353
|
+
// question, and conflating them silently dark-ended the seam for every
|
|
354
|
+
// scaffolded game. The IDIOMATIC three world (`export default function
|
|
355
|
+
// World()`) mounts through `resolveR3FEntryAdapter` with
|
|
356
|
+
// `engineRuntime: false` (`r3f-root-factory.tsx`), so it has no
|
|
357
|
+
// `runtime` and no `bridge` — by design, because the vgai runtime
|
|
358
|
+
// CONTEXT (`useGameContext`) must not enter an ecosystem-native tree.
|
|
359
|
+
// `WorldProvider` is not that context: it carries the Game handle the
|
|
360
|
+
// host already holds, and it is the only way a game's own
|
|
361
|
+
// `useDebugProvider('stat', …)` can reach the ONE game-scoped registry
|
|
362
|
+
// the editor/`vgai eval` read. Keyed off `runtime`, every
|
|
363
|
+
// default-exported world's registrations no-opped in silence and
|
|
364
|
+
// `vgai eval 'await game.providers()'` listed only the built-ins.
|
|
365
|
+
// The canvas lane never had the gap (`canvas-react/
|
|
366
|
+
// pixi-react-root-factory.tsx` keeps its entry runtime on).
|
|
367
|
+
const game = host.game;
|
|
324
368
|
// `SoftParticleDepth` trails the world's own tree deliberately — see its
|
|
325
|
-
// docblock for why the subscription must land last.
|
|
369
|
+
// docblock for why the subscription must land last. A no-context design
|
|
370
|
+
// mount omits this fiber callback because the adopting editor viewport
|
|
371
|
+
// runs the pass with its own renderer and camera instead.
|
|
326
372
|
const world = createElement(
|
|
327
373
|
Fragment,
|
|
328
374
|
null,
|
|
329
375
|
content,
|
|
330
|
-
runtime
|
|
376
|
+
runtime && typeof host.renderer.getContext === 'function'
|
|
377
|
+
? createElement(SoftParticleDepth, { key: 'vgai-soft-particle-depth' })
|
|
378
|
+
: null,
|
|
331
379
|
createElement(MountEffectsReady, { key: 'vgai-mount-effects-ready' }),
|
|
332
380
|
);
|
|
381
|
+
// The two providers are INDEPENDENT: `EngineBridge` rides `bridge` (the
|
|
382
|
+
// vgai runtime context, absent for a native default-exported world),
|
|
383
|
+
// `WorldProvider` rides `host.game` (the React seam, present whenever a
|
|
384
|
+
// Game hosts this world). Nesting the second inside the first is what
|
|
385
|
+
// made the seam a hostage of the first.
|
|
386
|
+
const provided = game ? createElement(WorldProvider, { game }, world) : world;
|
|
333
387
|
root.render(
|
|
334
|
-
bridge
|
|
335
|
-
? createElement(
|
|
336
|
-
EngineBridge.Provider,
|
|
337
|
-
{ value: bridge },
|
|
338
|
-
game ? createElement(WorldProvider, { game }, world) : world,
|
|
339
|
-
)
|
|
340
|
-
: world,
|
|
388
|
+
bridge ? createElement(EngineBridge.Provider, { value: bridge }, provided) : provided,
|
|
341
389
|
);
|
|
342
390
|
// A reconcile-time crash (e.g. a missing `extend` catalogue entry)
|
|
343
391
|
// surfaces as an uncaught window error and `onCreated` never fires —
|
|
@@ -349,6 +397,7 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): RootAdapter
|
|
|
349
397
|
cleanup();
|
|
350
398
|
root.unmount();
|
|
351
399
|
runtime?.dispose();
|
|
400
|
+
if (!runtime) gameDebugRegistry?.strip(id);
|
|
352
401
|
restoreRendererConfig();
|
|
353
402
|
reject(
|
|
354
403
|
new Error(
|
|
@@ -361,6 +410,7 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): RootAdapter
|
|
|
361
410
|
cleanup();
|
|
362
411
|
root.unmount();
|
|
363
412
|
runtime?.dispose();
|
|
413
|
+
if (!runtime) gameDebugRegistry?.strip(id);
|
|
364
414
|
restoreRendererConfig();
|
|
365
415
|
reject(
|
|
366
416
|
new Error(
|
|
@@ -420,7 +470,6 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): RootAdapter
|
|
|
420
470
|
// `RootState` for precisely this reason, so reading it per frame is the
|
|
421
471
|
// supported way to see the live root. It is a plain map read.
|
|
422
472
|
const live = (): RootState => state.get();
|
|
423
|
-
const gameDebugRegistry = host.game ? getDebugRegistry(host.game) : null;
|
|
424
473
|
|
|
425
474
|
// --- Live render vitals (issue #1504), for an R3F world -----------------
|
|
426
475
|
// The SAME door the classic adapter seeds
|
|
@@ -432,11 +481,11 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): RootAdapter
|
|
|
432
481
|
// world is the only shape a scaffolded project's three root has, so a
|
|
433
482
|
// door wired only into the classic path is a door no scaffolded project
|
|
434
483
|
// ever gets (issue #1518: `game.state('render.vitals')` answered "no
|
|
435
|
-
// state provider registered" in the field while every other
|
|
436
|
-
// the same world worked).
|
|
484
|
+
// state provider registered" in the field while every other debug
|
|
485
|
+
// reading of the same world worked).
|
|
437
486
|
//
|
|
438
487
|
// The gate is the same three conditions, minus the one that cannot
|
|
439
|
-
// arise: `
|
|
488
|
+
// arise: `devBuildEnabled()` (`../runtime/dev-build.ts`, the ONE owner
|
|
440
489
|
// of "is this a dev context" — a ship build registers nothing and pays
|
|
441
490
|
// nothing), and a `Game` shell (the readings are folded out of that
|
|
442
491
|
// game's profiler frames). "Not headless" needs no term here: a headless
|
|
@@ -454,7 +503,7 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): RootAdapter
|
|
|
454
503
|
// (the fold) and `runtime.dispose()`'s scoped `strip(id)` (the
|
|
455
504
|
// registrations) — the same split the classic adapter documents.
|
|
456
505
|
let renderVitals: RenderVitalsRegistration | null = null;
|
|
457
|
-
if (host.game &&
|
|
506
|
+
if (host.game && devBuildEnabled()) {
|
|
458
507
|
// Same auto-enable as the classic adapter: the readings are folded out
|
|
459
508
|
// of profiler frames, and the profiler is a flag the editor already
|
|
460
509
|
// sets on play — under the dev gate this is the same cost arriving a
|
|
@@ -640,6 +689,26 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): RootAdapter
|
|
|
640
689
|
renderVitals?.dispose();
|
|
641
690
|
renderVitals = null;
|
|
642
691
|
runtime?.dispose();
|
|
692
|
+
// The no-context path has no `runtime.dispose()` to carry its scoped
|
|
693
|
+
// strip, so it ends its own registrations here — the same
|
|
694
|
+
// `strip(id)`, at the same moment. That is what ends this world's
|
|
695
|
+
// PROVIDERS AND COMMANDS (the render-vitals door above is the one
|
|
696
|
+
// this path actually registers under `id`); sibling roots are
|
|
697
|
+
// untouched.
|
|
698
|
+
//
|
|
699
|
+
// It does NOT end the input seams `wireGameInputSeams` set, and
|
|
700
|
+
// nothing else does either: `strip` walks providers/commands only
|
|
701
|
+
// (`runtime/debug-registry.ts`), so `virtualInputTargets`/
|
|
702
|
+
// `inputActionsSources`/`inputTraceSources` keep this world's id
|
|
703
|
+
// until a remount of the same id overwrites it. The runtime path has
|
|
704
|
+
// always been identical, and the residue is inert rather than
|
|
705
|
+
// hidden: every one of those closures reads GAME-scoped state
|
|
706
|
+
// (`game.input`, `game.loop.fixedDt`, the game's seed), so a stale
|
|
707
|
+
// entry actuates exactly what a live one would. What it can still do
|
|
708
|
+
// is name a dead world in `DEBUG_INPUT_WORLD_NOT_FOUND`'s registered
|
|
709
|
+
// list. Do not write "the strip clears the input seams" here again
|
|
710
|
+
// without changing `strip` to actually do it.
|
|
711
|
+
if (!runtime) gameDebugRegistry?.strip(id);
|
|
643
712
|
restoreRendererConfig();
|
|
644
713
|
},
|
|
645
714
|
};
|
|
@@ -70,8 +70,18 @@ export function resolveR3FEntryAdapter(entryModule: unknown, rootId: string): Ro
|
|
|
70
70
|
id: rootId,
|
|
71
71
|
content: createElement(mod.default),
|
|
72
72
|
// A default-exported R3F component is an ordinary R3F app. The host
|
|
73
|
-
// controls Fiber's native `frameloop: 'never'` scheduler; vgai
|
|
74
|
-
//
|
|
73
|
+
// controls Fiber's native `frameloop: 'never'` scheduler; the vgai
|
|
74
|
+
// runtime CONTEXT (`useGameContext`, `ctx.systems`) never enters its
|
|
75
|
+
// React tree.
|
|
76
|
+
//
|
|
77
|
+
// This flag governs that context and nothing else. It does NOT govern
|
|
78
|
+
// `WorldProvider`: `useDebugProvider`/`useDebugCommand`/`useWorldState`
|
|
79
|
+
// are the documented door a game declares its own state/commands
|
|
80
|
+
// through, and they must work in the idiomatic shape — which is the
|
|
81
|
+
// ONLY shape `vgai create` scaffolds. `r3f-adapter.tsx` therefore keys
|
|
82
|
+
// that provider off `host.game`, not off this flag; see the comment
|
|
83
|
+
// beside `const game = host.game` there for the measured symptom
|
|
84
|
+
// (`vgai eval 'await game.providers()'` listing only built-ins).
|
|
75
85
|
engineRuntime: false,
|
|
76
86
|
});
|
|
77
87
|
}
|