@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.
Files changed (109) hide show
  1. package/dist/adapter/adapter-module.d.ts +1 -16
  2. package/dist/adapter/adapter-module.d.ts.map +1 -1
  3. package/dist/adapter/adapter-module.js +1 -11
  4. package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts +0 -2
  5. package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -1
  6. package/dist/adapter/finders/scenes-from-entrypoint-selection.js +3 -6
  7. package/dist/adapter/renderer-config.d.ts +83 -0
  8. package/dist/adapter/renderer-config.d.ts.map +1 -0
  9. package/dist/adapter/renderer-config.js +121 -0
  10. package/dist/adapter/root-adapter.d.ts +6 -2
  11. package/dist/adapter/root-adapter.d.ts.map +1 -1
  12. package/dist/adapter/root-seam-contract.d.ts +5 -0
  13. package/dist/adapter/root-seam-contract.d.ts.map +1 -1
  14. package/dist/adapter/root-seam-contract.js +1 -0
  15. package/dist/adapter/setup-three-root-adapter.js +9 -9
  16. package/dist/canvas-react/pixi-react-adapter.d.ts.map +1 -1
  17. package/dist/canvas-react/pixi-react-adapter.js +92 -41
  18. package/dist/data/curve.d.ts +1 -1
  19. package/dist/data/curve.js +2 -2
  20. package/dist/dev/instruments.d.ts +4 -4
  21. package/dist/dev/instruments.js +4 -4
  22. package/dist/dev/register-render-vitals.d.ts +2 -2
  23. package/dist/dev/register-render-vitals.js +2 -2
  24. package/dist/dev/static-batch-advisor.d.ts +1 -1
  25. package/dist/dev/static-batch-advisor.js +2 -2
  26. package/dist/input/input-manager.d.ts +15 -0
  27. package/dist/input/input-manager.d.ts.map +1 -1
  28. package/dist/input/input-manager.js +22 -0
  29. package/dist/manifest/load.d.ts +0 -3
  30. package/dist/manifest/load.d.ts.map +1 -1
  31. package/dist/manifest/load.js +0 -4
  32. package/dist/manifest/schema.d.ts +0 -2
  33. package/dist/manifest/schema.d.ts.map +1 -1
  34. package/dist/manifest/schema.js +3 -24
  35. package/dist/pixi/authoring.d.ts +33 -0
  36. package/dist/pixi/authoring.d.ts.map +1 -1
  37. package/dist/pixi/authoring.js +86 -6
  38. package/dist/react/world-state.d.ts +12 -0
  39. package/dist/react/world-state.d.ts.map +1 -1
  40. package/dist/react/world-state.js +14 -0
  41. package/dist/render/soft-particle-depth.d.ts +3 -3
  42. package/dist/render/soft-particle-depth.d.ts.map +1 -1
  43. package/dist/render/soft-particle-depth.js +8 -13
  44. package/dist/runtime/create-runtime.d.ts +2 -16
  45. package/dist/runtime/create-runtime.d.ts.map +1 -1
  46. package/dist/runtime/create-runtime.js +36 -38
  47. package/dist/runtime/dev-build.d.ts +37 -0
  48. package/dist/runtime/dev-build.d.ts.map +1 -0
  49. package/dist/runtime/dev-build.js +49 -0
  50. package/dist/runtime/game.d.ts +9 -1
  51. package/dist/runtime/game.d.ts.map +1 -1
  52. package/dist/runtime/game.js +25 -1
  53. package/dist/runtime/mount-manifest.d.ts +0 -13
  54. package/dist/runtime/mount-manifest.d.ts.map +1 -1
  55. package/dist/runtime/mount-manifest.js +2 -21
  56. package/dist/world3d-react/engine-bridge.d.ts.map +1 -1
  57. package/dist/world3d-react/engine-bridge.js +28 -3
  58. package/dist/world3d-react/index.d.ts +2 -2
  59. package/dist/world3d-react/index.d.ts.map +1 -1
  60. package/dist/world3d-react/index.js +2 -2
  61. package/dist/world3d-react/r3f-adapter.d.ts +2 -2
  62. package/dist/world3d-react/r3f-adapter.d.ts.map +1 -1
  63. package/dist/world3d-react/r3f-adapter.js +90 -15
  64. package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
  65. package/dist/world3d-react/r3f-root-factory.js +12 -2
  66. package/dist/world3d-react/rapier-physics-bridge.d.ts +4 -4
  67. package/dist/world3d-react/rapier-physics-bridge.js +3 -3
  68. package/dist/world3d-react/renderer-config.d.ts +7 -69
  69. package/dist/world3d-react/renderer-config.d.ts.map +1 -1
  70. package/dist/world3d-react/renderer-config.js +7 -108
  71. package/dist/world3d-react/world-context.d.ts +43 -0
  72. package/dist/world3d-react/world-context.d.ts.map +1 -1
  73. package/dist/world3d-react/world-context.js +89 -46
  74. package/dist-config/config.js.map +1 -1
  75. package/package.json +1 -1
  76. package/schemas/engine-capabilities.json +3 -3
  77. package/schemas/vgai-project.schema.json +2 -7
  78. package/src/adapter/adapter-module.ts +1 -25
  79. package/src/adapter/finders/scenes-from-entrypoint-selection.ts +3 -8
  80. package/src/adapter/renderer-config.ts +178 -0
  81. package/src/adapter/root-adapter.ts +6 -2
  82. package/src/adapter/root-seam-contract.ts +1 -0
  83. package/src/adapter/setup-three-root-adapter.ts +9 -9
  84. package/src/canvas-react/pixi-react-adapter.tsx +98 -53
  85. package/src/data/curve.ts +2 -2
  86. package/src/dev/instruments.ts +4 -4
  87. package/src/dev/register-render-vitals.ts +2 -2
  88. package/src/dev/static-batch-advisor.ts +2 -2
  89. package/src/input/input-manager.ts +23 -0
  90. package/src/manifest/load.ts +0 -7
  91. package/src/manifest/schema.ts +3 -26
  92. package/src/pixi/authoring.ts +98 -6
  93. package/src/react/world-state.tsx +15 -0
  94. package/src/render/soft-particle-depth.ts +8 -13
  95. package/src/runtime/create-runtime.ts +38 -52
  96. package/src/runtime/dev-build.ts +49 -0
  97. package/src/runtime/game.ts +31 -2
  98. package/src/runtime/mount-manifest.ts +2 -33
  99. package/src/world3d-react/engine-bridge.ts +28 -3
  100. package/src/world3d-react/index.ts +5 -5
  101. package/src/world3d-react/r3f-adapter.tsx +89 -20
  102. package/src/world3d-react/r3f-root-factory.tsx +12 -2
  103. package/src/world3d-react/rapier-physics-bridge.tsx +4 -4
  104. package/src/world3d-react/renderer-config.ts +7 -164
  105. package/src/world3d-react/world-context.ts +100 -51
  106. package/dist/runtime/dev-layers.d.ts +0 -34
  107. package/dist/runtime/dev-layers.d.ts.map +0 -1
  108. package/dist/runtime/dev-layers.js +0 -46
  109. package/src/runtime/dev-layers.ts +0 -46
@@ -3,7 +3,7 @@
3
3
  * `@react-three/rapier` physics VISIBLE to the engine's editor/dev seams.
4
4
  *
5
5
  * WHY IT EXISTS. `SystemAdapters.physics` is what the editor's transform
6
- * coordination (`freeze → apply → unfreeze`) and the dev menu's universal
6
+ * coordination (`freeze → apply → unfreeze`) and the universal
7
7
  * "Colliders drawn" instrument (`@engine/dev/instruments`) read. The
8
8
  * first-party imperative three root registers one from
9
9
  * `../adapter/setup-three-root-adapter.ts`; an R3F world builds no first-party
@@ -44,7 +44,7 @@
44
44
  * on the R3F root it is mounted in. It allocates nothing else — no timers, no
45
45
  * subscriptions, no per-frame work (see the phase note below), and no Rapier
46
46
  * objects: the bodies it edits belong to `<Physics>`.
47
- * SHARERS: every reader of `game.systemAdapters.physics` — the dev menu's
47
+ * SHARERS: every reader of `game.systemAdapters.physics` — the
48
48
  * colliders instrument, the editor's transform coordination. They read; they
49
49
  * never dispose.
50
50
  * TEARDOWN: the effect's cleanup, and nothing else — it clears the SAME key it
@@ -55,7 +55,7 @@
55
55
  *
56
56
  * PHASE ORDERING: none to respect — this component registers pull-only
57
57
  * callbacks and runs no per-frame work. Every method it exposes is invoked
58
- * BETWEEN frames, by an editor gesture or a dev-menu command, never from a
58
+ * BETWEEN frames, by an editor gesture or an instrument command, never from a
59
59
  * phase.
60
60
  */
61
61
 
@@ -78,7 +78,7 @@ import { useGameContext } from './engine-bridge';
78
78
  export interface RapierPhysicsBridgeProps {
79
79
  /**
80
80
  * Set the `debug` prop of the SAME `<Physics>` this bridge is mounted in.
81
- * The dev menu's "Colliders drawn" toggle calls it; the world's own state is
81
+ * The "Colliders drawn" instrument calls it; the world's own state is
82
82
  * what `<Physics debug={…}>` reads, so the library's debug rendering is the
83
83
  * only thing that ever draws a collider.
84
84
  */
@@ -1,168 +1,11 @@
1
1
  /**
2
- * world3d-react/renderer-config.tsthe seam a WORLD uses to state how its own frame is rendered.
2
+ * `@engine/world3d-react/renderer-config` — a published entry point.
3
3
  *
4
- * The host owns the `WebGLRenderer` (`ThreeHostContext.renderer`) and configures it with this
5
- * engine's defaults: ACES tone mapping, sRGB output, PCF-soft shadows. Those defaults are right for
6
- * a world authored against them and WRONG for a world that was authored against a different
7
- * engine's pipeline an imported Godot 3 GLES2 game does gamma-space lighting with no tonemapper
8
- * at all, so ACES quietly desaturates and darkens every colour its author picked.
9
- *
10
- * So a world may DECLARE the pipeline it was authored for, and `createR3FAdapter` applies it to the
11
- * host's renderer for the life of the mount, restoring what it found on dispose. Three properties
12
- * of that shape are load-bearing:
13
- *
14
- * - **It is per-renderer, never engine-wide.** Every field here is a `WebGLRenderer` instance
15
- * property, and a play root gets its own renderer (`create-runtime.ts`). Nothing here reaches a
16
- * module-level three global, so one world's declaration cannot change how the editor's own
17
- * viewport, another root, or a thumbnail bake renders.
18
- * - **Absent means "leave the host's value alone".** Every field is optional and an omitted one
19
- * is never written, so declaring a tone mapping does not silently reset the clear colour.
20
- * - **It is restored on dispose.** The renderer outlives the mount, so a world that did not put
21
- * back what it found would leak its pipeline into whatever mounts next.
22
- *
23
- * This is deliberately NOT a general render-settings system. It carries what a world can honestly
24
- * state about its own colour pipeline and nothing else; a property the host fixes at CONSTRUCTION
25
- * (the WebGL context's `antialias` attribute, and therefore the MSAA sample count) cannot be
26
- * declared here, because there would be no honest moment to apply it.
27
- */
28
-
29
- import type * as THREE from 'three';
30
-
31
- /** The tone-mapping operators three exposes, named as data rather than as three's numeric enum. */
32
- export type WorldToneMapping =
33
- | 'none'
34
- | 'linear'
35
- | 'reinhard'
36
- | 'cineon'
37
- | 'aces'
38
- | 'agx'
39
- | 'neutral';
40
-
41
- /**
42
- * The output transfer function the frame is written with.
43
- *
44
- * - `srgb` — three's own default and this engine's: linear lighting, sRGB encode on output.
45
- * - `srgb-linear` — NO output transform. This is what a gamma-space renderer needs: the shading
46
- * result is already in display space and encoding it a second time washes the frame out.
47
- */
48
- export type WorldOutputColorSpace = 'srgb' | 'srgb-linear';
49
-
50
- /**
51
- * The shadow-map filter, named as data rather than as three's numeric enum.
52
- *
53
- * This is a renderer INSTANCE property (`WebGLRenderer.shadowMap.type`), not a context attribute,
54
- * so unlike MSAA it has an honest moment at which a world can ask for it — which is the whole test
55
- * this file's header states. A source engine that declares its own shadow filter (Godot 3's
56
- * `rendering/quality/shadows/filter_mode`) would otherwise inherit whatever the host built with.
57
- */
58
- export type WorldShadowMapType = 'basic' | 'pcf' | 'pcf-soft' | 'vsm';
59
-
60
- /** What a world may declare about the renderer that draws it. Every field is optional; see header. */
61
- export interface WorldRendererConfig {
62
- readonly toneMapping?: WorldToneMapping | undefined;
63
- readonly toneMappingExposure?: number | undefined;
64
- readonly outputColorSpace?: WorldOutputColorSpace | undefined;
65
- /**
66
- * `WebGLRenderer.shadowMap.type`. Writing it after a shadow map has already been built needs
67
- * `shadowMap.needsUpdate`, which this function sets — three caches the compiled depth material
68
- * per type and would otherwise keep filtering with the previous one.
69
- */
70
- readonly shadowMapType?: WorldShadowMapType | undefined;
71
- /**
72
- * The colour the frame is cleared to, as a CSS hex string. The renderer's existing clear ALPHA
73
- * is preserved: a stacked canvas is transparent on purpose (`create-runtime.ts` gives every
74
- * non-bottom root `alpha: true`), and forcing it opaque here would hide every layer below.
75
- */
76
- readonly clearColor?: string | undefined;
77
- }
78
-
79
- /**
80
- * Apply `config` to `renderer`, returning the restore function that puts back what was there.
81
- *
82
- * `three` is passed in rather than imported for values so the enum constants come from the HOST's
83
- * three instance — the same identity rule `r3f-adapter.tsx` follows for the scene and camera.
4
+ * The engine package's export map is the wildcard `"./*"`, so every file under
5
+ * `packages/engine/src/` is an entry point a game outside this repo can import by path. The module
6
+ * itself now lives in the adapter seam (`@engine/adapter/renderer-config`, which its header
7
+ * explains), and this file keeps the path that shipped resolving to it. Import the seam path in
8
+ * new code.
84
9
  */
85
- export function applyWorldRendererConfig(
86
- three: typeof THREE,
87
- renderer: THREE.WebGLRenderer,
88
- config: WorldRendererConfig,
89
- ): () => void {
90
- // A host that mounts a world WITHOUT rasterizing it hands the adapter a duck-typed renderer —
91
- // the editor's design session (`createDesignTimeRenderer`: four members, deliberately never
92
- // widened) and jsdom test harnesses both do. Such a surface has no colour pipeline to configure:
93
- // the frame the user sees is drawn by a DIFFERENT renderer (the editor's own), so applying the
94
- // world's config there is meaningless — and calling `getClearColor` on it is a TypeError that
95
- // unmounts the whole world at edit time (measured: every Godot port's edit viewport blanked with
96
- // '"world" failed to mount — renderer.getClearColor is not a function'). Detect the real
97
- // `WebGLRenderer` surface by the one method this function must call, and no-op otherwise.
98
- if (typeof renderer.getClearColor !== 'function') {
99
- return () => {};
100
- }
101
- const toneMappings: Record<WorldToneMapping, THREE.ToneMapping> = {
102
- none: three.NoToneMapping,
103
- linear: three.LinearToneMapping,
104
- reinhard: three.ReinhardToneMapping,
105
- cineon: three.CineonToneMapping,
106
- aces: three.ACESFilmicToneMapping,
107
- agx: three.AgXToneMapping,
108
- neutral: three.NeutralToneMapping,
109
- };
110
- const colorSpaces: Record<WorldOutputColorSpace, THREE.ColorSpace> = {
111
- srgb: three.SRGBColorSpace,
112
- 'srgb-linear': three.LinearSRGBColorSpace,
113
- };
114
- const shadowMapTypes: Record<WorldShadowMapType, THREE.ShadowMapType> = {
115
- basic: three.BasicShadowMap,
116
- pcf: three.PCFShadowMap,
117
- 'pcf-soft': three.PCFSoftShadowMap,
118
- vsm: three.VSMShadowMap,
119
- };
120
-
121
- const restores: (() => void)[] = [];
122
-
123
- if (config.toneMapping !== undefined) {
124
- const previous = renderer.toneMapping;
125
- renderer.toneMapping = toneMappings[config.toneMapping];
126
- restores.push(() => {
127
- renderer.toneMapping = previous;
128
- });
129
- }
130
- if (config.toneMappingExposure !== undefined) {
131
- const previous = renderer.toneMappingExposure;
132
- renderer.toneMappingExposure = config.toneMappingExposure;
133
- restores.push(() => {
134
- renderer.toneMappingExposure = previous;
135
- });
136
- }
137
- if (config.outputColorSpace !== undefined) {
138
- const previous = renderer.outputColorSpace;
139
- renderer.outputColorSpace = colorSpaces[config.outputColorSpace];
140
- restores.push(() => {
141
- renderer.outputColorSpace = previous;
142
- });
143
- }
144
- if (config.shadowMapType !== undefined && renderer.shadowMap !== undefined) {
145
- const previous = renderer.shadowMap.type;
146
- renderer.shadowMap.type = shadowMapTypes[config.shadowMapType];
147
- renderer.shadowMap.needsUpdate = true;
148
- restores.push(() => {
149
- renderer.shadowMap.type = previous;
150
- renderer.shadowMap.needsUpdate = true;
151
- });
152
- }
153
- if (config.clearColor !== undefined) {
154
- const previousColor = new three.Color();
155
- renderer.getClearColor(previousColor);
156
- // Alpha is READ BACK and re-passed, never assumed: see `clearColor`'s doc above.
157
- const alpha = renderer.getClearAlpha();
158
- renderer.setClearColor(new three.Color(config.clearColor), alpha);
159
- restores.push(() => {
160
- renderer.setClearColor(previousColor, alpha);
161
- });
162
- }
163
10
 
164
- return () => {
165
- // Reverse order, so a field written twice (it cannot be, today) unwinds correctly.
166
- for (let i = restores.length - 1; i >= 0; i--) restores[i]?.();
167
- };
168
- }
11
+ export * from '../adapter/renderer-config';
@@ -147,6 +147,103 @@ export interface R3FRootRuntime {
147
147
  dispose(): void;
148
148
  }
149
149
 
150
+ /**
151
+ * The GAME-SCOPED input wiring one three root owns — and the half that has
152
+ * nothing to do with the vgai runtime context.
153
+ *
154
+ * Two things happen here, both of them HOST work performed from outside the
155
+ * world's own tree:
156
+ *
157
+ * 1. the project's input map is loaded through `Game.loadInputMap` (game-owned,
158
+ * load-once), so the actions the project declared exist;
159
+ * 2. this root's seams are registered on the game-scoped debug registry
160
+ * (`setVirtualInputTarget`/`setInputActionsSource`/`setInputTraceSource`),
161
+ * which is what makes `game.input.hold/tap/...` (the bot/`vgai eval` input
162
+ * doctrine) and the built-in `input.actions`/`input.trace` providers
163
+ * resolve to this world instead of throwing `DEBUG_INPUT_UNAVAILABLE`.
164
+ *
165
+ * It is called on BOTH three-root paths, and that is the point: a world that
166
+ * mounts as a default-exported component receives no vgai runtime context
167
+ * (`engineRuntime: false` — see `r3f-root-factory.tsx`), but "no context in the
168
+ * tree" was never meant to say "the host does not wire this game's input". It
169
+ * was read that way, and the cost was measured on a fresh scaffold: the
170
+ * declared map never loaded and every `game.input.*` door threw, so the QA
171
+ * bot, autoplay routes and `vgai eval` could not drive the ONE world shape
172
+ * `vgai create` produces. Same split, same reasoning, as `WorldProvider`
173
+ * riding `host.game` rather than the runtime (see `r3f-adapter.tsx`).
174
+ *
175
+ * `optionalInputMap` is the CONVENTIONAL-PATH probe used when the project
176
+ * never named a map: an absent file is the ordinary state of a brand-new
177
+ * project, so it resolves quietly instead of printing an error on every boot.
178
+ * A map that exists and fails to parse is as loud as ever.
179
+ *
180
+ * LIFETIME, honestly: `registry.strip(id)` runs on the mount's dispose (both
181
+ * paths), but it walks providers/commands only — the three seams set here
182
+ * survive it, keyed by this world's id, until a remount of the same id
183
+ * overwrites them. That residue is inert rather than hidden: every closure
184
+ * below reads GAME-scoped state (`game.input`, `game.loop.fixedDt`, the game's
185
+ * seed), so a stale entry actuates exactly what a live one would. See the
186
+ * dispose comment in `r3f-adapter.tsx` for the full accounting.
187
+ */
188
+ export function wireGameInputSeams(
189
+ host: ThreeHostContext,
190
+ registry: DebugRegistry,
191
+ options: {
192
+ readonly id: string;
193
+ readonly inputMapPath?: string | null | undefined;
194
+ readonly optionalInputMap?: boolean | undefined;
195
+ },
196
+ ): Promise<void> {
197
+ const { id, inputMapPath = DEFAULT_INPUT_MAP_PATH, optionalInputMap = false } = options;
198
+ if (!host.game) return Promise.resolve();
199
+ const game = host.game;
200
+ const input = game.input;
201
+ registry.setInputActionsSource(id, () =>
202
+ input.actionNames().map((name) => ({ name, valueType: input.getActionValueType(name) })),
203
+ );
204
+ registry.setInputTraceSource(id, () => {
205
+ const raw = input.getInputTrace();
206
+ return {
207
+ version: raw.version,
208
+ seed: getSeededRandom(game)?.seed ?? null,
209
+ fixedDt: game.loop.fixedDt,
210
+ ticks: raw.ticks,
211
+ };
212
+ });
213
+ registry.setVirtualInputTarget(id, {
214
+ setVirtualAction: (action, value) => input.setVirtualAction(action, value),
215
+ tapVirtualAction: (action) => input.tapVirtualAction(action),
216
+ clearVirtualActions: () => input.clearVirtualActions(),
217
+ scheduleActionAtTick: (tick, action, value) => input.scheduleActionAtTick(tick, action, value),
218
+ startInputRecording: () => input.startInputRecording(),
219
+ stopInputRecording: () => input.stopInputRecording(),
220
+ isInputRecording: () => input.isInputRecording(),
221
+ injectAxis: (sourceId, value) => input.injectAxis(sourceId, value),
222
+ injectVector2: (sourceId, value) => input.injectVector2(sourceId, value),
223
+ injectPointerDelta: (sourceId, delta) => input.injectPointerDelta(sourceId, delta),
224
+ injectPointerPosition: (sourceId, value) => input.injectPointerPosition(sourceId, value),
225
+ });
226
+ if (host.headless || inputMapPath === null) return Promise.resolve();
227
+ // Load-once through the game-owned path (competing paths across roots throw
228
+ // THERE, loudly). A FAILED load (missing/bad file) must not fail the mount:
229
+ // a three world with no declared actions is legal. It degrades loudly
230
+ // instead — naming exactly what breaks.
231
+ // The non-optional call passes ONE argument, exactly as it always has — a
232
+ // trailing `undefined` is a different call to any observer of it.
233
+ const load = optionalInputMap
234
+ ? game.loadInputMap(inputMapPath, { optional: true })
235
+ : game.loadInputMap(inputMapPath);
236
+ return load.catch((err: unknown) => {
237
+ // biome-ignore lint/suspicious/noConsole: deliberate loud degrade — the documented alternative to failing the mount (see comment above)
238
+ console.error(
239
+ `three world "${id}": failed to load input map "${inputMapPath}" — declared input ` +
240
+ 'actions and `game.input.*` (bot/virtual input) will not work until a valid map ' +
241
+ 'loads. Ship one at the conventional path or pass `inputMapPath` to ' +
242
+ `createR3FAdapter (null to opt out). Cause: ${err instanceof Error ? err.message : String(err)}`,
243
+ );
244
+ });
245
+ }
246
+
150
247
  /**
151
248
  * Build the engine runtime for one R3F world from a `ThreeHostContext` — the same
152
249
  * wiring, at the same layer, as the classic `SetupThreeRootAdapter.mount()`
@@ -216,57 +313,9 @@ export function createR3FRootContext(
216
313
  gameClock ?? createSimClock({ dispose: (obj) => disposeDebrisSubtree(obj, []) });
217
314
  ctx.clock = clock;
218
315
 
219
- // --- ctx.input + the debug-registry input seams (gap 3) — the game-owned
220
- // InputManager, polled by the game-scoped runner each frame; this world
221
- // registers the SAME per-world seams the classic adapter registers so
222
- // `game.input.*` (virtual input) and the built-in `input.actions`/
223
- // `input.trace` providers resolve to it. ---
224
- let inputMapReady: Promise<void> = Promise.resolve();
225
- if (host.game) {
226
- const game = host.game;
227
- const input = game.input;
228
- debugRegistry.setInputActionsSource(id, () =>
229
- input.actionNames().map((name) => ({ name, valueType: input.getActionValueType(name) })),
230
- );
231
- debugRegistry.setInputTraceSource(id, () => {
232
- const raw = input.getInputTrace();
233
- return {
234
- version: raw.version,
235
- seed: getSeededRandom(game)?.seed ?? null,
236
- fixedDt: game.loop.fixedDt,
237
- ticks: raw.ticks,
238
- };
239
- });
240
- debugRegistry.setVirtualInputTarget(id, {
241
- setVirtualAction: (action, value) => input.setVirtualAction(action, value),
242
- tapVirtualAction: (action) => input.tapVirtualAction(action),
243
- clearVirtualActions: () => input.clearVirtualActions(),
244
- scheduleActionAtTick: (tick, action, value) =>
245
- input.scheduleActionAtTick(tick, action, value),
246
- startInputRecording: () => input.startInputRecording(),
247
- stopInputRecording: () => input.stopInputRecording(),
248
- isInputRecording: () => input.isInputRecording(),
249
- injectAxis: (sourceId, value) => input.injectAxis(sourceId, value),
250
- injectVector2: (sourceId, value) => input.injectVector2(sourceId, value),
251
- injectPointerDelta: (sourceId, delta) => input.injectPointerDelta(sourceId, delta),
252
- injectPointerPosition: (sourceId, value) => input.injectPointerPosition(sourceId, value),
253
- });
254
- if (!host.headless && inputMapPath !== null) {
255
- // Load-once through the game-owned path (classic parity — competing
256
- // paths across roots throw THERE, loudly). A FAILED load (missing/bad
257
- // file) must not fail this mount: an R3F tree with no declared actions
258
- // is legal. It degrades loudly instead — naming exactly what breaks.
259
- inputMapReady = game.loadInputMap(inputMapPath).catch((err: unknown) => {
260
- // biome-ignore lint/suspicious/noConsole: deliberate loud degrade — the documented alternative to failing the mount (see comment above)
261
- console.error(
262
- `R3F world "${id}": failed to load input map "${inputMapPath}" — declared input ` +
263
- 'actions and `game.input.*` (bot/virtual input) will not work until a valid map ' +
264
- 'loads. Ship one at the conventional path or pass `inputMapPath` to ' +
265
- `createR3FAdapter (null to opt out). Cause: ${err instanceof Error ? err.message : String(err)}`,
266
- );
267
- });
268
- }
269
- }
316
+ // --- ctx.input + the debug-registry input seams (gap 3) — see
317
+ // `wireGameInputSeams` below; `ctx.input` is that same game-owned manager. ---
318
+ const inputMapReady = wireGameInputSeams(host, debugRegistry, { id, inputMapPath });
270
319
 
271
320
  return {
272
321
  ctx,
@@ -1,34 +0,0 @@
1
- /**
2
- * THE one owner of "are dev layers enabled" (`vgai.project.json`'s `dev: true`
3
- * root flag). Every reader that has to answer that question calls
4
- * {@link devLayersEnabled}; there is deliberately NO second source of truth —
5
- * not a module-level cached boolean, not a `globalThis` flag, not a per-host
6
- * copy of the `import.meta.env` read below. A dev GUI that ships to players
7
- * because two places disagreed about what "dev" means is exactly the failure
8
- * this single owner exists to make 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: `mountManifestRoots` (the ship gate — a dev root only mounts
13
- * when this returns true). Any future host that mounts manifest roots
14
- * itself calls this too, with its own `override`.
15
- * - TEARDOWN: none. This is a pure predicate over build config and one
16
- * caller-supplied argument — it owns no resource, allocates nothing, and
17
- * has no lifecycle to end.
18
- *
19
- * The three inputs, highest precedence first:
20
- * 1. `override` — the explicit per-mount answer. A headless test, a capture
21
- * harness, or a host that knows better passes `true`/`false` and gets
22
- * exactly that. Passing `undefined` (or omitting it) means "decide for
23
- * me" and falls through.
24
- * 2. A dev build — `import.meta.env.DEV`. The ordinary local/editor case:
25
- * dev layers are on.
26
- * 3. A production build's EXPLICIT opt-in — `VITE_VGAI_DEV_LAYERS=true`.
27
- * Shipping the dev GUI in a production bundle is a real, legitimate
28
- * choice (an internal playtest build, a QA build), and it must be an
29
- * opt-in someone had to type, never something a default drifts into.
30
- *
31
- * Anything else — a production build with no opt-in — is `false`.
32
- */
33
- export declare function devLayersEnabled(override?: boolean | undefined): boolean;
34
- //# sourceMappingURL=dev-layers.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"dev-layers.d.ts","sourceRoot":"","sources":["../../src/runtime/dev-layers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAaxE"}
@@ -1,46 +0,0 @@
1
- /**
2
- * THE one owner of "are dev layers enabled" (`vgai.project.json`'s `dev: true`
3
- * root flag). Every reader that has to answer that question calls
4
- * {@link devLayersEnabled}; there is deliberately NO second source of truth —
5
- * not a module-level cached boolean, not a `globalThis` flag, not a per-host
6
- * copy of the `import.meta.env` read below. A dev GUI that ships to players
7
- * because two places disagreed about what "dev" means is exactly the failure
8
- * this single owner exists to make 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: `mountManifestRoots` (the ship gate — a dev root only mounts
13
- * when this returns true). Any future host that mounts manifest roots
14
- * itself calls this too, with its own `override`.
15
- * - TEARDOWN: none. This is a pure predicate over build config and one
16
- * caller-supplied argument — it owns no resource, allocates nothing, and
17
- * has no lifecycle to end.
18
- *
19
- * The three inputs, highest precedence first:
20
- * 1. `override` — the explicit per-mount answer. A headless test, a capture
21
- * harness, or a host that knows better passes `true`/`false` and gets
22
- * exactly that. Passing `undefined` (or omitting it) means "decide for
23
- * me" and falls through.
24
- * 2. A dev build — `import.meta.env.DEV`. The ordinary local/editor case:
25
- * dev layers are on.
26
- * 3. A production build's EXPLICIT opt-in — `VITE_VGAI_DEV_LAYERS=true`.
27
- * Shipping the dev GUI in a production bundle is a real, legitimate
28
- * choice (an internal playtest build, a QA build), and it must be an
29
- * opt-in someone had to type, never something a default drifts into.
30
- *
31
- * Anything else — a production build with no opt-in — is `false`.
32
- */
33
- export function devLayersEnabled(override) {
34
- if (override !== undefined)
35
- return override;
36
- // `import.meta` is cast whole, not just its `.env`: this module is reachable
37
- // from programs whose tsconfig does not pull in `vite/client` (the session
38
- // client's, for one, which reaches the three adapter transitively), and there
39
- // `ImportMeta` has no declared `env` at all. The cast keeps the single owner
40
- // of the dev answer importable from ANY program rather than forcing every
41
- // downstream tsconfig to adopt Vite's ambient types.
42
- const env = import.meta.env;
43
- if (env?.DEV === true)
44
- return true;
45
- return env?.VITE_VGAI_DEV_LAYERS === 'true';
46
- }
@@ -1,46 +0,0 @@
1
- /**
2
- * THE one owner of "are dev layers enabled" (`vgai.project.json`'s `dev: true`
3
- * root flag). Every reader that has to answer that question calls
4
- * {@link devLayersEnabled}; there is deliberately NO second source of truth —
5
- * not a module-level cached boolean, not a `globalThis` flag, not a per-host
6
- * copy of the `import.meta.env` read below. A dev GUI that ships to players
7
- * because two places disagreed about what "dev" means is exactly the failure
8
- * this single owner exists to make 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: `mountManifestRoots` (the ship gate — a dev root only mounts
13
- * when this returns true). Any future host that mounts manifest roots
14
- * itself calls this too, with its own `override`.
15
- * - TEARDOWN: none. This is a pure predicate over build config and one
16
- * caller-supplied argument — it owns no resource, allocates nothing, and
17
- * has no lifecycle to end.
18
- *
19
- * The three inputs, highest precedence first:
20
- * 1. `override` — the explicit per-mount answer. A headless test, a capture
21
- * harness, or a host that knows better passes `true`/`false` and gets
22
- * exactly that. Passing `undefined` (or omitting it) means "decide for
23
- * me" and falls through.
24
- * 2. A dev build — `import.meta.env.DEV`. The ordinary local/editor case:
25
- * dev layers are on.
26
- * 3. A production build's EXPLICIT opt-in — `VITE_VGAI_DEV_LAYERS=true`.
27
- * Shipping the dev GUI in a production bundle is a real, legitimate
28
- * choice (an internal playtest build, a QA build), and it must be an
29
- * opt-in someone had to type, never something a default drifts into.
30
- *
31
- * Anything else — a production build with no opt-in — is `false`.
32
- */
33
- export function devLayersEnabled(override?: boolean | undefined): boolean {
34
- if (override !== undefined) return override;
35
- // `import.meta` is cast whole, not just its `.env`: this module is reachable
36
- // from programs whose tsconfig does not pull in `vite/client` (the session
37
- // client's, for one, which reaches the three adapter transitively), and there
38
- // `ImportMeta` has no declared `env` at all. The cast keeps the single owner
39
- // of the dev answer importable from ANY program rather than forcing every
40
- // downstream tsconfig to adopt Vite's ambient types.
41
- const env = (import.meta as unknown as { env?: unknown }).env as
42
- | { DEV?: boolean | undefined; VITE_VGAI_DEV_LAYERS?: string | undefined }
43
- | undefined;
44
- if (env?.DEV === true) return true;
45
- return env?.VITE_VGAI_DEV_LAYERS === 'true';
46
- }