@vgai/engine 0.2.0 → 0.4.0-canary.20260715.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/package.json +24 -4
- package/schemas/engine-api.json +124 -0
- package/schemas/engine-api.md +53 -0
- package/schemas/engine-capabilities.json +124 -0
- package/schemas/inputmap.schema.json +314 -0
- package/schemas/mat.schema.json +286 -0
- package/schemas/prefab.schema.json +10148 -0
- package/schemas/scn2d.schema.json +475 -0
- package/schemas/vgai-game.schema.json +383 -0
- package/schemas/vscn.schema.json +11007 -0
- package/src/adapter/{world-kind.ts → adapter-surface.ts} +6 -6
- package/src/adapter/authoring.ts +77 -0
- package/src/adapter/first-party-systems.ts +23 -34
- package/src/adapter/game-adapter.ts +8 -8
- package/src/adapter/host-context.ts +2 -4
- package/src/adapter/index.ts +4 -4
- package/src/adapter/system-adapter.ts +88 -22
- package/src/adapter/vgai-scene-game-adapter.ts +244 -194
- package/src/animation/anim-graph-types.ts +12 -43
- package/src/animation/animation-clock.ts +479 -0
- package/src/animation/camera-ownership.ts +467 -0
- package/src/animation/cinematic-cues.ts +451 -0
- package/src/animation/clip-map.ts +41 -0
- package/src/animation/gsap-registration.ts +184 -0
- package/src/animation/theatre-clock-binding.ts +111 -0
- package/src/animation/theatre-director.ts +347 -0
- package/src/animation/theatre-object-binding.ts +661 -0
- package/src/animation/xstate-animation-binding.ts +436 -0
- package/src/animation/xstate-animation-meta.ts +319 -0
- package/src/audio/index.ts +39 -7
- package/src/audio/tone-clock-binding.ts +98 -0
- package/src/audio/tone-context.ts +129 -0
- package/src/audio/tone-offline-render.ts +167 -0
- package/src/audio/wav-encode.ts +119 -0
- package/src/character/cloth-sim.ts +533 -0
- package/src/character/spring-chain.ts +307 -0
- package/src/core/game-loop.ts +57 -2
- package/src/core/seeded-random.ts +161 -0
- package/src/core/system-runner.ts +20 -3
- package/src/core/types.ts +50 -0
- package/src/data/data-asset.ts +167 -0
- package/src/data/data-check-core.ts +242 -0
- package/src/data/data-ref.ts +145 -0
- package/src/data/vite-plugin-data.ts +290 -0
- package/src/dev/performance-profiler.ts +213 -0
- package/src/dev/webgl-gpu-timer.ts +53 -0
- package/src/ecs/component-manager.ts +45 -12
- package/src/ecs/game-component.ts +95 -11
- package/src/humanoid/bake.operation.ts +326 -0
- package/src/humanoid/body.ts +663 -0
- package/src/humanoid/clips.ts +149 -0
- package/src/humanoid/compose.ts +209 -0
- package/src/humanoid/generate.ts +189 -0
- package/src/humanoid/index.ts +36 -0
- package/src/humanoid/schema.ts +108 -0
- package/src/humanoid/skeleton.ts +345 -0
- package/src/index.ts +48 -0
- package/src/input/input-manager.ts +1886 -33
- package/src/input/input-types.ts +158 -3
- package/src/input/prompt-labels.ts +122 -0
- package/src/input/rebind-controller.ts +105 -0
- package/src/input/schema.ts +206 -52
- package/src/manifest/index.ts +5 -5
- package/src/manifest/load.ts +125 -72
- package/src/manifest/schema.ts +362 -255
- package/src/react/game-state.tsx +135 -32
- package/src/react/root-adapter.tsx +49 -0
- package/src/react/unmanaged-root-detector.ts +66 -0
- package/src/react/use-data.ts +124 -0
- package/src/react/use-selection.tsx +135 -0
- package/src/runtime/create-runtime.ts +112 -273
- package/src/runtime/debug-bridge.ts +483 -0
- package/src/runtime/debug-registry.ts +856 -0
- package/src/runtime/game.ts +342 -93
- package/src/runtime/gameplay-rng-trap.ts +134 -0
- package/src/runtime/input-router.ts +7 -7
- package/src/runtime/mount-game.ts +40 -38
- package/src/runtime/mount-manifest.ts +169 -37
- package/src/runtime/render-audio-control.ts +168 -0
- package/src/runtime/render-control.ts +522 -0
- package/src/runtime/render-seed.ts +79 -0
- package/src/runtime/state-bridge.ts +24 -10
- package/src/runtime/types.ts +110 -33
- package/src/scene/asset-loaders.ts +10 -36
- package/src/scene/asset-paths.ts +0 -2
- package/src/scene/asset-ref-check.ts +248 -0
- package/src/scene/asset-registry.ts +22 -0
- package/src/scene/component-registry.ts +14 -3
- package/src/scene/defaults.ts +1 -0
- package/src/scene/light-camera-factory.ts +11 -3
- package/src/scene/parse.ts +133 -0
- package/src/scene/scene-apply.ts +55 -4
- package/src/scene/scene-loader.ts +91 -123
- package/src/scene/scene-types.ts +0 -1
- package/src/scene/schema/animation.ts +30 -79
- package/src/scene/schema/entity.ts +20 -0
- package/src/scene/schema/index.ts +2 -46
- package/src/scene/schema/light.ts +16 -1
- package/src/scene/schema/material.ts +96 -91
- package/src/scene/schema/scene-file.ts +1 -7
- package/src/scene/user-data.ts +22 -10
- package/src/setup/setup-renderer.ts +10 -3
- package/src/tools/define-tool.ts +191 -0
- package/src/world2d/authoring-2d.ts +17 -1
- package/src/world2d/collision-2d.ts +1 -1
- package/src/world2d/pixi-game-adapter.ts +19 -17
- package/src/world2d/scene2d-loader.ts +1 -0
- package/src/world2d/types.ts +8 -2
- package/src/animation/anim-graph.ts +0 -406
- package/src/animation/anim-system.ts +0 -28
- package/src/animation/property-track.ts +0 -178
- package/src/animation/schema.ts +0 -204
- package/src/audio/ambient.ts +0 -300
- package/src/audio/impacts.ts +0 -212
- package/src/audio/movement.ts +0 -140
- package/src/audio/musical.ts +0 -200
- package/src/audio/ui-sounds.ts +0 -171
- package/src/audio/vehicle.ts +0 -235
- package/src/audio/weapons.ts +0 -152
- package/src/runtime/scene-ui-bridge.ts +0 -86
- package/src/runtime/scene-ui-data.ts +0 -119
- package/src/scene/schema/ui.ts +0 -602
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D15 — the dev-mode `Math.random` phase trap (`docs/D15-DETERMINISM-DESIGN.md`
|
|
3
|
+
* §2.a enforcer 3, T-D15.3). A project that declares
|
|
4
|
+
* `determinism.seededRandom` is claiming ALL gameplay RNG flows through
|
|
5
|
+
* `ctx.random` — the burn-down scan (`test/gameplay-rng-ban.test.ts`) catches
|
|
6
|
+
* raw `Math.random()`/`Date.now()`/`performance.now()` call sites it can
|
|
7
|
+
* SEE in source; this trap catches what the scan structurally can't: a
|
|
8
|
+
* TRANSITIVE draw made by a third-party library the game calls into during a
|
|
9
|
+
* gameplay tick.
|
|
10
|
+
*
|
|
11
|
+
* Scope is exactly one fixed-step frame (`GameInternal.runFrame`'s whole
|
|
12
|
+
* body — see `game.ts`'s `runFrameImpl`, which brackets its entire call with
|
|
13
|
+
* `profiler.beginFrame()`/`endFrame()`; this trap's `enable()`/`disable()`
|
|
14
|
+
* are called at those exact two points) — code that runs OUTSIDE a frame
|
|
15
|
+
* (module-load-time library init, an unrelated timer callback) never trips
|
|
16
|
+
* it. Warn-once PER CALL SITE (not once ever, not once per call) — a
|
|
17
|
+
* third-party library that draws from several different internal call sites
|
|
18
|
+
* gets one warning per site, so nothing is silently swallowed after the
|
|
19
|
+
* first hit, but a hot per-frame draw from the SAME site doesn't spam.
|
|
20
|
+
* Never throws: a cosmetic third-party draw is legal, just outside the
|
|
21
|
+
* contract (the design doc is explicit about this — "warn, never throw").
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export interface GameplayRngTrap {
|
|
25
|
+
/** Wrap `Math.random` for the duration of one frame — call at frame start. */
|
|
26
|
+
enable(): void;
|
|
27
|
+
/** Restore whatever `Math.random` was immediately before `enable()` — call
|
|
28
|
+
* at frame end. A no-op if something else already replaced `Math.random`
|
|
29
|
+
* out from under this trap between `enable()` and `disable()` (defensive;
|
|
30
|
+
* should not happen in practice). */
|
|
31
|
+
disable(): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Pull a single representative "call site" line out of a captured stack —
|
|
35
|
+
* the warn-once dedupe key. `stack[0]` is the `Error:` header line,
|
|
36
|
+
* `stack[1]` is this trap's OWN wrapper frame — the caller's frame is the
|
|
37
|
+
* next one down. Falls back to a fixed string when `Error().stack` isn't
|
|
38
|
+
* populated (some non-V8 engines) so the trap still functions, just with
|
|
39
|
+
* coarser (single-bucket) deduping. */
|
|
40
|
+
function callSiteFromStack(stack: string | undefined): string {
|
|
41
|
+
if (!stack) return '<unknown call site — no stack captured>';
|
|
42
|
+
const lines = stack.split('\n');
|
|
43
|
+
return (lines[2] ?? lines[1] ?? lines[0] ?? '<unknown call site>').trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Construct a fresh trap. `warn` defaults to `console.warn`; tests (and any
|
|
48
|
+
* caller that wants to assert on the message) can override it to a plain
|
|
49
|
+
* capturing function instead of spying on the global console.
|
|
50
|
+
*/
|
|
51
|
+
export function createGameplayRngTrap(
|
|
52
|
+
warn: (message: string) => void = (message: string) => {
|
|
53
|
+
// biome-ignore lint/suspicious/noConsole: the trap's entire purpose is a structured, greppable dev-mode warning (D15) — this IS the console sink, not incidental debug output.
|
|
54
|
+
console.warn(message);
|
|
55
|
+
},
|
|
56
|
+
): GameplayRngTrap {
|
|
57
|
+
const warnedSites = new Set<string>();
|
|
58
|
+
let previous: (() => number) | undefined;
|
|
59
|
+
|
|
60
|
+
function trapped(): number {
|
|
61
|
+
const site = callSiteFromStack(new Error().stack);
|
|
62
|
+
if (!warnedSites.has(site)) {
|
|
63
|
+
warnedSites.add(site);
|
|
64
|
+
warn(
|
|
65
|
+
// Deliberately NOT the literal substring "Math.random(" (the phrasing
|
|
66
|
+
// below reads fine without it) — this engine repo's OWN gameplay-rng-
|
|
67
|
+
// ban scan (`test/gameplay-rng-ban.test.ts`) greps engine src/ for
|
|
68
|
+
// that exact pattern, and a plain-text mention of the call inside
|
|
69
|
+
// this warning STRING would otherwise false-positive against itself.
|
|
70
|
+
'[determinism] the global Math.random generator was drawn from during a gameplay frame ' +
|
|
71
|
+
'while this project declares determinism.seededRandom — use ctx.random() / ' +
|
|
72
|
+
`ctx.random.stream(name) instead (docs/D15-DETERMINISM-DESIGN.md). Call site:\n${site}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
// `previous` is always set by the time `trapped` can run — `enable()`
|
|
76
|
+
// assigns it before installing `trapped` as `Math.random`, and nothing
|
|
77
|
+
// else can invoke this function reference.
|
|
78
|
+
return previous!();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
enable() {
|
|
83
|
+
previous = Math.random;
|
|
84
|
+
Math.random = trapped;
|
|
85
|
+
},
|
|
86
|
+
disable() {
|
|
87
|
+
if (Math.random === trapped && previous) {
|
|
88
|
+
Math.random = previous;
|
|
89
|
+
previous = undefined;
|
|
90
|
+
}
|
|
91
|
+
// else: something else replaced `Math.random` out from under this trap
|
|
92
|
+
// between `enable()` and `disable()` — leave `previous` untouched.
|
|
93
|
+
// `trapped` may still be reachable via a chain built on top of it (e.g.
|
|
94
|
+
// a third party did `const wrapped = Math.random; Math.random = () =>
|
|
95
|
+
// { ...; return wrapped(); }` while `wrapped` was `trapped`) — nulling
|
|
96
|
+
// `previous` here would make that still-chained call to `trapped()`
|
|
97
|
+
// throw (`previous!()` on undefined) instead of quietly forwarding to
|
|
98
|
+
// the real original generator.
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Game-scoped registry — same WeakMap-on-owner-object pattern as
|
|
105
|
+
// `core/seeded-random.ts`'s `registerSeededRandom`/`getSeededRandom` (and
|
|
106
|
+
// `debug-registry.ts`'s `registerDebugRegistry`/`getDebugRegistry`): the
|
|
107
|
+
// trap's enable/disable calls live inside `game.ts`'s `runFrameImpl`, gated
|
|
108
|
+
// on a boolean only the manifest-aware boot path (`mount-manifest.ts`) knows
|
|
109
|
+
// how to set — `createGame` constructs the trap unconditionally (cheap, does
|
|
110
|
+
// nothing while disabled) and registers a control surface for that boot path
|
|
111
|
+
// to flip once it has resolved `manifest.determinism?.seededRandom`.
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
export interface GameplayRngTrapControl {
|
|
115
|
+
setEnabled(enabled: boolean): void;
|
|
116
|
+
readonly enabled: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const controlByOwner = new WeakMap<object, GameplayRngTrapControl>();
|
|
120
|
+
|
|
121
|
+
/** Called once by `createGame`, alongside `registerSeededRandom`/
|
|
122
|
+
* `registerDebugRegistry`. */
|
|
123
|
+
export function registerGameplayRngTrapControl(
|
|
124
|
+
owner: object,
|
|
125
|
+
control: GameplayRngTrapControl,
|
|
126
|
+
): void {
|
|
127
|
+
controlByOwner.set(owner, control);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** `null` for an owner built without one — same absence precedent as
|
|
131
|
+
* `getSeededRandom`/`getDebugRegistry`. */
|
|
132
|
+
export function getGameplayRngTrapControl(owner: object): GameplayRngTrapControl | null {
|
|
133
|
+
return controlByOwner.get(owner) ?? null;
|
|
134
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The delegating input router (COMPOSITION-DESIGN.md D5 §2a) —
|
|
3
|
-
* `createGameRuntime`'s
|
|
3
|
+
* `createGameRuntime`'s roots path (T6.1 slice 1,
|
|
4
4
|
* docs/MULTI-WORLD-DESIGN.md §1.C) uses this to route pointer input across
|
|
5
5
|
* N stacked world canvases with exactly ONE root listener.
|
|
6
6
|
*
|
|
7
7
|
* Split into two layers so the decision logic is unit-testable with plain
|
|
8
|
-
* fake
|
|
8
|
+
* fake roots — no real DOM/canvas needed:
|
|
9
9
|
*
|
|
10
10
|
* - `stackOrder` / `resolveClaimingWorld`: pure functions over
|
|
11
11
|
* `{id, zOrder, hitTest?}` — "who claims this point".
|
|
@@ -26,21 +26,21 @@ export interface ClaimEntry {
|
|
|
26
26
|
/**
|
|
27
27
|
* Optional claim predicate over a point RELATIVE TO THE CONTAINER. Absent
|
|
28
28
|
* means: "this world claims only if it is the bottom (lowest zOrder)
|
|
29
|
-
* world" — D5 §2a's stated default ("transparent upper
|
|
29
|
+
* world" — D5 §2a's stated default ("transparent upper roots do not
|
|
30
30
|
* claim, the bottom world claims everything").
|
|
31
31
|
*/
|
|
32
32
|
readonly hitTest?: ((x: number, y: number) => boolean) | undefined;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/** The DOM-wiring inputs — one real canvas per world. */
|
|
36
|
-
export interface
|
|
36
|
+
export interface RouterAdapterRoot extends ClaimEntry {
|
|
37
37
|
readonly canvas: HTMLCanvasElement;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Bottom-to-top paint order: ascending `zOrder`, ties -> original array
|
|
42
42
|
* order — the SAME rule `manifest/load.ts`'s `loadGameManifest` sorts
|
|
43
|
-
*
|
|
43
|
+
* roots by, so a world's DOM stacking position always matches its
|
|
44
44
|
* manifest-resolved zOrder. The last element is the topmost (visually on
|
|
45
45
|
* top / highest z-index).
|
|
46
46
|
*/
|
|
@@ -83,7 +83,7 @@ export function resolveClaimingWorld(
|
|
|
83
83
|
* DOM-hit-tests it — the router's JS-level `hitTest` is what still lets it
|
|
84
84
|
* claim a point via `createInputRouter`'s forwarding).
|
|
85
85
|
*/
|
|
86
|
-
export function applyPointerEventsStacking(entries: readonly
|
|
86
|
+
export function applyPointerEventsStacking(entries: readonly RouterAdapterRoot[]): void {
|
|
87
87
|
const order = stackOrder(entries);
|
|
88
88
|
order.forEach((entry, i) => {
|
|
89
89
|
entry.canvas.style.pointerEvents = i === 0 ? 'auto' : 'none';
|
|
@@ -179,7 +179,7 @@ export interface InputRouterHandle {
|
|
|
179
179
|
*/
|
|
180
180
|
export function createInputRouter(
|
|
181
181
|
container: HTMLElement,
|
|
182
|
-
entries: readonly
|
|
182
|
+
entries: readonly RouterAdapterRoot[],
|
|
183
183
|
): InputRouterHandle {
|
|
184
184
|
applyPointerEventsStacking(entries);
|
|
185
185
|
const byId = new Map(entries.map((e) => [e.id, e] as const));
|
|
@@ -4,22 +4,22 @@
|
|
|
4
4
|
// duplicates its per-kind dispatch/validation) to add the one thing that
|
|
5
5
|
// module deliberately does NOT have: a KIND REGISTRY, so a project doesn't
|
|
6
6
|
// have to hand-build an `entries` map inline in its own `main.ts` every time
|
|
7
|
-
// — it registers a `
|
|
7
|
+
// — it registers a `AdapterSurfaceFactory` per kind it uses (once, at module load)
|
|
8
8
|
// and then mounts with a single `mountGameFromManifest(manifest, host)` call.
|
|
9
9
|
//
|
|
10
10
|
// Engine-core react/pixi-free discipline (mirrors `mount-manifest.ts`'s own
|
|
11
11
|
// header comment, and the HARD INVARIANT this file was built under): this
|
|
12
|
-
// registry is pure mechanism — it holds whatever `
|
|
12
|
+
// registry is pure mechanism — it holds whatever `AdapterSurfaceFactory` functions
|
|
13
13
|
// callers register, but it registers NONE itself. In particular there is no
|
|
14
14
|
// built-in 'pixijs'/'react' registration here (that would require this file
|
|
15
15
|
// to value-import `pixi.js`/`react`/`react-dom`, exactly what `mount-manifest
|
|
16
16
|
// .ts` forbids); a project that has a pixijs or react world MUST register a
|
|
17
|
-
// factory for that kind itself — see `
|
|
17
|
+
// factory for that kind itself — see `defaultThreeAdapterFactory` below for why
|
|
18
18
|
// even the 'threejs' convenience export is a plain function the CALLER
|
|
19
19
|
// registers, not something this module wires up automatically.
|
|
20
20
|
|
|
21
21
|
import type { GameAdapter } from '../adapter/game-adapter';
|
|
22
|
-
import type {
|
|
22
|
+
import type { ResolvedAdapterRoot } from '../manifest/load';
|
|
23
23
|
import type { ComponentRegistry } from '../scene/component-registry';
|
|
24
24
|
import type { GameSession } from './create-runtime';
|
|
25
25
|
import { type MountEntry, mountManifestWorlds, resolveManifest } from './mount-manifest';
|
|
@@ -42,7 +42,7 @@ export interface ManifestHost {
|
|
|
42
42
|
/**
|
|
43
43
|
* Resolve a manifest-declared `entry` (a project-relative module path,
|
|
44
44
|
* e.g. `"src/scripts/main.ts"`) to the ALREADY-IMPORTED module namespace a
|
|
45
|
-
* registered {@link
|
|
45
|
+
* registered {@link AdapterSurfaceFactory} needs to build a `MountEntry` from.
|
|
46
46
|
*
|
|
47
47
|
* Deliberately NOT "import this arbitrary string path" — a static bundler
|
|
48
48
|
* (Vite/rollup building a `dist/`) cannot resolve a runtime-computed import
|
|
@@ -55,7 +55,7 @@ export interface ManifestHost {
|
|
|
55
55
|
*/
|
|
56
56
|
loadEntryModule?(path: string): Promise<unknown>;
|
|
57
57
|
/** Resolve asset/scene URLs relative to the project — forwarded to a
|
|
58
|
-
* registered {@link
|
|
58
|
+
* registered {@link AdapterSurfaceFactory} via {@link AdapterSurfaceFactoryContext},
|
|
59
59
|
* never called by this module itself (a factory only needs it for a
|
|
60
60
|
* non-default resolution scheme, e.g. a hosted-editor iframe). */
|
|
61
61
|
resolveUrl?(path: string): string;
|
|
@@ -66,8 +66,8 @@ export interface ManifestHost {
|
|
|
66
66
|
readonly headless?: boolean | undefined;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/** What a registered {@link
|
|
70
|
-
export interface
|
|
69
|
+
/** What a registered {@link AdapterSurfaceFactory} is handed for one world. */
|
|
70
|
+
export interface AdapterSurfaceFactoryContext {
|
|
71
71
|
readonly host: ManifestHost;
|
|
72
72
|
/**
|
|
73
73
|
* The result of `host.loadEntryModule(world.entry)`, if the world declares
|
|
@@ -85,19 +85,19 @@ export interface WorldKindFactoryContext {
|
|
|
85
85
|
* `examples/tri-world/src/main.ts`'s pre-E4 shape). May be async (loading an
|
|
86
86
|
* asset, fetching a scene file, etc.).
|
|
87
87
|
*/
|
|
88
|
-
export type
|
|
89
|
-
world:
|
|
90
|
-
ctx:
|
|
88
|
+
export type AdapterSurfaceFactory = (
|
|
89
|
+
world: ResolvedAdapterRoot,
|
|
90
|
+
ctx: AdapterSurfaceFactoryContext,
|
|
91
91
|
) => MountEntry | Promise<MountEntry>;
|
|
92
92
|
|
|
93
93
|
// ---------------------------------------------------------------------------
|
|
94
94
|
// The registry
|
|
95
95
|
// ---------------------------------------------------------------------------
|
|
96
96
|
|
|
97
|
-
const registry = new Map<string,
|
|
97
|
+
const registry = new Map<string, AdapterSurfaceFactory>();
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
* Register a `
|
|
100
|
+
* Register a `AdapterSurfaceFactory` for a manifest world `kind` (`'threejs'` /
|
|
101
101
|
* `'pixijs'` / `'react'`, or any future kind the manifest schema grows).
|
|
102
102
|
* Call once, at module load — mirrors `registerSceneUIRenderer()`'s existing
|
|
103
103
|
* "call before mounting" convention (`packages/editor/template/src/main.ts`).
|
|
@@ -105,14 +105,14 @@ const registry = new Map<string, WorldKindFactory>();
|
|
|
105
105
|
* Throws on double-registration of the SAME kind: an accidental duplicate
|
|
106
106
|
* (two side-effect imports of the same registration module, or a copy-paste)
|
|
107
107
|
* is far more likely than an intentional runtime swap — a caller that really
|
|
108
|
-
* wants to replace a registration must `
|
|
108
|
+
* wants to replace a registration must `unregisterAdapter` first, making
|
|
109
109
|
* the intent explicit.
|
|
110
110
|
*/
|
|
111
|
-
export function
|
|
111
|
+
export function registerAdapter(kind: string, factory: AdapterSurfaceFactory): void {
|
|
112
112
|
if (registry.has(kind)) {
|
|
113
113
|
throw new Error(
|
|
114
|
-
`
|
|
115
|
-
'call
|
|
114
|
+
`registerAdapter: a factory is already registered for kind "${kind}" — ` +
|
|
115
|
+
'call unregisterAdapter(kind) first if you intend to replace it (an accidental ' +
|
|
116
116
|
'double-registration, e.g. two side-effect imports of the same registration module, ' +
|
|
117
117
|
'is far more common than an intentional swap).',
|
|
118
118
|
);
|
|
@@ -121,12 +121,12 @@ export function registerWorldKind(kind: string, factory: WorldKindFactory): void
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/** Remove a kind's registration (e.g. before re-registering a replacement). */
|
|
124
|
-
export function
|
|
124
|
+
export function unregisterAdapter(kind: string): void {
|
|
125
125
|
registry.delete(kind);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
/** Whether a factory is currently registered for `kind`. */
|
|
129
|
-
export function
|
|
129
|
+
export function isAdapterRegistered(kind: string): boolean {
|
|
130
130
|
return registry.has(kind);
|
|
131
131
|
}
|
|
132
132
|
|
|
@@ -136,17 +136,17 @@ export function isWorldKindRegistered(kind: string): boolean {
|
|
|
136
136
|
* call this; it exists so unit tests can start each case from a clean
|
|
137
137
|
* registry without cross-test leakage.
|
|
138
138
|
*/
|
|
139
|
-
export function
|
|
139
|
+
export function __clearAdapterSurfaceRegistryForTests(): void {
|
|
140
140
|
registry.clear();
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
// ---------------------------------------------------------------------------
|
|
144
|
-
// `
|
|
144
|
+
// `defaultThreeAdapterFactory` — an OPT-IN convenience, never auto-registered
|
|
145
145
|
// ---------------------------------------------------------------------------
|
|
146
146
|
|
|
147
147
|
/** The entry-module export shape every first-party entry module follows
|
|
148
|
-
* (
|
|
149
|
-
*
|
|
148
|
+
* (an explicit world entry module, including `examples/*` projects'
|
|
149
|
+
* `src/index.ts` — see CLAUDE.md's "Adding an example project"). */
|
|
150
150
|
interface ThreeEntryModuleExports {
|
|
151
151
|
readonly adapter?: GameAdapter;
|
|
152
152
|
readonly setup?: GameSetupFn;
|
|
@@ -154,7 +154,7 @@ interface ThreeEntryModuleExports {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
|
-
* A ready-to-register {@link
|
|
157
|
+
* A ready-to-register {@link AdapterSurfaceFactory} for `kind: 'threejs'` roots
|
|
158
158
|
* that follow the first-party entry-module convention (`export const
|
|
159
159
|
* adapter` and/or `export async function setup`) or are plain scene files
|
|
160
160
|
* with an optional named `componentRegistry` export. Threejs is safe to
|
|
@@ -162,7 +162,7 @@ interface ThreeEntryModuleExports {
|
|
|
162
162
|
* `VgaiSceneGameAdapter` are already unconditional engine dependencies —
|
|
163
163
|
* see `mount-manifest.ts`'s header comment. Still never auto-registered:
|
|
164
164
|
* the registry itself stays zero-policy (see this file's header comment) —
|
|
165
|
-
* a caller opts in with `
|
|
165
|
+
* a caller opts in with `registerAdapter('threejs', defaultThreeAdapterFactory)`.
|
|
166
166
|
*
|
|
167
167
|
* Resolution mirrors `mount-manifest.ts`'s own `resolveThreeAdapter` branch
|
|
168
168
|
* order: an entry module's `adapter` export wins outright; else its `setup`
|
|
@@ -171,15 +171,15 @@ interface ThreeEntryModuleExports {
|
|
|
171
171
|
* `entry` at all) a scene-driven world, forwarding `componentRegistry` if
|
|
172
172
|
* the entry module supplied one.
|
|
173
173
|
*/
|
|
174
|
-
export const
|
|
174
|
+
export const defaultThreeAdapterFactory: AdapterSurfaceFactory = (world, ctx) => {
|
|
175
175
|
const mod = ctx.entryModule as ThreeEntryModuleExports | undefined;
|
|
176
176
|
if (mod?.adapter) return { kind: 'threejs', adapter: mod.adapter };
|
|
177
177
|
if (mod?.setup) return { kind: 'threejs', setup: mod.setup };
|
|
178
178
|
if (world.entry !== undefined) {
|
|
179
179
|
throw new Error(
|
|
180
|
-
`
|
|
180
|
+
`defaultThreeAdapterFactory: entry module "${world.entry}" for world "${world.id}" exports ` +
|
|
181
181
|
'neither `adapter` nor `setup` — every first-party entry module exports at least one ' +
|
|
182
|
-
'(
|
|
182
|
+
'(export one from the declared entry module).',
|
|
183
183
|
);
|
|
184
184
|
}
|
|
185
185
|
return { kind: 'threejs', componentRegistry: mod?.componentRegistry };
|
|
@@ -203,13 +203,15 @@ export interface MountGameOptions {
|
|
|
203
203
|
* nothing beyond the manifest itself). Every other shape (pixijs, react, an
|
|
204
204
|
* entry-declaring or module/ingest-adapter threejs world) needs SOMETHING
|
|
205
205
|
* in `entries[id]` to succeed. */
|
|
206
|
-
function worldIsSelfSufficient(world:
|
|
207
|
-
return
|
|
206
|
+
function worldIsSelfSufficient(world: ResolvedAdapterRoot): boolean {
|
|
207
|
+
return (
|
|
208
|
+
world.surface === 'threejs' && world.adapter.type === 'builtin' && world.entry === undefined
|
|
209
|
+
);
|
|
208
210
|
}
|
|
209
211
|
|
|
210
212
|
/**
|
|
211
213
|
* Mount every world declared by a `vgai.game.json` manifest onto `host
|
|
212
|
-
* .container`, resolving each world's kind through the `
|
|
214
|
+
* .container`, resolving each world's kind through the `registerAdapter`
|
|
213
215
|
* registry — falling back to an explicit `opts.entries[id]` where supplied,
|
|
214
216
|
* exactly like `mountManifestWorlds` (so a caller can migrate one world at a
|
|
215
217
|
* time, or never touch the registry if it prefers hand-building entries).
|
|
@@ -224,7 +226,7 @@ function worldIsSelfSufficient(world: ResolvedWorldEntry): boolean {
|
|
|
224
226
|
* into E4): a world whose kind has no registered factory AND no explicit
|
|
225
227
|
* entry, and which cannot self-mount from the manifest alone, throws a named
|
|
226
228
|
* `Error` identifying the world id + kind and BOTH ways to fix it
|
|
227
|
-
* (`
|
|
229
|
+
* (`registerAdapter` or `opts.entries`) — never a silent skip.
|
|
228
230
|
*/
|
|
229
231
|
export async function mountGameFromManifest(
|
|
230
232
|
manifestInput: unknown,
|
|
@@ -234,10 +236,10 @@ export async function mountGameFromManifest(
|
|
|
234
236
|
const manifest = resolveManifest(manifestInput);
|
|
235
237
|
const entries: Record<string, MountEntry> = { ...opts.entries };
|
|
236
238
|
|
|
237
|
-
for (const world of manifest.
|
|
239
|
+
for (const world of manifest.roots) {
|
|
238
240
|
if (entries[world.id] !== undefined) continue; // explicit entry always wins
|
|
239
241
|
|
|
240
|
-
const factory = registry.get(world.
|
|
242
|
+
const factory = registry.get(world.adapter.identity);
|
|
241
243
|
if (factory) {
|
|
242
244
|
const entryModule =
|
|
243
245
|
world.entry !== undefined && host.loadEntryModule
|
|
@@ -250,10 +252,10 @@ export async function mountGameFromManifest(
|
|
|
250
252
|
if (worldIsSelfSufficient(world)) continue; // mountManifestWorlds needs nothing further
|
|
251
253
|
|
|
252
254
|
throw new Error(
|
|
253
|
-
`mountGameFromManifest:
|
|
254
|
-
`
|
|
255
|
-
`
|
|
256
|
-
'`
|
|
255
|
+
`mountGameFromManifest: root "${world.id}" (adapter "${world.adapter.identity}") has no registered ` +
|
|
256
|
+
`adapter factory and no explicit entries["${world.id}"] — call ` +
|
|
257
|
+
`registerAdapter("${world.adapter.identity}", factory) before mounting (see ` +
|
|
258
|
+
'`defaultThreeAdapterFactory` for a worked threejs example), or pass ' +
|
|
257
259
|
`opts.entries["${world.id}"] directly (see mount-manifest.ts's \`MountEntry\`).`,
|
|
258
260
|
);
|
|
259
261
|
}
|