@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,164 +0,0 @@
1
- /**
2
- * GameAdapter — the host ⇄ runtime contract. A "game" is *anything that mounts*.
3
- *
4
- * This is the interface the HOST depends on. It replaces the old
5
- * `{ setup, components }` shape: the host no longer knows about `GameSetupFn`,
6
- * `.vscn`, or `GameComponent` — those are the internals of ONE implementer
7
- * (`VgaiSceneGameAdapter`). First-party content and an unmodified external game
8
- * are peer implementers of THIS interface; the host has no branch on which.
9
- */
10
-
11
- import type { Container } from 'pixi.js';
12
- import type * as THREE from 'three';
13
- import type { ComponentManager } from '../ecs/component-manager';
14
- import type { AdapterSurface } from './adapter-surface';
15
- import type { AuthoringAdapter } from './authoring';
16
- import type { HostContext } from './host-context';
17
- import type { SystemAdapters } from './system-adapter';
18
-
19
- /**
20
- * The ingested-world observation contract (T7.4 slice 2 — `docs/
21
- * REACT-STATE-BRIDGE.md` §4, "the must-answer"). Cross-world data flow with a
22
- * foreign game is observation via an adapter-provided interface, not ordinary
23
- * component access (that's D7's answer for first-party↔first-party flow —
24
- * foreign games host no `GameComponent`s). Optional on {@link MountedGame} —
25
- * absence means the adapter has nothing genuinely observable to expose (the
26
- * anti-shim rule: never fabricate state), and the world takes the honest
27
- * "no state bridge" tier hit (see `Game.registerWorld` in `runtime/game.ts`)
28
- * rather than silently offering an `undefined`-forever subscription.
29
- */
30
- export interface WorldStateObserver {
31
- /**
32
- * Notified at most once per frame IF the adapter can hook the game's own
33
- * update (`loop: 'gated'` roots, `docs/CAPABILITY-TIERS.md` §(d));
34
- * self-driven roots (`loop: 'self-driven'`, raw-rAF) may notify on their
35
- * OWN rAF cadence instead — consumers must not assume our frame timing.
36
- * Returns an unsubscribe function.
37
- */
38
- subscribe(onChange: () => void): () => void;
39
- /**
40
- * A cheap, adapter-chosen snapshot of this world's observable state.
41
- * Returns a STABLE reference while nothing has changed (so a selector run
42
- * over it can cheaply bail out by reference, same spirit as
43
- * `GameStateBridge`'s frame-version cache). Shape is adapter-defined —
44
- * not centrally schematized in v1 (see `docs/REACT-STATE-BRIDGE.md` §6).
45
- */
46
- snapshot(): unknown;
47
- }
48
-
49
- /**
50
- * Everything a mounted world provides EXCEPT its render surface (T7.5,
51
- * `docs/BACKBONE-TASKS.md`'s D6 row — "replace THREE.Scene/THREE.Camera
52
- * typing with kind-tagged world surfaces"). Split out of the old flat
53
- * `MountedGame` so a non-threejs world can carry its own kind-appropriate
54
- * surface field (a pixi `stage`, a react `container`) instead of being
55
- * force-fit through THREE `scene`/`camera` fields it doesn't have.
56
- */
57
- export interface MountedWorldBase {
58
- /**
59
- * Loop model:
60
- * - `false` (host-driven): first-party + "clean" externals. The host ticks
61
- * `update(dt)` in its loop.
62
- * - `true` (self-driven): an unmodified game that owns its renderer + rAF.
63
- * The host does NOT tick it. Pause/step control is a DECLARED CAPABILITY,
64
- * not a promise: implement `setPaused`/`step` only where the game exposes
65
- * a sanctioned pause/step mechanism. Gating a raw-rAF loop from outside
66
- * was demonstrated and REJECTED (D5, docs/COMPOSITION-DESIGN.md §5.2 —
67
- * it halts the loop rather than pausing it); such games are the
68
- * "composited, unsynchronized" tier: `setPaused` absent, host degrades
69
- * loudly (T7.6 owns the tier surface).
70
- */
71
- readonly drivesOwnLoop: boolean;
72
-
73
- update?(dt: number): void; // host-driven only
74
- fixedUpdate?(dt: number): void;
75
- setPaused?(paused: boolean): void; // capability, not promise — see loop-model note above
76
- step?(): void;
77
-
78
- resize?(width: number, height: number): void;
79
- dispose(): void;
80
-
81
- /** Optional capability providers — absence = "not supported", host degrades. */
82
- readonly authoring?: AuthoringAdapter;
83
- readonly systems?: SystemAdapters;
84
- /**
85
- * Optional component-query capability: an adapter that runs the engine's
86
- * REAL `ComponentManager` without being the classic first-party mount
87
- * (e.g. `@engine/world3d-react`'s R3F adapter — `<Behavior>` attaches
88
- * through a real manager) exposes it here so `Game.queryByComponent`
89
- * (`runtime/game.ts`) aggregates this world's instances and
90
- * `registerThreeWorld` (`runtime/create-runtime.ts`) backfills
91
- * `instance.world` at registration, exactly like a first-party world.
92
- * Absent for a genuinely foreign mount that hosts no GameComponents —
93
- * such a world legitimately answers "no components".
94
- */
95
- readonly components?: ComponentManager;
96
- /** Optional state-observation capability (T7.4 slice 2, §4) — absent means
97
- * "no state bridge"; `Game.registerWorld` reports this loudly, once, for
98
- * any non-first-party mount. First-party mounts are exempt — their state
99
- * is observed through `Game.state`/`useGameState` instead (§3), not this
100
- * field. */
101
- readonly observe?: WorldStateObserver;
102
- }
103
-
104
- /** A live, mounted threejs world. The host obtains `scene`/`camera` to
105
- * render+author. This is what `MountedGame` (the permanent alias every
106
- * pre-T7.5 threejs-world call site still names) now means. */
107
- export interface MountedThreeWorld extends MountedWorldBase {
108
- readonly kind: 'threejs';
109
- /** The live scene + camera the editor inspects/renders for authoring. */
110
- readonly scene: THREE.Scene;
111
- readonly camera: THREE.Camera;
112
- }
113
-
114
- /** A live, mounted pixijs world (T7.3) — the pixi analog of
115
- * {@link MountedThreeWorld}. `stage` is the pixi world container the host
116
- * renders/authors, the surface `WorldInstance.pixiRoot()` returns. */
117
- export interface MountedPixiWorld extends MountedWorldBase {
118
- readonly kind: 'pixijs';
119
- readonly stage: Container;
120
- }
121
-
122
- /** A live, mounted react world (T6.2) — the react analog of
123
- * {@link MountedThreeWorld}. `container` is the DOM-root layer the host
124
- * handed the adapter's `mount` (the SAME element `WorldInstance.reactRoot()`
125
- * returns) — a react world's tree renders into it via `createRoot`. */
126
- export interface MountedReactWorld extends MountedWorldBase {
127
- readonly kind: 'react';
128
- readonly container: HTMLElement;
129
- }
130
-
131
- /** Every kind of live, mounted world (T7.5) — the union `WorldInstance.mounted`
132
- * is typed against now, replacing the THREE-only `MountedGame`. */
133
- export type MountedWorld = MountedThreeWorld | MountedPixiWorld | MountedReactWorld;
134
-
135
- /** Map a {@link AdapterSurface} to its mounted-world shape (mirrors `NodeOf`/
136
- * `BodyOf`/`ColliderOf` in `ecs/game-component.ts`) — lets generic code over
137
- * `K extends AdapterSurface` name the right surface without a manual union. */
138
- export type MountedWorldFor<K extends AdapterSurface> = K extends 'threejs'
139
- ? MountedThreeWorld
140
- : K extends 'pixijs'
141
- ? MountedPixiWorld
142
- : K extends 'react'
143
- ? MountedReactWorld
144
- : never;
145
-
146
- /**
147
- * Permanent alias (T7.1 precedent — old names stay valid forever once a
148
- * generalization lands): `MountedGame` now means "a mounted THREEJS world."
149
- * Every pre-T7.5 threejs-only call site (`VgaiMountedGame extends MountedGame`,
150
- * `registerThreeWorld`, ingest adapters, …) keeps compiling unchanged.
151
- */
152
- export type MountedGame = MountedThreeWorld;
153
-
154
- /** The interface every game implements to run on the host. Generic over
155
- * {@link AdapterSurface} (T7.5) so a non-threejs implementer's `mount` returns its
156
- * OWN kind-tagged surface instead of being cast through the threejs shape —
157
- * defaults to `'threejs'` so every pre-T7.5 implementer/call site
158
- * (`GameAdapter`, unparameterized) keeps compiling unchanged. */
159
- export interface GameAdapter<K extends AdapterSurface = 'threejs'> {
160
- /** Stable id (telemetry/registry/conformance). */
161
- readonly id: string;
162
- /** Build/start the game against a host-provided context; return the handle. */
163
- mount(host: HostContext): Promise<MountedWorldFor<K>>;
164
- }
@@ -1,207 +0,0 @@
1
- /**
2
- * `applyVgaiOverlay` — the T3.8 deliverable (docs/DECISIONS-PENDING.md §D9):
3
- * the opt-in runtime overlay applier a game owner adds to THEIR OWN deployment
4
- * so an adapted (ingested-then-edited) game ships with the editor's edits
5
- * visible, with zero involvement from the editor at runtime.
6
- *
7
- * The opt-in shape (D9's "minimal source opt-in, consistent with the
8
- * zero-diff-unless-opted-in invariant"): one import + one call, placed before
9
- * the game's own entry point runs (so the capture trap below is installed
10
- * before the game's `WebGLRenderer` is constructed):
11
- *
12
- * import * as THREE from 'three';
13
- * import { applyVgaiOverlay } from '@vgai/engine/adapter/ingest/overlay-applier';
14
- *
15
- * applyVgaiOverlay({ three: THREE, gameId: 'my-game' }); // fire-and-forget
16
- * import('./my-game-entry.js'); // the UNMODIFIED game, unchanged
17
- *
18
- * No scene is passed manually — this reuses the SAME render-accessor trap
19
- * (`./scene-capture.ts`) the editor's ingest mode uses to obtain the game's
20
- * live `Scene`/camera the moment it first renders, so this works for ANY
21
- * unmodified three.js game the SAME way ingestion already captures it (per
22
- * `scene-capture.ts`'s own caveat, this requires the game to share the host's
23
- * `three` module instance — a bundled/vendored copy of `three` cannot be
24
- * captured this way, matching the editor's own capture ceiling).
25
- *
26
- * Failure modes are all graceful (the game always keeps running unmodified)
27
- * but never silent — every one is logged via `console.warn` under the shared
28
- * `overlay-report` prefix (`./overlay-report.ts`) so ops can grep one string
29
- * for every overlay-related issue across both the editor and the ship path:
30
- * - no overlay file at `overlayUrl` (never authored, or a 404/network
31
- * error) → nothing applied, logged, scene never even captured;
32
- * - the game never renders within `captureTimeoutMs` (bundled/non-shared
33
- * three, or the game simply never boots) → nothing applied, logged;
34
- * - a version mismatch and/or orphaned overlay ids (D9) → whatever DOES
35
- * resolve is still applied; the report names what didn't.
36
- *
37
- * Precedence (D9 "overlay-vs-game-save-system precedence"): the overlay is
38
- * applied exactly ONCE, synchronously, in the continuation of the game's
39
- * FIRST captured render call — i.e. between that frame finishing and the
40
- * game's own next frame starting. Any later mutation of the SAME
41
- * property by the game's own logic (its own save/load system, a per-frame
42
- * animation, etc.) therefore wins from then on purely by construction: this
43
- * applier never re-applies and never re-reads the overlay after this one
44
- * pass. A property the game's own logic never revisits stays overlaid for
45
- * the life of the session; a property the game's own logic re-derives every
46
- * frame reverts to the game's own value on the very next frame. This
47
- * ordering is exercised directly by `overlay-applier.test.ts`'s "precedence"
48
- * case (apply, then simulate one more game-owned mutation frame).
49
- */
50
-
51
- import type * as THREE from 'three';
52
- import { applyOverlayToObjects, assignStructuralIds } from './overlay-apply';
53
- import { type OverlayFile, overlayPath, parseOverlayFile } from './overlay-file';
54
- import {
55
- buildOverlayApplyReport,
56
- logOverlayApplyReport,
57
- OVERLAY_REPORT_PREFIX,
58
- type OverlayApplyReport,
59
- } from './overlay-report';
60
- import { installSceneCapture } from './scene-capture';
61
-
62
- export interface ApplyVgaiOverlayOptions {
63
- /**
64
- * The game's own `three` module namespace — pass `import * as THREE from
65
- * 'three'` from the SAME module instance the game imports (required for the
66
- * capture trap to see the game's renderer; see `scene-capture.ts`).
67
- */
68
- three: unknown;
69
- /** Stable id for this game — namespaces the overlay file (matches the editor's `overlayPath(gameId)`). */
70
- gameId: string;
71
- /** Where to fetch the saved overlay from. Defaults to `overlayPath(gameId)` (`.vgai/overlays/<gameId>.json`, resolved relative to the deployed page — the editor saves to `<projectRoot>/.vgai/overlays/`; a deployment copies that file next to the built page). */
72
- overlayUrl?: string | undefined;
73
- /**
74
- * The deployed game's own version, if known — compared against the
75
- * overlay's `authoredAgainst.gameVersion` for drift reporting. Omit/null:
76
- * never fabricated (anti-shim) — a mismatch is only ever reported when
77
- * BOTH sides are known (`./overlay-report.ts`).
78
- */
79
- gameVersion?: string | null | undefined;
80
- /**
81
- * R5 (docs/MASTER-ARCHITECTURE-REVIEW.md §4(c)): the deployed game's own
82
- * `UPSTREAM.md` pin, if the game owner happens to know it (e.g. baked in at
83
- * build time from their own vendoring step). Compared against the overlay's
84
- * `authoredAgainst.upstreamPin` for the same drift reporting `gameVersion`
85
- * gets, but catches a same-slot SUBSTITUTION that `gameVersion` alone
86
- * cannot (see `./upstream-pin.ts` + `./overlay-report.ts`'s header).
87
- * DELIBERATE NO-OP by default: this ship-path applier runs in the game
88
- * owner's OWN deployed build, which has no filesystem access to the
89
- * `UPSTREAM.md` that lived in the vgai monorepo/editor project at
90
- * ingest-authoring time — there is no way for this applier to discover the
91
- * pin itself, so it is never fabricated (anti-shim) and the check silently
92
- * skips (report's `upstreamPin.mismatch` stays `false`) unless the caller
93
- * supplies this option explicitly.
94
- */
95
- upstreamPin?: string | null | undefined;
96
- /** How long to wait for the game's first captured render call before giving up (ms). Defaults to `installSceneCapture`'s own default (10s). */
97
- captureTimeoutMs?: number | undefined;
98
- /** Injectable `fetch` (tests / non-browser hosts). Defaults to the global `fetch`. */
99
- fetchImpl?: typeof fetch | undefined;
100
- }
101
-
102
- export interface ApplyVgaiOverlayResult {
103
- /** Whether the game's scene was actually captured (false only when capture itself timed out or no overlay was found — either way the game runs unmodified). */
104
- captured: boolean;
105
- /** The D9 apply report, or `null` when there was no overlay to apply at all (not an error — the game runs unmodified). */
106
- report: OverlayApplyReport | null;
107
- /**
108
- * The captured scene/camera, present only when `captured` is true. Most
109
- * callers (a game's own entry point) have no reason to touch this — it's
110
- * surfaced for diagnostics/tooling (and for tests, which use it to inspect
111
- * the live post-apply scene the same way the editor's ingest proof pages do)
112
- * rather than requiring a second, independent capture.
113
- */
114
- scene?: THREE.Scene | undefined;
115
- camera?: THREE.Camera | undefined;
116
- }
117
-
118
- /** Shared warn helper — every graceful-failure path below logs through here. */
119
- function warn(gameId: string, message: string): void {
120
- // biome-ignore lint/suspicious/noConsole: deliberate, greppable (mirrors overlay-report.ts's own console.warn)
121
- console.warn(`${OVERLAY_REPORT_PREFIX} game "${gameId}": ${message}`);
122
- }
123
-
124
- async function fetchOverlayFile(
125
- url: string,
126
- gameId: string,
127
- fetchImpl: typeof fetch | undefined,
128
- ): Promise<OverlayFile | null> {
129
- const doFetch = fetchImpl ?? (typeof fetch === 'function' ? fetch : undefined);
130
- if (!doFetch) {
131
- warn(gameId, 'no fetch implementation available — overlay not applied, running unmodified.');
132
- return null;
133
- }
134
- try {
135
- const res = await doFetch(url);
136
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
137
- const raw: unknown = await res.json();
138
- return parseOverlayFile(raw);
139
- } catch (err) {
140
- const reason = err instanceof Error ? err.message : String(err);
141
- warn(gameId, `no overlay applied (${reason}) — running unmodified.`);
142
- return null;
143
- }
144
- }
145
-
146
- /**
147
- * The opt-in ship-path applier — see this module's header for the full
148
- * contract. Installs the scene-capture trap SYNCHRONOUSLY (before the first
149
- * `await`), so a caller that follows the documented pattern (call this, THEN
150
- * import/boot the game) never races the trap against the game's first frame
151
- * — the trap's `captured` slot latches on the game's first render regardless
152
- * of when (or whether) this function later calls `waitForCapture` on it.
153
- * Only PROCEEDS to actually wait for that capture once an overlay has been
154
- * found (fetched first, deliberately) — the common "never edited this game"
155
- * case then costs one fetch, never a multi-second capture-timeout wait.
156
- */
157
- export async function applyVgaiOverlay(
158
- options: ApplyVgaiOverlayOptions,
159
- ): Promise<ApplyVgaiOverlayResult> {
160
- const { three, gameId, gameVersion = null, upstreamPin = null, fetchImpl } = options;
161
- const overlayUrl = options.overlayUrl ?? overlayPath(gameId);
162
-
163
- // Install the capture trap before anything else in this function (and
164
- // before returning control to the caller's microtask queue) — see the
165
- // header comment's ordering guarantee.
166
- const capture = installSceneCapture(three);
167
-
168
- const overlayFile = await fetchOverlayFile(overlayUrl, gameId, fetchImpl);
169
- if (!overlayFile) {
170
- // Nothing to apply (never authored, 404, network error — already logged
171
- // by fetchOverlayFile) — don't bother waiting for a render at all.
172
- capture.uninstall();
173
- return { captured: false, report: null };
174
- }
175
-
176
- let rt: Awaited<ReturnType<typeof capture.waitForCapture>>;
177
- try {
178
- rt = await capture.waitForCapture(options.captureTimeoutMs);
179
- } catch (err) {
180
- capture.uninstall();
181
- const reason = err instanceof Error ? err.message : String(err);
182
- warn(gameId, `scene capture failed (${reason}) — overlay not applied, running unmodified.`);
183
- return { captured: false, report: null };
184
- }
185
- capture.uninstall(); // one-shot: we have what we need, stop wrapping future renders.
186
-
187
- const { byId } = assignStructuralIds(rt.scene, rt.camera);
188
- const { applied, orphanedIds } = applyOverlayToObjects(byId, overlayFile.overrides);
189
-
190
- const report = buildOverlayApplyReport({
191
- gameId,
192
- authoredAgainst: overlayFile.authoredAgainst.gameVersion,
193
- currentVersion: gameVersion,
194
- applied,
195
- orphanedIds,
196
- // R5: `authoredUpstreamPin` passes through undefined for a legacy overlay
197
- // (the key was never in `authoredAgainst` at all — see overlay-file.ts),
198
- // preserving the "never checked" vs. "checked, found nothing" distinction
199
- // all the way from disk. `currentUpstreamPin` is this no-op-by-default
200
- // option (see its doc comment above) — `null` unless a game owner passes it.
201
- authoredUpstreamPin: overlayFile.authoredAgainst.upstreamPin,
202
- currentUpstreamPin: upstreamPin,
203
- });
204
- logOverlayApplyReport(report);
205
-
206
- return { captured: true, report, scene: rt.scene, camera: rt.camera };
207
- }
@@ -1,168 +0,0 @@
1
- /**
2
- * Ingest overlay application core — the structural-path identity scheme and
3
- * override-application loop shared by the EDITOR's `IngestAuthoringAdapter`
4
- * (`packages/editor/src/authoring/ingest-authoring-adapter.ts`, which layers
5
- * hierarchy/selection/undo bookkeeping over it) and the T3.8 runtime ship-path
6
- * applier (`./overlay-applier.ts`, which uses it directly with none of that
7
- * editor bookkeeping). Extracted here (rather than duplicated) per D9's task
8
- * split (docs/DECISIONS-PENDING.md §D9) — the editor re-exports/reuses this
9
- * module so its behavior (and the ids it mints) stay byte-for-byte identical
10
- * between "authored in the editor" and "reapplied at ship time".
11
- *
12
- * Identity: each object gets a deterministic **structural-path id** — stable
13
- * for a given scene shape (position in the tree + three.js type + name), so
14
- * the SAME id re-binds to the SAME object after an independent fresh walk of
15
- * an unmodified game's rebuilt scene (a reload, or a separate page entirely —
16
- * this is what lets an overlay authored in the editor apply cleanly in a
17
- * shipped build that never ran the editor). It is also written to
18
- * `userData.entityId` so the editor's object map / gizmo / selection (which
19
- * key on `entityId`) bind to foreign objects with no further shim.
20
- */
21
-
22
- import type * as THREE from 'three';
23
- import { setUserData } from '../../scene/user-data';
24
- import type { IngestOverlay } from './overlay-file';
25
-
26
- /** Fixed id for the captured render camera (not a scene child, so no structural path). */
27
- export const CAMERA_ID = 'ingest:camera';
28
-
29
- /**
30
- * The editor parks its OWN objects — grid, its two lights, the particle
31
- * BatchedRenderer, every TransformControls/gizmo helper — on layer 31
32
- * (`packages/editor/src/editor-layers.ts`'s `EDITOR_LAYER`) so the game camera
33
- * never sees them. In ingest mode those objects are added to the GAME'S OWN
34
- * scene, which is the same tree this walk indexes.
35
- */
36
- const EDITOR_ONLY_LAYER_MASK = 1 << 31;
37
-
38
- /**
39
- * Editor furniture, not game content. Excluded from the walk entirely — see
40
- * {@link assignStructuralIds} for why that exclusion is what makes the id
41
- * scheme's central promise true.
42
- */
43
- function isEditorOnly(o: THREE.Object3D): boolean {
44
- return (o.layers.mask & EDITOR_ONLY_LAYER_MASK) !== 0;
45
- }
46
-
47
- /** The single (non-array) standard-ish material of a mesh, if it has a color. */
48
- export function colorMaterialOf(o: THREE.Object3D): THREE.MeshStandardMaterial | null {
49
- const mat = (o as THREE.Mesh).material;
50
- if (!mat || Array.isArray(mat)) return null;
51
- const m = mat as THREE.MeshStandardMaterial;
52
- return m.color ? m : null;
53
- }
54
-
55
- /** Result of a structural-path walk: the id→object map plus a few reflection stats. */
56
- export interface StructuralIdWalk {
57
- byId: Map<string, THREE.Object3D>;
58
- count: number;
59
- meshes: number;
60
- lights: number;
61
- }
62
-
63
- /**
64
- * (Re)assign structural-path ids to every object under `scene` (and, if
65
- * given, the separately-captured render `camera`, under the fixed
66
- * {@link CAMERA_ID}). Idempotent and deterministic from scene structure
67
- * (position + type + name) — the prerequisite for overlay reapplication
68
- * across an independent fresh walk (editor reload, or a shipped build that
69
- * never ran the editor at all).
70
- *
71
- * EDITOR FURNITURE IS EXCLUDED, and that exclusion is load-bearing rather than
72
- * cosmetic. In ingest mode the editor adds its grid, lights, particle
73
- * BatchedRenderer and TransformControls gizmo to the GAME'S OWN scene, and
74
- * they arrive asynchronously — some before the walk, some after. Indexing them
75
- * made a game object's path depend on how much editor furniture happened to be
76
- * attached at walk time, which broke the two promises above:
77
- *
78
- * • Across a RELOAD, the same mesh changed id. Observed in the games-fps
79
- * overlay-persistence e2e: an object saved as `ingest:106/0/0/0:…` came
80
- * back as `ingest:102/0:Mesh:Cube004` — four root slots earlier, exactly
81
- * the grid + two editor lights + BatchedRenderer — so the persisted
82
- * override landed on nothing and was reported as an orphan.
83
- * • A SHIPPED build (no editor, so no furniture at all) minted a different
84
- * id set than the editor that authored the overlay — i.e. the ship path
85
- * this module exists to serve could not reliably consume its own output.
86
- *
87
- * Skipping the furniture entirely — not merely declining to give it an id —
88
- * is what fixes both: the game's own children occupy 0..N-1 whatever else is
89
- * parented alongside them. It also keeps gizmo handles and the grid out of the
90
- * hierarchy/inspector projections built on this walk, where they were
91
- * selectable and colorable as if they were game content.
92
- */
93
- export function assignStructuralIds(
94
- scene: THREE.Object3D,
95
- camera?: THREE.Object3D | undefined,
96
- ): StructuralIdWalk {
97
- const byId = new Map<string, THREE.Object3D>();
98
- let count = 0;
99
- let meshes = 0;
100
- let lights = 0;
101
- const visit = (
102
- o: THREE.Object3D & { isMesh?: boolean; isLight?: boolean },
103
- path: string,
104
- ): void => {
105
- const id = `ingest:${path}:${o.type}:${o.name || ''}`;
106
- setUserData(o, 'entityId', id);
107
- byId.set(id, o);
108
- count++;
109
- if (o.isLight) lights++;
110
- else if (o.isMesh) meshes++;
111
- visitChildren(o, path);
112
- };
113
- const visitChildren = (o: THREE.Object3D, path: string): void => {
114
- let i = 0;
115
- for (const c of o.children) {
116
- if (isEditorOnly(c)) continue;
117
- visit(c, path ? `${path}/${i}` : `${i}`);
118
- i++;
119
- }
120
- };
121
- visitChildren(scene, '');
122
- if (camera) {
123
- setUserData(camera, 'entityId', CAMERA_ID);
124
- byId.set(CAMERA_ID, camera);
125
- count++;
126
- }
127
- return { byId, count, meshes, lights };
128
- }
129
-
130
- /** Result of applying a saved overlay onto a structural-id-walked scene. */
131
- export interface OverlayApplyOutcome {
132
- applied: number;
133
- orphanedIds: string[];
134
- }
135
-
136
- /**
137
- * Apply a saved overlay's overrides onto the live objects in `byId` (from
138
- * {@link assignStructuralIds}). Ids absent from the map are "orphaned" — e.g. a
139
- * dynamic child that didn't respawn, or the vendored game was bumped and
140
- * restructured — never an error; the caller reports them (never silently) via
141
- * `./overlay-report.ts`. Mutates matched objects in place; does not mutate
142
- * `overlay` itself or track dirty/undo state — that bookkeeping is the
143
- * editor-side `IngestAuthoringAdapter`'s job (persistable-edit tracking, so
144
- * orphans round-trip on the next save) and is irrelevant to the ship-path
145
- * applier (which never saves).
146
- */
147
- export function applyOverlayToObjects(
148
- byId: Map<string, THREE.Object3D>,
149
- overlay: IngestOverlay,
150
- ): OverlayApplyOutcome {
151
- let applied = 0;
152
- const orphanedIds: string[] = [];
153
- for (const [id, ov] of Object.entries(overlay)) {
154
- const o = byId.get(id);
155
- if (!o) {
156
- orphanedIds.push(id);
157
- continue;
158
- }
159
- if (ov.position) o.position.set(ov.position[0], ov.position[1], ov.position[2]);
160
- if (ov.rotation)
161
- o.quaternion.set(ov.rotation[0], ov.rotation[1], ov.rotation[2], ov.rotation[3]);
162
- if (ov.scale) o.scale.set(ov.scale[0], ov.scale[1], ov.scale[2]);
163
- if (ov.color) colorMaterialOf(o)?.color.set(ov.color);
164
- if (ov.visible !== undefined) o.visible = ov.visible;
165
- applied++;
166
- }
167
- return { applied, orphanedIds };
168
- }
@@ -1,126 +0,0 @@
1
- /**
2
- * Ingest overlay envelope — the on-disk shape shared by every consumer of a
3
- * saved ingest overlay: the editor (`packages/editor/src/authoring/ingest-overlay.ts`,
4
- * which persists it through the storage-backend seam) AND the T3.8 runtime ship-path
5
- * applier (`./overlay-applier.ts`, which fetches it over plain HTTP in a deployed
6
- * game with no editor/storage-backend dependency at all).
7
- *
8
- * This module is the dependency-free core (types + pure parsing) both share — moved
9
- * here (from the editor-only module that used to own it) per D9's task split
10
- * (docs/DECISIONS-PENDING.md §D9): the editor keeps the storage-backend-aware
11
- * `loadOverlay`/`saveOverlay` and re-exports everything below unchanged so its
12
- * existing import sites don't churn.
13
- */
14
-
15
- /** A single object's persisted overrides (only the fields the user changed). */
16
- export interface IngestOverride {
17
- position?: [number, number, number];
18
- rotation?: [number, number, number, number];
19
- scale?: [number, number, number];
20
- color?: string;
21
- visible?: boolean;
22
- }
23
-
24
- /** id (structural-path) → overrides. */
25
- export type IngestOverlay = Record<string, IngestOverride>;
26
-
27
- /** Current on-disk envelope format version (D9, docs/DECISIONS-PENDING.md §D9). */
28
- export const OVERLAY_FORMAT_VERSION = 1;
29
-
30
- /**
31
- * The on-disk overlay envelope (D9 — overlay versioning + orphan reporting).
32
- * Wraps the flat `IngestOverlay` (id → overrides) with save-time provenance:
33
- * which game version the edits were authored against, so a later mount (editor
34
- * OR shipped runtime) can detect drift and report it (`./overlay-report.ts`'s
35
- * `OverlayApplyReport`).
36
- *
37
- * `formatVersion` distinguishes this from the PRE-D9 flat file (the whole JSON
38
- * file WAS the `IngestOverlay`): {@link parseOverlayFile} recognizes both. An
39
- * object with `formatVersion` + `overrides` parses as the real envelope; any
40
- * other shape (or unparseable content) parses as a legacy flat file, wrapped as
41
- * `{ formatVersion: 0, authoredAgainst: { gameVersion: null }, overrides: <the flat object> }`
42
- * — never rejected, never warned about by itself.
43
- *
44
- * `authoredAgainst.upstreamPin` (R5, docs/MASTER-ARCHITECTURE-REVIEW.md
45
- * §4(c)/§7.1 item 9 — the overlay-identity gap `gameVersion` alone doesn't
46
- * close: `gameVersion` is the manifest's own `version` field, which a
47
- * re-vendor of the upstream game need not bump — see every `public/ingest/*`
48
- * game's `vgai.game.json`, all pinned at `"version": "0.1.0"` regardless of
49
- * upstream commit) is an OPTIONAL sibling key, deliberately absent from this
50
- * type's older call sites' literal shape: its ABSENCE (the key missing from
51
- * `authoredAgainst` entirely) is the "legacy overlay" signal — saved by code
52
- * that predates this field, so the pin concept never existed for it, and
53
- * `./overlay-report.ts`'s apply-time check must skip it (not treat it as a
54
- * guaranteed mismatch OR a guaranteed match — anti-shim, no guessing). A
55
- * `null` VALUE (key present, value `null`) means R5-aware save-time code DID
56
- * look for a pin and honestly found none (e.g. an externally-authored,
57
- * non-vendored project opened via the CLI-on-folder route, which has no
58
- * `UPSTREAM.md` at all). See `./upstream-pin.ts` for the pure extraction, and
59
- * `packages/editor/src/ingest-mode.ts` for where it's read at save/apply
60
- * time. This is purely additive — {@link parseOverlayFile} never invents the
61
- * key for an envelope that didn't have it.
62
- */
63
- export interface OverlayFile {
64
- formatVersion: number;
65
- authoredAgainst: { gameVersion: string | null; upstreamPin?: string | null };
66
- overrides: IngestOverlay;
67
- }
68
-
69
- /** Where a game's overlay lives, project-root-relative (also the ship path's default fetch URL). */
70
- export function overlayPath(gameId: string): string {
71
- return `.vgai/overlays/${gameId}.json`;
72
- }
73
-
74
- /** The empty envelope — no overlay found / nothing authored yet. */
75
- export function emptyOverlayFile(): OverlayFile {
76
- return {
77
- formatVersion: OVERLAY_FORMAT_VERSION,
78
- authoredAgainst: { gameVersion: null },
79
- overrides: {},
80
- };
81
- }
82
-
83
- /** An envelope has both its distinguishing keys — anything else is a legacy flat file. */
84
- function isEnvelope(raw: unknown): raw is OverlayFile {
85
- return (
86
- !!raw &&
87
- typeof raw === 'object' &&
88
- 'formatVersion' in (raw as Record<string, unknown>) &&
89
- 'overrides' in (raw as Record<string, unknown>)
90
- );
91
- }
92
-
93
- /**
94
- * Parse a loaded/fetched overlay's raw JSON into the {@link OverlayFile} shape,
95
- * transparently upgrading a legacy (pre-D9) flat file — the whole parsed value
96
- * IS the `IngestOverlay` in that case. Pure — no I/O, no storage backend, so
97
- * it's usable from any context (editor `loadOverlay`, the ship-path applier's
98
- * `fetch().then(r => r.json())`).
99
- */
100
- export function parseOverlayFile(raw: unknown): OverlayFile {
101
- if (isEnvelope(raw)) {
102
- const rawAuthored = (raw.authoredAgainst ?? { gameVersion: null }) as {
103
- gameVersion?: string | null;
104
- upstreamPin?: string | null;
105
- };
106
- const authoredAgainst: OverlayFile['authoredAgainst'] = {
107
- gameVersion: rawAuthored.gameVersion ?? null,
108
- };
109
- // Preserve ABSENCE vs. explicit `null` (see this field's doc comment
110
- // above) — only copy the key over if the raw JSON actually had it, rather
111
- // than defaulting it in (which would make every pre-R5 saved overlay look
112
- // like a "checked, found nothing" R5 save instead of "never checked").
113
- if ('upstreamPin' in rawAuthored) authoredAgainst.upstreamPin = rawAuthored.upstreamPin ?? null;
114
- return {
115
- formatVersion: raw.formatVersion,
116
- authoredAgainst,
117
- overrides: raw.overrides ?? {},
118
- };
119
- }
120
- // Legacy flat file (pre-D9): the whole parsed object IS the overrides map.
121
- return {
122
- formatVersion: 0,
123
- authoredAgainst: { gameVersion: null },
124
- overrides: (raw ?? {}) as IngestOverlay,
125
- };
126
- }