@vgai/engine 0.5.2 → 0.5.3

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 (185) hide show
  1. package/README.md +18 -11
  2. package/package.json +10 -6
  3. package/schemas/engine-api.json +1 -68
  4. package/schemas/engine-api.md +1 -32
  5. package/schemas/engine-capabilities.json +30 -42
  6. package/schemas/{vgai-game.schema.json → vgai-project.schema.json} +52 -34
  7. package/src/adapter/adapter-surface.ts +5 -5
  8. package/src/adapter/authoring.ts +168 -226
  9. package/src/adapter/colyseus-networking-adapter.ts +44 -5
  10. package/src/adapter/first-party-systems.ts +156 -42
  11. package/src/adapter/host-context.ts +177 -46
  12. package/src/adapter/index.ts +47 -51
  13. package/src/adapter/ingest/game-contract.ts +2 -2
  14. package/src/adapter/ingest/scene-capture.ts +18 -19
  15. package/src/adapter/ingest/structural-ids.ts +127 -0
  16. package/src/adapter/ingest/upstream-pin.ts +9 -12
  17. package/src/adapter/loop-gate-report.ts +11 -11
  18. package/src/adapter/rapier-physics-adapter.ts +27 -9
  19. package/src/adapter/root-adapter.ts +217 -0
  20. package/src/adapter/{vgai-scene-game-adapter.ts → setup-three-root-adapter.ts} +173 -351
  21. package/src/adapter/system-adapter.ts +80 -63
  22. package/src/ai/navigation.ts +1 -1
  23. package/src/animation/animation-clock.ts +1 -1
  24. package/src/animation/camera-ownership.ts +1 -2
  25. package/src/animation/cubic-spline-interpolant.ts +132 -0
  26. package/src/animation/theatre-clock-binding.ts +2 -2
  27. package/src/animation/theatre-object-binding.ts +4 -4
  28. package/src/animation/xstate-animation-binding.ts +75 -5
  29. package/src/{scene/schema → asset-formats}/camera.ts +2 -5
  30. package/src/{scene/schema → asset-formats}/collider.ts +2 -5
  31. package/src/asset-formats/index.ts +54 -0
  32. package/src/{scene/schema → asset-formats}/instances.ts +10 -6
  33. package/src/{scene/schema → asset-formats}/light.ts +3 -6
  34. package/src/{scene/schema → asset-formats}/material.ts +4 -7
  35. package/src/{scene/schema → asset-formats}/mesh.ts +4 -7
  36. package/src/asset-formats/parse.ts +39 -0
  37. package/src/{scene/schema → asset-formats}/particles.ts +3 -6
  38. package/src/{scene/schema/environment.ts → asset-formats/render-env.ts} +23 -86
  39. package/src/{scene/schema → asset-formats}/tuples.ts +1 -1
  40. package/src/{scene/asset-loaders.ts → asset-loaders.ts} +8 -9
  41. package/src/asset-parse-error.ts +33 -0
  42. package/src/{scene/asset-registry.ts → asset-registry.ts} +1 -1
  43. package/src/assets.ts +1 -1
  44. package/src/audio/wav-encode.ts +9 -9
  45. package/src/canvas-react/engine-bridge.ts +59 -0
  46. package/src/canvas-react/index.ts +50 -0
  47. package/src/canvas-react/pixi-primitive.tsx +202 -0
  48. package/src/canvas-react/pixi-react-adapter.tsx +290 -0
  49. package/src/canvas-react/pixi-react-root-factory.tsx +88 -0
  50. package/src/canvas-react/world-context.ts +328 -0
  51. package/src/core/frame-pacing.ts +100 -0
  52. package/src/core/game-loop.ts +50 -28
  53. package/src/core/seeded-random.ts +7 -7
  54. package/src/core/sim-clock.ts +388 -0
  55. package/src/core/system-runner.ts +17 -63
  56. package/src/core/types.ts +34 -15
  57. package/src/data/data-asset.ts +3 -3
  58. package/src/data/data-check-core.ts +6 -7
  59. package/src/data/data-ref.ts +11 -11
  60. package/src/data/vite-plugin-data.ts +10 -10
  61. package/src/{scene/defaults.ts → defaults.ts} +18 -40
  62. package/src/dev/render-debug-adapter.ts +1 -1
  63. package/src/dev/webgl-frame-capture.ts +1 -1
  64. package/src/ecs/scene-index.ts +439 -0
  65. package/src/ecs/scene-query.ts +43 -0
  66. package/src/{scene → ecs}/user-data.ts +17 -36
  67. package/src/index.ts +7 -9
  68. package/src/input/input-manager.ts +29 -32
  69. package/src/input/input-types.ts +2 -2
  70. package/src/input/schema.ts +5 -5
  71. package/src/loader.ts +57 -0
  72. package/src/manifest/editor-port.ts +69 -0
  73. package/src/manifest/filename.ts +49 -0
  74. package/src/manifest/index.ts +8 -2
  75. package/src/manifest/load-file.ts +11 -0
  76. package/src/manifest/load.ts +65 -77
  77. package/src/manifest/locate.ts +55 -0
  78. package/src/manifest/schema.ts +400 -233
  79. package/src/{scene → physics}/collider-dimensions.ts +3 -3
  80. package/src/physics/physics-registry.ts +1 -1
  81. package/src/{world2d/authoring-2d.ts → pixi/authoring.ts} +24 -11
  82. package/src/pixi/index.ts +43 -0
  83. package/src/{world2d/ingest-iframe-2d.ts → pixi/ingest-iframe.ts} +9 -9
  84. package/src/{world2d/ingest2d.ts → pixi/ingest.ts} +28 -28
  85. package/src/{world2d/physics2d-registry.ts → pixi/physics-registry.ts} +1 -1
  86. package/src/{world2d/scene-capture-2d.ts → pixi/scene-capture.ts} +3 -3
  87. package/src/{world2d/system-adapters-2d.ts → pixi/system-adapters.ts} +2 -2
  88. package/src/react/unmanaged-root-detector.ts +26 -1
  89. package/src/react/use-data.ts +5 -5
  90. package/src/react/use-selection.tsx +15 -42
  91. package/src/react/{game-state.tsx → world-state.tsx} +44 -46
  92. package/src/render/auto-batcher.ts +1 -2
  93. package/src/{scene → render}/instance-mesh.ts +1 -1
  94. package/src/{scene → render}/light-camera-factory.ts +14 -13
  95. package/src/render/lod.ts +17 -0
  96. package/src/{scene → render}/material-factory.ts +8 -7
  97. package/src/{scene → render}/particles-factory.ts +62 -12
  98. package/src/render/render-batch-system.ts +14 -41
  99. package/src/render/render-features.ts +1 -1
  100. package/src/render/render-settings.ts +1 -2
  101. package/src/render/spark-renderer-lifecycle.ts +1 -1
  102. package/src/runtime/create-runtime.ts +352 -463
  103. package/src/runtime/debug-bridge.ts +148 -93
  104. package/src/runtime/debug-registry.ts +79 -54
  105. package/src/runtime/dev-layers.ts +40 -0
  106. package/src/runtime/frame-selector-cache.ts +4 -4
  107. package/src/runtime/game.ts +618 -369
  108. package/src/runtime/gameplay-rng-trap.ts +6 -7
  109. package/src/runtime/input-router.ts +11 -11
  110. package/src/runtime/mount-game.ts +54 -55
  111. package/src/runtime/mount-manifest.ts +154 -150
  112. package/src/runtime/presentation.ts +141 -0
  113. package/src/runtime/render-audio-control.ts +64 -53
  114. package/src/runtime/render-control.ts +45 -51
  115. package/src/runtime/render-seed.ts +3 -4
  116. package/src/runtime/state-bridge.ts +17 -18
  117. package/src/runtime/types.ts +94 -61
  118. package/src/setup/setup-renderer.ts +11 -6
  119. package/src/world3d-react/engine-bridge.ts +46 -33
  120. package/src/world3d-react/index.ts +31 -26
  121. package/src/world3d-react/r3f-adapter.tsx +211 -96
  122. package/src/world3d-react/r3f-root-factory.tsx +91 -0
  123. package/src/world3d-react/renderer-config.ts +137 -0
  124. package/src/world3d-react/world-context.ts +141 -111
  125. package/schemas/entity2d.schema.json +0 -468
  126. package/schemas/prefab.schema.json +0 -9992
  127. package/schemas/scn2d.schema.json +0 -494
  128. package/schemas/vscn.schema.json +0 -10851
  129. package/src/adapter/game-adapter.ts +0 -164
  130. package/src/adapter/ingest/overlay-applier.ts +0 -207
  131. package/src/adapter/ingest/overlay-apply.ts +0 -168
  132. package/src/adapter/ingest/overlay-file.ts +0 -126
  133. package/src/adapter/ingest/overlay-report.ts +0 -176
  134. package/src/animation/gsap-registration.ts +0 -184
  135. package/src/audio/audio-introspection.ts +0 -290
  136. package/src/audio/index.ts +0 -39
  137. package/src/audio/tone-clock-binding.ts +0 -98
  138. package/src/audio/tone-context.ts +0 -175
  139. package/src/audio/tone-offline-render.ts +0 -167
  140. package/src/ecs/component-manager.ts +0 -814
  141. package/src/ecs/game-component.ts +0 -260
  142. package/src/ecs/hmr-swap-report.ts +0 -65
  143. package/src/physics/trigger-dispatch.ts +0 -97
  144. package/src/react/root-adapter.tsx +0 -49
  145. package/src/scene/asset-paths.ts +0 -121
  146. package/src/scene/asset-ref-check.ts +0 -248
  147. package/src/scene/component-registry.ts +0 -51
  148. package/src/scene/parse.ts +0 -204
  149. package/src/scene/scene-apply.ts +0 -407
  150. package/src/scene/scene-diff-schema.ts +0 -115
  151. package/src/scene/scene-diff-types.ts +0 -29
  152. package/src/scene/scene-loader.ts +0 -1526
  153. package/src/scene/scene-query.ts +0 -63
  154. package/src/scene/scene-types.ts +0 -33
  155. package/src/scene/scene-version.ts +0 -40
  156. package/src/scene/schema/animation.ts +0 -46
  157. package/src/scene/schema/audio.ts +0 -25
  158. package/src/scene/schema/entity-ref.ts +0 -78
  159. package/src/scene/schema/entity.ts +0 -189
  160. package/src/scene/schema/index.ts +0 -51
  161. package/src/scene/schema/joint.ts +0 -26
  162. package/src/scene/schema/physics.ts +0 -49
  163. package/src/scene/schema/scene-file.ts +0 -292
  164. package/src/scene/schema/shadow.ts +0 -24
  165. package/src/scene/schema/spline.ts +0 -21
  166. package/src/world2d/asset-paths2d.ts +0 -44
  167. package/src/world2d/capture-to-scene2d.ts +0 -52
  168. package/src/world2d/collision-2d.ts +0 -99
  169. package/src/world2d/entity2d-asset.ts +0 -22
  170. package/src/world2d/index.ts +0 -91
  171. package/src/world2d/physics2d-transform.ts +0 -173
  172. package/src/world2d/physics2d-units.ts +0 -10
  173. package/src/world2d/pixi-game-adapter.ts +0 -439
  174. package/src/world2d/pixi-surface.ts +0 -78
  175. package/src/world2d/scene2d-identity.ts +0 -49
  176. package/src/world2d/scene2d-loader.ts +0 -433
  177. package/src/world2d/schema/entity2d.ts +0 -163
  178. package/src/world2d/schema/physics2d.ts +0 -64
  179. package/src/world2d/schema/sprite.ts +0 -99
  180. package/src/world2d/schema/tilemap.ts +0 -39
  181. package/src/world2d/schema/tuples2d.ts +0 -25
  182. package/src/world2d/transform-writer-2d.ts +0 -42
  183. package/src/world2d/types.ts +0 -74
  184. package/src/world3d-react/behavior.tsx +0 -146
  185. /package/src/{scene → render}/mesh-shadow.ts +0 -0
@@ -1,22 +1,21 @@
1
1
  /**
2
- * The game-scoped debug/synthetic-player registry (`docs/SYNTHETIC-PLAYER-SPEC.md`
3
- * §3.1). ONE registry per `Game` root — every world's `ctx.debug` (see
4
- * `DebugCtxSurface`, `runtime/types.ts`) feeds the SAME registry via
5
- * {@link DebugRegistry.forWorld}, so a name a game registers is visible (and
6
- * name-collision-checked) across every world, not just the one that
2
+ * The game-scoped debug/synthetic-player registry. ONE registry per `Game` root —
3
+ * every world's `ctx.debug` (see `DebugCtxSurface`, `runtime/types.ts`) feeds the
4
+ * SAME registry via {@link DebugRegistry.forRoot}, so a name a game registers is
5
+ * visible (and name-collision-checked) across every world, not just the one that
7
6
  * registered it. `createGame` (`runtime/game.ts`) constructs the registry
8
- * alongside the state bridge and files it in the `Game -> DebugRegistry`
9
- * WeakMap this module owns; later consumers (the react `useDebugProvider`/
7
+ * alongside the state bridge and files it in the `Game -> DebugRegistry` WeakMap
8
+ * this module owns; later consumers (the react `useDebugProvider`/
10
9
  * `useDebugCommand` hooks, editor panels) reach it via {@link getDebugRegistry}
11
10
  * rather than threading it through every call site.
12
11
  *
13
12
  * Provenance note: a registration's "world" is the MOUNT's adapter id (the
14
- * `fromSetup(id, ...)` / `VgaiSceneConfig.id` a project already names each
15
- * world's entry with) rather than the `WorldInstance.id` `registerThreeWorld`
13
+ * `fromSetup(id, ...)` / `SetupThreeRootConfig.id` a project already names each
14
+ * world's entry with) rather than the `RootInstance.id` `registerThreeRoot`
16
15
  * assigns — that id isn't known until AFTER `mount()` resolves (`create-
17
- * runtime.ts` calls `registerThreeWorld` with it only once `mount()` returns),
16
+ * runtime.ts` calls `registerThreeRoot` with it only once `mount()` returns),
18
17
  * which is after every `setup()`-time registration has already run. For the
19
- * single-world case (nearly every project today) this is simply `'vgai-scene'`;
18
+ * single-world case (nearly every project today) this is simply `'setup-three'`;
20
19
  * a multi-world manifest names each world's adapter to match its declared id
21
20
  * by convention, so collision messages stay meaningful in practice.
22
21
  */
@@ -46,6 +45,26 @@ export class DebugError extends Error {
46
45
  const REGISTRATION_HINT =
47
46
  'ctx.debug.registerStateProvider(name, fn) — see the template example component';
48
47
 
48
+ /** A thrown value flattened to JSON-SAFE fields. Every caller of a debug
49
+ * command is on the far side of a JSON boundary — the editor session relay
50
+ * (`POST /__editor/command`) and `page.evaluate` both serialize the error's
51
+ * `data` bag — and an `Error` stringifies to `{}` there, so a command's own
52
+ * refusal text ("no such waypoint") vanished and the developer at
53
+ * `vgai eval` saw only `debug: command "x" threw`. The text has to travel as
54
+ * plain strings, and in the `message` above all: that is the one field every
55
+ * client (`SessionError`, the CLI's own error print) actually surfaces. */
56
+ function describeCause(cause: unknown): {
57
+ causeName: string;
58
+ causeMessage: string;
59
+ causeStack?: string;
60
+ } {
61
+ if (cause instanceof Error) {
62
+ const described = { causeName: cause.name, causeMessage: cause.message };
63
+ return cause.stack === undefined ? described : { ...described, causeStack: cause.stack };
64
+ }
65
+ return { causeName: typeof cause, causeMessage: String(cause) };
66
+ }
67
+
49
68
  /** Task 2.2 — how long `invoke()` waits for a `locus: 'server'` command's
50
69
  * `__vgai:debugCommandResult` reply before failing loudly. */
51
70
  const SERVER_COMMAND_TIMEOUT_MS = 10_000;
@@ -54,7 +73,7 @@ const SERVER_COMMAND_TIMEOUT_MS = 10_000;
54
73
  * actuates through — the SAME three methods `InputManager` exposes
55
74
  * (`setVirtualAction`/`tapVirtualAction`/`clearVirtualActions`, Task 1.4),
56
75
  * typed narrowly here so this module never imports `InputManager` itself.
57
- * Wired once by the hosting adapter (`vgai-scene-game-adapter.ts`, at the
76
+ * Wired once by the hosting adapter (`setup-three-root-adapter.ts`, at the
58
77
  * same seed spot as `setInputActionsSource`) — absent until then. */
59
78
  export interface DebugVirtualInputTarget {
60
79
  setVirtualAction(
@@ -63,9 +82,9 @@ export interface DebugVirtualInputTarget {
63
82
  ): { delivered: boolean; reason?: string };
64
83
  tapVirtualAction(action: string): { delivered: boolean; reason?: string };
65
84
  clearVirtualActions(): void;
66
- /** D15/T-D15.5 (`docs/D15-DETERMINISM-DESIGN.md` §2.c) — schedule a virtual
67
- * actuation for a specific future (or current) tick, applied at the start
68
- * of that tick's input phase (composes with `runTicks`). */
85
+ /** D15/T-D15.5 — schedule a virtual actuation for a specific future (or
86
+ * current) tick, applied at the start of that tick's input phase (composes
87
+ * with `runTicks`). */
69
88
  scheduleActionAtTick(
70
89
  tick: number,
71
90
  action: string,
@@ -85,8 +104,8 @@ export interface DebugVirtualInputTarget {
85
104
  }
86
105
 
87
106
  /**
88
- * D15/T-D15.5 — the `input.trace` builtin provider's shape (docs/D15-
89
- * DETERMINISM-DESIGN.md §2.c's format sketch). `seed`/`fixedDt` are
107
+ * D15/T-D15.5 — the `input.trace` builtin provider's shape.
108
+ * `seed`/`fixedDt` are
90
109
  * replay-critical metadata a future SP5 consumer needs BESIDE the raw
91
110
  * per-tick action deltas (`ticks`, straight off `InputManager.getInputTrace()`)
92
111
  * to know what to replay the trace AGAINST — recording deltas alone is not
@@ -132,12 +151,12 @@ interface PendingServerCommand {
132
151
  * (spec §3.1's "react-world registration door" paragraph), so all of them
133
152
  * share this one provenance string; a name collision between two DIFFERENT
134
153
  * react components is therefore the same-world "replace + warn once" case,
135
- * never the cross-world throw (which is reserved for a GameComponent world
154
+ * never the cross-world throw (which is reserved for a first-party world
136
155
  * vs. the react door genuinely disagreeing about a name).
137
156
  *
138
157
  * Defect 7 fix: this used to be the bare string `'react'`, which collided in
139
158
  * provenance with a manifest world literally named `'react'` (e.g. a
140
- * `kind: 'react'` world whose `id` is `"react"`) — `forWorld('react')` and
159
+ * `kind: 'dom'` world whose `id` is `"react"`) — `forRoot('react')` and
141
160
  * the react door would then be treated as the SAME world for collision
142
161
  * purposes, which is wrong (they are genuinely different registrants that
143
162
  * happen to share a display name). Namespaced like `'__engine__'` so no
@@ -167,14 +186,14 @@ interface CommandEntry {
167
186
 
168
187
  /** What {@link createDebugRegistry} returns — the adapter half (`DebugAdapter`,
169
188
  * for `SystemAdapters.debug`) plus the registration/lifecycle surface the
170
- * hosting adapter (`vgai-scene-game-adapter.ts`) and `createGame` drive. */
189
+ * hosting adapter (`setup-three-root-adapter.ts`) and `createGame` drive. */
171
190
  export interface DebugRegistry {
172
191
  /** The `SystemAdapters.debug` implementer — one shared instance, seeded
173
192
  * onto every world's adapter bag. */
174
193
  readonly adapter: DebugAdapter;
175
194
  /** Build the `ctx.debug` surface for one world/mount — registrations made
176
195
  * through it carry `worldId` as their provenance for collision messages. */
177
- forWorld(worldId: string): DebugCtxSurface;
196
+ forRoot(worldId: string): DebugCtxSurface;
178
197
  /** Emit from a React root through the same tick-stamped event ring as
179
198
  * `ctx.debug.emit`. React roots have no setup context, so
180
199
  * `useDebugEmit()` calls this dedicated door instead of reaching through
@@ -208,7 +227,7 @@ export interface DebugRegistry {
208
227
  * exist yet when the registry is constructed (`createGame`, before any
209
228
  * world mounts). Before ANY source is set, `input.actions` reads `[]`;
210
229
  * once one or more roots have registered, the built-in `input.actions`
211
- * provider reads the DEFAULT world's (see {@link resolveInputWorldId}) —
230
+ * provider reads the DEFAULT world's (see {@link resolveInputRootId}) —
212
231
  * same resolution every other per-world seam on this interface uses. */
213
232
  setInputActionsSource(worldId: string, fn: () => { name: string; valueType: string }[]): void;
214
233
  /** D15/T-D15.5 — wire the built-in `input.trace` provider to a live
@@ -218,7 +237,7 @@ export interface DebugRegistry {
218
237
  * `{version: 1, seed: null, fixedDt: null, ticks: []}`. `seed`/`fixedDt`
219
238
  * are the two replay-critical metadata fields the design doc's format
220
239
  * sketch (§2.c) calls for beside the raw per-tick deltas — the wiring
221
- * adapter (`vgai-scene-game-adapter.ts`) assembles them from
240
+ * adapter (`setup-three-root-adapter.ts`) assembles them from
222
241
  * `getSeededRandom(game)?.seed`/`host.game?.loop.fixedDt` alongside
223
242
  * `InputManager.getInputTrace()`'s own `{version, ticks}`. An `engine`
224
243
  * (package version) stamp remains a KNOWN GAP — no build-time version
@@ -239,14 +258,14 @@ export interface DebugRegistry {
239
258
  * multi-world project — the bridge always got whichever world mounted
240
259
  * LAST, the relay always got the FIRST/default world. Now every world's
241
260
  * target is kept, keyed by `worldId`, and {@link getVirtualInputTarget}
242
- * resolves ONE of them via {@link resolveInputWorldId} — the SAME
261
+ * resolves ONE of them via {@link resolveInputRootId} — the SAME
243
262
  * resolution the bridge and the relay both call through, so they can never
244
263
  * disagree again. An explicit `worldId` reaches that world specifically. */
245
264
  setVirtualInputTarget(worldId: string, target: DebugVirtualInputTarget): void;
246
265
  /**
247
266
  * Resolve and return a virtual-input target: `worldId` given and
248
267
  * registered → that world's; omitted → the DEFAULT world's (per
249
- * {@link resolveInputWorldId} — the manifest's first/default world when a
268
+ * {@link resolveInputRootId} — the manifest's first/default world when a
250
269
  * `Game` is behind this registry, else the single registered world, else
251
270
  * whichever registered first), consistently, for every caller (the debug
252
271
  * bridge and the editor relay both call this — see this interface's own
@@ -277,13 +296,13 @@ export interface DebugRegistry {
277
296
  /** D15/T-D15.3/.5 — the CURRENT shared game tick (the same counter the
278
297
  * built-in `time` provider's `tick` field reads), for a per-world
279
298
  * `InputManager.poll(tick)` call to key its `scheduleActionAtTick`
280
- * numbering off — see `vgai-scene-game-adapter.ts`'s `systems.add('input',
299
+ * numbering off — see `setup-three-root-adapter.ts`'s `systems.add('input',
281
300
  * ...)` wiring. `0` for a bare `createDebugRegistry()` test stand-in with
282
301
  * no real `Game`/tick counter behind it (matching `getTick`'s own
283
302
  * constructor-supplied default in that case). */
284
303
  getGameTick(): number;
285
304
  /** React-door registration (Task 1.5, spec §3.1's "react-world
286
- * registration door") — same accumulator as `forWorld(...)
305
+ * registration door") — same accumulator as `forRoot(...)
287
306
  * .registerStateProvider`, provenance `'__react__'` (see {@link REACT_WORLD_ID}),
288
307
  * but returns a disposer instead of requiring a separate unregister call.
289
308
  * Calling the disposer removes the registration ONLY if it is still the
@@ -322,13 +341,13 @@ function warnOnce(
322
341
  * Construct a fresh game-scoped debug registry. `getTick`/`getSimT` are
323
342
  * suppliers (not values) so the built-in `time` provider always reads the
324
343
  * CURRENT counters — `createGame` passes closures over its own mutable
325
- * `tick`/`simT`, incremented in `runFrame`'s tail (T1.2). `getDefaultWorldId`
344
+ * `tick`/`simT`, incremented in `runFrame`'s tail (T1.2). `getDefaultRootId`
326
345
  * (D15/T-D15.5, optional) resolves the manifest's first/default world id —
327
- * `createGame` passes `() => (roots.length ? requireDefaultWorld().id :
346
+ * `createGame` passes `() => (roots.length ? requireDefaultRoot().id :
328
347
  * null)`; a bare `createDebugRegistry()` test stand-in with no `Game` behind
329
348
  * it omits it (per-world resolution then falls back to "the single
330
349
  * registered world" or "whichever registered first" — see
331
- * `resolveInputWorldId`). `getLoopLiveness` (issue #175, optional) supplies
350
+ * `resolveInputRootId`). `getLoopLiveness` (issue #175, optional) supplies
332
351
  * the REAL `GameLoop.liveness` — `createGame` passes `() => opts.loop.
333
352
  * liveness`; a bare `createDebugRegistry()` test stand-in with no loop
334
353
  * behind it omits it, and the built-in `time` provider reports `null`
@@ -338,7 +357,7 @@ function warnOnce(
338
357
  export function createDebugRegistry(opts: {
339
358
  getTick(): number;
340
359
  getSimT(): number;
341
- getDefaultWorldId?(): string | null;
360
+ getDefaultRootId?(): string | null;
342
361
  getLoopLiveness?(): GameLoopLiveness;
343
362
  }): DebugRegistry {
344
363
  const providers = new Map<string, ProviderEntry>();
@@ -356,8 +375,8 @@ export function createDebugRegistry(opts: {
356
375
 
357
376
  let roomDeclared = false;
358
377
  // D15/T-D15.5 — per-world maps (Map preserves insertion order, which
359
- // `resolveInputWorldId`'s "whichever registered first" fallback relies on
360
- // when no `getDefaultWorldId` is available to disambiguate).
378
+ // `resolveInputRootId`'s "whichever registered first" fallback relies on
379
+ // when no `getDefaultRootId` is available to disambiguate).
361
380
  const inputActionsSources = new Map<string, () => { name: string; valueType: string }[]>();
362
381
  const inputTraceSources = new Map<string, () => InputTraceSnapshot>();
363
382
  const virtualInputTargets = new Map<string, DebugVirtualInputTarget>();
@@ -375,7 +394,7 @@ export function createDebugRegistry(opts: {
375
394
  * `DEBUG_INPUT_WORLD_NOT_FOUND` (a caller mistake — distinct from
376
395
  * "nothing mounted yet", which returns `null` below instead of throwing).
377
396
  * - `explicit` omitted: the manifest's first/default world id
378
- * (`opts.getDefaultWorldId()`) iff that world has actually registered —
397
+ * (`opts.getDefaultRootId()`) iff that world has actually registered —
379
398
  * else (no `Game`/no default resolvable, or the default world never
380
399
  * wired one — e.g. a foreign/opaque mount) the single registered world,
381
400
  * or, with more than one and no resolvable default, whichever registered
@@ -384,7 +403,7 @@ export function createDebugRegistry(opts: {
384
403
  * instead of "whichever mounted last".
385
404
  * - Nothing registered at all: `null`.
386
405
  */
387
- function resolveInputWorldId(explicit?: string): string | null {
406
+ function resolveInputRootId(explicit?: string): string | null {
388
407
  if (explicit !== undefined) {
389
408
  if (!virtualInputTargets.has(explicit)) {
390
409
  throw new DebugError(
@@ -396,11 +415,11 @@ export function createDebugRegistry(opts: {
396
415
  }
397
416
  return explicit;
398
417
  }
399
- return resolveWorldForSource(virtualInputTargets.keys());
418
+ return resolveRootForSource(virtualInputTargets.keys());
400
419
  }
401
420
 
402
421
  /** The same "default world, else the one registered, else whichever
403
- * registered first" fallback {@link resolveInputWorldId} uses for the
422
+ * registered first" fallback {@link resolveInputRootId} uses for the
404
423
  * ACTUATION target, generalized over any per-world registration set
405
424
  * (`inputActionsSources`/`inputTraceSources` included) — every one of
406
425
  * these maps is keyed by the same world ids, populated at the same
@@ -408,10 +427,10 @@ export function createDebugRegistry(opts: {
408
427
  * for all of them. Never throws (no explicit-id case here — the two
409
428
  * builtin providers that call this have no way to accept a caller-chosen
410
429
  * worldId today; see their own doc comments). */
411
- function resolveWorldForSource(registered: IterableIterator<string>): string | null {
430
+ function resolveRootForSource(registered: IterableIterator<string>): string | null {
412
431
  const ids = [...registered];
413
432
  if (ids.length === 0) return null;
414
- const defaultId = opts.getDefaultWorldId?.() ?? null;
433
+ const defaultId = opts.getDefaultRootId?.() ?? null;
415
434
  if (defaultId !== null && ids.includes(defaultId)) return defaultId;
416
435
  return ids[0]!;
417
436
  }
@@ -431,11 +450,11 @@ export function createDebugRegistry(opts: {
431
450
  builtin: true,
432
451
  });
433
452
  providers.set('input.actions', {
434
- // Resolves to the DEFAULT world's action list (see `resolveInputWorldId`)
453
+ // Resolves to the DEFAULT world's action list (see `resolveInputRootId`)
435
454
  // — deterministic across every world that registers, rather than the
436
455
  // pre-D15.5 "whichever mounted last" behavior.
437
456
  fn: () => {
438
- const worldId = resolveWorldForSource(inputActionsSources.keys());
457
+ const worldId = resolveRootForSource(inputActionsSources.keys());
439
458
  return (worldId ? inputActionsSources.get(worldId) : undefined)?.() ?? [];
440
459
  },
441
460
  tier: 'observable',
@@ -448,7 +467,7 @@ export function createDebugRegistry(opts: {
448
467
  // `input.actions` immediately above.
449
468
  providers.set('input.trace', {
450
469
  fn: () => {
451
- const worldId = resolveWorldForSource(inputTraceSources.keys());
470
+ const worldId = resolveRootForSource(inputTraceSources.keys());
452
471
  return (
453
472
  (worldId ? inputTraceSources.get(worldId) : undefined)?.() ?? {
454
473
  version: 1,
@@ -544,12 +563,11 @@ export function createDebugRegistry(opts: {
544
563
  }
545
564
 
546
565
  /** See `DebugAdapter.events`'s doc comment (`adapter/system-adapter.ts`)
547
- * for the full contract — `sinceSeq` (when given) wins over `sinceTick`,
548
- * since it's the unambiguous one. */
549
- function events(sinceTick?: number, sinceSeq?: number): TickStampedEvent[] {
550
- if (sinceSeq !== undefined) return ring.filter((e) => e.seq > sinceSeq);
551
- if (sinceTick === undefined) return ring.slice();
552
- return ring.filter((e) => e.tick > sinceTick);
566
+ * for the full contract — `seq` is the ONLY fence (the defective
567
+ * `sinceTick` filter was removed). */
568
+ function events(sinceSeq?: number): TickStampedEvent[] {
569
+ if (sinceSeq === undefined) return ring.slice();
570
+ return ring.filter((e) => e.seq > sinceSeq);
553
571
  }
554
572
 
555
573
  /**
@@ -728,17 +746,24 @@ export function createDebugRegistry(opts: {
728
746
  try {
729
747
  return await entry.fn(...parsedArgs);
730
748
  } catch (cause) {
731
- throw new DebugError('DEBUG_COMMAND_FAILED', `debug: command "${name}" threw`, { cause });
749
+ // `cause` stays for in-process readers; the flattened fields are what
750
+ // survive the wire (see `describeCause`).
751
+ const described = describeCause(cause);
752
+ throw new DebugError(
753
+ 'DEBUG_COMMAND_FAILED',
754
+ `debug: command "${name}" threw: ${described.causeMessage}`,
755
+ { cause, ...described },
756
+ );
732
757
  }
733
758
  },
734
- events(sinceTick?: number, sinceSeq?: number) {
735
- return events(sinceTick, sinceSeq);
759
+ events(sinceSeq?: number) {
760
+ return events(sinceSeq);
736
761
  },
737
762
  };
738
763
 
739
764
  return {
740
765
  adapter,
741
- forWorld(worldId: string): DebugCtxSurface {
766
+ forRoot(worldId: string): DebugCtxSurface {
742
767
  return {
743
768
  registerStateProvider(name, fn, providerOpts) {
744
769
  registerStateProvider(worldId, name, fn, providerOpts?.tier ?? 'observable');
@@ -796,7 +821,7 @@ export function createDebugRegistry(opts: {
796
821
  virtualInputTargets.set(worldId, target);
797
822
  },
798
823
  getVirtualInputTarget(worldId?: string) {
799
- const resolved = resolveInputWorldId(worldId);
824
+ const resolved = resolveInputRootId(worldId);
800
825
  return resolved !== null ? (virtualInputTargets.get(resolved) ?? null) : null;
801
826
  },
802
827
  setRunTicksTarget(target: RunTicksTarget) {
@@ -821,7 +846,7 @@ export function createDebugRegistry(opts: {
821
846
  },
822
847
  registerReactCommand(name, spec, fn) {
823
848
  const token = Symbol(name);
824
- // See the `forWorld().registerCommand` cast above — same reason.
849
+ // See the `forRoot().registerCommand` cast above — same reason.
825
850
  registerCommand(
826
851
  REACT_WORLD_ID,
827
852
  name,
@@ -0,0 +1,40 @@
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
+ const env = import.meta.env as
36
+ | { DEV?: boolean | undefined; VITE_VGAI_DEV_LAYERS?: string | undefined }
37
+ | undefined;
38
+ if (env?.DEV === true) return true;
39
+ return env?.VITE_VGAI_DEV_LAYERS === 'true';
40
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Pure, react-free frame-version cache (T7.4 slice 1 — `docs/
3
- * REACT-STATE-BRIDGE.md` §3). `useGameState` (`packages/editor/template/src/
2
+ * Pure, react-free frame-version cache (T7.4 slice 1).
3
+ * `useWorldState` (`packages/editor/template/src/
4
4
  * ui/game-state.tsx`) is a thin `useSyncExternalStore` wrapper
5
5
  * around this: `getSnapshot` re-runs the selector once per `frameVersion`
6
6
  * bump (`GameStateBridge`, `runtime/state-bridge.ts`), caches by version,
@@ -15,7 +15,7 @@
15
15
  * exists in this repo today; see `test/frame-selector-cache.test.ts`).
16
16
  */
17
17
 
18
- /** Equality comparator for `FrameSelectorCache`/`useGameState`. Defaults to
18
+ /** Equality comparator for `FrameSelectorCache`/`useWorldState`. Defaults to
19
19
  * `Object.is` (T7.4 §3); pass {@link shallow} for tuples/plain objects. */
20
20
  export type Equals<T> = (a: T, b: T) => boolean;
21
21
 
@@ -52,7 +52,7 @@ export interface FrameSelectorCache<T> {
52
52
  get(frameVersion: number, compute: () => T): T;
53
53
  }
54
54
 
55
- /** Construct a fresh per-subscriber cache. One per `useGameState` call site
55
+ /** Construct a fresh per-subscriber cache. One per `useWorldState` call site
56
56
  * (a fresh cache each mount — see the hook). */
57
57
  export function createFrameSelectorCache<T>(equals: Equals<T> = Object.is): FrameSelectorCache<T> {
58
58
  let hasValue = false;