@vgai/engine 0.5.2 → 0.5.4

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,4 +1,4 @@
1
- // T3.1 slice 1 — the game manifest loader/validator (docs/GAME-MANIFEST-DESIGN.md §5).
1
+ // The game manifest loader/validator.
2
2
  //
3
3
  // Pure data-in/data-out: `loadGameManifest(raw)` parses `raw` against
4
4
  // `GameManifestSchema`, runs the cross-field checks §3 promises beyond what
@@ -31,23 +31,22 @@ import {
31
31
  export type ResolvedAdapter =
32
32
  | {
33
33
  readonly type: 'builtin';
34
- readonly identity: 'threejs' | 'pixijs' | 'react';
35
- readonly surface: 'threejs' | 'pixijs' | 'react';
34
+ readonly identity: 'three' | 'canvas' | 'dom';
35
+ readonly surface: 'three' | 'canvas' | 'dom';
36
36
  }
37
37
  | {
38
38
  readonly type: 'module';
39
39
  readonly identity: 'module';
40
40
  readonly module: string;
41
- readonly surface: 'threejs' | 'pixijs' | 'react';
41
+ readonly surface: 'three' | 'canvas' | 'dom';
42
42
  }
43
43
  | {
44
44
  readonly type: 'ingest';
45
- // 'ingest-react' (Track N, N1 docs/REACT-INGEST-LANDING-DESIGN.md
46
- // D-N2) is assigned here purely structurally (kind === 'react'), same
47
- // as 'ingest-three'/'ingest-pixi': this loader stays strategy-agnostic
48
- // at resolve time, exactly like the other two ingest identities
49
- // (`ceilingFor`/`resolveCapabilities` below already derive tiers per
50
- // `strategy` regardless of `kind`). The v1 "deduped rung only" rule
45
+ // 'ingest-react' (Track N, N1) is assigned here purely structurally (kind
46
+ // === 'dom'), same as 'ingest-three'/'ingest-pixi': this loader stays
47
+ // strategy-agnostic at resolve time, exactly like the other two ingest
48
+ // identities (`ceilingFor`/`resolveCapabilities` below already derive tiers
49
+ // per `strategy` regardless of `kind`). The v1 "deduped rung only" rule
51
50
  // (D-N3) is enforced editor-side, at the resolver
52
51
  // (`packages/editor/src/adapter-resolver.ts`'s `resolveIngestReactAdapter`)
53
52
  // — not here — mirroring how THIS file never rejects
@@ -55,13 +54,13 @@ export type ResolvedAdapter =
55
54
  // strategies either; `resolveIngestDescriptor`/`resolveIngest2DDescriptor`
56
55
  // are where "not yet buildable" strategies throw.
57
56
  readonly identity: 'ingest-three' | 'ingest-pixi' | 'ingest-react';
58
- readonly surface: 'threejs' | 'pixijs' | 'react';
57
+ readonly surface: 'three' | 'canvas' | 'dom';
59
58
  readonly strategy: IngestStrategy;
60
59
  readonly entryHtml: string | undefined;
61
60
  readonly assets: Record<string, string> | undefined;
62
61
  readonly domStubs: string[] | undefined;
63
62
  readonly captureTimeoutMs: number | undefined;
64
- /** Track P (docs/PIXI-INGEST-LANDING-DESIGN.md §2) — iframe-reachable-multi mount fields. */
63
+ /** Iframe-reachable-multi mount fields. */
65
64
  readonly bundleUrl: string | undefined;
66
65
  readonly baseHref: string | undefined;
67
66
  readonly assetBaseUrl: string | undefined;
@@ -75,10 +74,12 @@ export interface ResolvedAdapterRoot {
75
74
  readonly surface: ResolvedAdapter['surface'];
76
75
  readonly description: string | undefined;
77
76
  readonly adapter: ResolvedAdapter;
78
- readonly scene: string | undefined;
79
77
  readonly entry: string | undefined;
80
78
  readonly zOrder: number;
81
79
  readonly pausable: boolean;
80
+ /** `dev: true` — this root is a DEV LAYER, not shipped game content. See
81
+ * `manifest/schema.ts`'s `dev` field for the four readers. */
82
+ readonly dev: boolean;
82
83
  readonly loop: 'gated' | 'self-driven';
83
84
  /** Every optional resolved: both contexts always present (§4/§5). */
84
85
  readonly capabilities: { readonly local: Tier; readonly hosted: Tier };
@@ -88,7 +89,6 @@ export interface ResolvedGameManifest {
88
89
  readonly manifestVersion: 2;
89
90
  readonly name: string;
90
91
  readonly appId: string | undefined;
91
- readonly editorPort: number | undefined;
92
92
  readonly version: string;
93
93
  readonly engine: { readonly version: string };
94
94
  /** Sorted by zOrder; ties broken by original array order (§3). */
@@ -116,7 +116,7 @@ export interface ResolvedGameManifest {
116
116
  /** D15 — read by `mount-manifest.ts`'s boot-time seeding reader (gates
117
117
  * whether `ctx.random` is seeded from `defaultSeed`/`?vgai-seed=`), the
118
118
  * gameplay-rng-ban burn-down scan, and the dev-mode Math.random phase
119
- * trap. See `docs/D15-DETERMINISM-DESIGN.md`. */
119
+ * trap.. */
120
120
  readonly determinism:
121
121
  | { readonly seededRandom: boolean; readonly defaultSeed?: number | undefined }
122
122
  | undefined;
@@ -133,16 +133,16 @@ export interface ResolvedGameManifest {
133
133
  readonly maxTriangles?: number | undefined;
134
134
  }
135
135
  | undefined;
136
- /** FT-5 (docs/FTUE-LEARN-SYSTEM-DESIGN.md §3) learner-facing metadata on
137
- * shipped example projects; read by the editor server's registry surface
138
- * (`GET /__editor/examples`, packages/editor/server/editor-server.ts) to
139
- * generate the gallery/wizard/Learn-catalog registries. */
136
+ /** Learner-facing metadata on shipped example projects; read by the editor
137
+ * server's registry surface (`GET /__editor/examples`,
138
+ * packages/editor/server/editor-server.ts) to generate the
139
+ * gallery/wizard/Learn-catalog registries. */
140
140
  readonly learn: LearnMetadata | undefined;
141
141
  }
142
142
 
143
143
  // ---------------------------------------------------------------------------
144
- // §4 — the per-context ceiling table (named + exported: the tier-table doc
145
- // test in slice 2 pins docs/CAPABILITY-TIERS.md's rows against this).
144
+ // §4 — the per-context ceiling table (named + exported so the tier-table test
145
+ // can pin its rows against this).
146
146
  // ---------------------------------------------------------------------------
147
147
 
148
148
  /**
@@ -204,45 +204,26 @@ function isModuleAdapter(
204
204
  return typeof adapter === 'object' && 'module' in adapter;
205
205
  }
206
206
 
207
- function isIngestAdapter(
208
- adapter: RootAdapter,
209
- ): adapter is Extract<RootAdapter, { ingest: { strategy: IngestStrategy } }> {
210
- return typeof adapter === 'object' && 'ingest' in adapter;
211
- }
212
-
213
207
  // ---------------------------------------------------------------------------
214
208
  // Cross-field checks (§3 — beyond what Zod alone enforces)
215
209
  // ---------------------------------------------------------------------------
216
210
  //
217
- // D-V6 (docs/WAVE5-MULTIWORLD-INGEST-DESIGN.md): root-id uniqueness used to
218
- // be a cross-field check HERE (`checkUniqueWorldIds`, removed) — it moved
219
- // INTO `GameManifestSchema`'s `roots` `superRefine` (schema.ts) instead, so
220
- // the generated JSON Schema and any other schema consumer see the same
221
- // constraint the loader always enforced. `parseManifest` below catches that
222
- // one recognizable schema-level issue and rethrows it as a clean, single
223
- // `Error` the exact message shape `checkUniqueWorldIds` used to throw
224
- // directly — rather than a raw multi-issue `ZodError` dump, so existing
225
- // callers/tests that pattern-match the message text are unaffected by the
226
- // enforcement moving one layer down.
227
-
228
- function checkAdapterSceneEntryRules(root: AdapterRoot): void {
211
+ // Root-id uniqueness used to be a cross-field check HERE
212
+ // (`checkUniqueRootIds`, removed) — it moved INTO `GameManifestSchema`'s
213
+ // `roots` `superRefine` (schema.ts) instead, so the generated JSON Schema and
214
+ // any other schema consumer see the same constraint the loader always
215
+ // enforced. `parseManifest` below catches that one recognizable schema-level
216
+ // issue and rethrows it as a clean, single `Error` — the exact message shape
217
+ // `checkUniqueRootIds` used to throw directly rather than a raw multi-issue
218
+ // `ZodError` dump, so existing callers/tests that pattern-match the message
219
+ // text are unaffected by the enforcement moving one layer down.
220
+
221
+ function checkAdapterEntryRules(root: AdapterRoot): void {
229
222
  const { adapter } = root;
230
- if (typeof adapter === 'string' && !root.scene && !root.entry) {
231
- throw new Error(
232
- `Game manifest: root "${root.id}" uses the '${adapter}' adapter but declares neither ` +
233
- '`scene` nor `entry` — a built-in adapter root requires an authored document or entry.',
234
- );
235
- }
236
- if (adapter === 'react' && root.scene !== undefined) {
237
- throw new Error(
238
- `Game manifest: React root "${root.id}" declares \`scene\` — React roots use JSX entry modules.`,
239
- );
240
- }
241
- if (isIngestAdapter(adapter) && root.scene !== undefined) {
223
+ if (typeof adapter === 'string' && !root.entry) {
242
224
  throw new Error(
243
- `Game manifest: root "${root.id}" uses an { ingest } adapter but declares \`scene\` ` +
244
- 'ingest adapters forbid `scene` (adapt-never-migrate: ingested games keep their own ' +
245
- "formats; overlay persistence is T3.2's) (§3).",
225
+ `Game manifest: root "${root.id}" uses the '${adapter}' adapter but declares no ` +
226
+ '`entry` a built-in adapter root is authored as an entry module.',
246
227
  );
247
228
  }
248
229
  }
@@ -250,14 +231,12 @@ function checkAdapterSceneEntryRules(root: AdapterRoot): void {
250
231
  // Historical note (Track N, N1): this file used to reject EVERY `kind:
251
232
  // 'react'` + `{ ingest }` root outright here (`checkReactAdapterRestriction`,
252
233
  // citing "no react ingest program is un-parked"). Track N un-parks exactly
253
- // that program (docs/REACT-INGEST-LANDING-DESIGN.md) — react+ingest is now a
254
- // structurally legal combination, resolved to the `'ingest-react'` identity
255
- // by `resolveAdapter` below like any other ingest root. There is nothing
256
- // left to reject at THIS layer: `checkAdapterSceneEntryRules` above already
257
- // forbids `scene` on any `{ ingest }`-adapter root (kind-agnostic), and the
258
- // v1 "deduped strategy only" restriction is an editor-resolver concern
259
- // (D-N3), not a manifest-shape concern — see the `ResolvedAdapter` type's
260
- // comment above.
234
+ // that program — react+ingest is now a structurally legal combination,
235
+ // resolved to the `'ingest-react'` identity by `resolveAdapter` below like
236
+ // any other ingest root. There is nothing left to reject at THIS layer: the
237
+ // v1 "deduped strategy only" restriction is an editor-resolver concern (D-N3),
238
+ // not a manifest-shape concern see the `ResolvedAdapter` type's comment
239
+ // above.
261
240
 
262
241
  // ---------------------------------------------------------------------------
263
242
  // Resolution (§5)
@@ -283,9 +262,9 @@ function resolveAdapter(root: AdapterRoot): ResolvedAdapter {
283
262
  // 'ingest-pixi' — see the `ResolvedAdapter` type's comment above for why
284
263
  // this stays strategy-agnostic here).
285
264
  const identity =
286
- adapter.surface === 'threejs'
265
+ adapter.surface === 'three'
287
266
  ? 'ingest-three'
288
- : adapter.surface === 'pixijs'
267
+ : adapter.surface === 'canvas'
289
268
  ? 'ingest-pixi'
290
269
  : 'ingest-react';
291
270
  const ingest = adapter.ingest;
@@ -363,16 +342,19 @@ function checkEngineVersionPin(version: string): void {
363
342
  * Every `AdapterRoot` field participates here or in a check above:
364
343
  * `id` drives uniqueness; `adapter` (incl. its surface and
365
344
  * `module`/`ingest.*` sub-fields) drives identity + capability-ceiling
366
- * resolution; `scene`/`entry` drive the scene/entry cross-field rule and are
345
+ * resolution; `entry` drives the entry cross-field rule and is
367
346
  * carried through; `zOrder` drives the final sort; `pausable`/`loop` are
368
347
  * carried through as-is (their consumers are the runtime/CLI, T3.2/T3.3);
348
+ * `dev` is carried through for the four dev-layer readers (`mountManifestRoots`'s
349
+ * ship gate, the host's topmost stacking pass, the play compositor's capture
350
+ * exclusion, and the editor's chrome-not-content seam);
369
351
  * `capabilities.{local,hosted}` drive tier resolution; `description` (T3.3
370
352
  * slice 3) is carried through as-is — its consumer today is
371
353
  * `resolveIngestDescriptor` (packages/editor/src/adapter-resolver.ts), which
372
354
  * threads an ingest-three root's description into `IngestGame.description`.
373
355
  */
374
356
  function resolveRoot(root: AdapterRoot): ResolvedAdapterRoot {
375
- checkAdapterSceneEntryRules(root);
357
+ checkAdapterEntryRules(root);
376
358
  const adapter = resolveAdapter(root);
377
359
 
378
360
  return {
@@ -380,10 +362,10 @@ function resolveRoot(root: AdapterRoot): ResolvedAdapterRoot {
380
362
  surface: adapter.surface,
381
363
  description: root.description,
382
364
  adapter,
383
- scene: root.scene,
384
365
  entry: root.entry,
385
366
  zOrder: root.zOrder,
386
367
  pausable: root.pausable,
368
+ dev: root.dev,
387
369
  loop: root.loop,
388
370
  capabilities: resolveCapabilities(root),
389
371
  };
@@ -400,18 +382,25 @@ function resolveRoot(root: AdapterRoot): ResolvedAdapterRoot {
400
382
  * any Zod validation failure or cross-field rule violation.
401
383
  */
402
384
  export function loadGameManifest(raw: unknown): ResolvedGameManifest {
385
+ // Removed-format guard FIRST, on the raw value: a legacy `.vscn.json` root
386
+ // would otherwise fail Zod's `roots` discriminated union with a confusing
387
+ // shape error instead of a migration message (WO-8).
403
388
  const parsed = GameManifestSchema.safeParse(raw);
404
389
  if (!parsed.success) {
405
- // D-V6: the `roots` `superRefine`'s duplicate-id issue is the one
406
- // schema-level violation this loader still surfaces as a clean, single
407
- // `Error` (matching `checkUniqueWorldIds`'s pre-D-V6 message shape)
408
- // every other schema violation keeps propagating as the raw `ZodError`
409
- // it always has (unchanged behavior, e.g. the iframe-reachable
410
- // field-exclusivity / manifestVersion checks).
411
- const dupIssue = parsed.error.issues.find((issue) =>
412
- issue.message.startsWith('Game manifest: duplicate root id'),
390
+ // D-V6: the `roots` `superRefine`'s composition issues a duplicate root
391
+ // id, or more than one world root of the same medium — are the
392
+ // schema-level violations this loader surfaces as a clean, single `Error`
393
+ // (matching `checkUniqueRootIds`'s pre-D-V6 message shape) rather than a
394
+ // raw multi-issue dump, because each one already reads as a finished
395
+ // sentence naming the offending ids. Every other schema violation keeps
396
+ // propagating as the raw `ZodError` it always has (unchanged behavior,
397
+ // e.g. the iframe-reachable field-exclusivity / manifestVersion checks).
398
+ const compositionIssue = parsed.error.issues.find(
399
+ (issue) =>
400
+ issue.message.startsWith('Game manifest: duplicate root id') ||
401
+ issue.message.startsWith('Game manifest: a project has at most one'),
413
402
  );
414
- if (dupIssue) throw new Error(dupIssue.message);
403
+ if (compositionIssue) throw new Error(compositionIssue.message);
415
404
  throw parsed.error;
416
405
  }
417
406
  const manifest: GameManifest = parsed.data;
@@ -437,7 +426,6 @@ export function loadGameManifest(raw: unknown): ResolvedGameManifest {
437
426
  manifestVersion: manifest.manifestVersion,
438
427
  name: manifest.name,
439
428
  appId: manifest.appId,
440
- editorPort: manifest.editorPort,
441
429
  version: manifest.version,
442
430
  engine: { version: manifest.engine.version },
443
431
  roots,
@@ -0,0 +1,55 @@
1
+ // Finding the project manifest ON DISK. There is ONE filename — see
2
+ // `./filename.ts` for why the old one was removed rather than kept readable.
3
+ //
4
+ // Deliberately dependency-light — `node:fs` + `node:path` + `./filename` and
5
+ // NOTHING else (no Zod, no `./load`). Several callers are explicitly
6
+ // schema-free by design (`packages/editor/server/project-root-surface.ts`,
7
+ // `packages/editor/vite-plugin-ui-oid.ts`) and must be able to locate a
8
+ // manifest without dragging the validator in.
9
+
10
+ import { existsSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import {
13
+ MANIFEST_FILENAME,
14
+ REMOVED_MANIFEST_FILENAME,
15
+ removedManifestFilenameMessage,
16
+ } from './filename';
17
+
18
+ /**
19
+ * Throw the rename instruction if `dir` carries only the REMOVED manifest
20
+ * filename. The `assertNoRemoved*` shape: a removed thing must reject loudly
21
+ * naming the exact fix, never be silently ignored (which would present as
22
+ * "this folder is not a project" — a diagnosis pointing nowhere).
23
+ *
24
+ * Returns normally when `dir` holds the current manifest, or neither file.
25
+ */
26
+ export function assertNoRemovedManifestFilename(dir: string): void {
27
+ if (existsSync(join(dir, MANIFEST_FILENAME))) return;
28
+ if (!existsSync(join(dir, REMOVED_MANIFEST_FILENAME))) return;
29
+ throw new Error(removedManifestFilenameMessage(join(dir, REMOVED_MANIFEST_FILENAME)));
30
+ }
31
+
32
+ /**
33
+ * The manifest path inside `dir`. Always `vgai.project.json` — so a "not
34
+ * found" message names the filename a project must be using — except that a
35
+ * directory carrying only the removed `vgai.game.json` THROWS the rename
36
+ * instruction instead of reporting a phantom path.
37
+ *
38
+ * This is the drop-in replacement for a hand-written `join(dir, …)` everywhere
39
+ * a manifest is READ. Writers want {@link manifestWritePath}.
40
+ */
41
+ export function resolveManifestPath(dir: string): string {
42
+ assertNoRemovedManifestFilename(dir);
43
+ return join(dir, MANIFEST_FILENAME);
44
+ }
45
+
46
+ /** True when `dir` holds a manifest. A lone `vgai.game.json` throws. */
47
+ export function hasManifest(dir: string): boolean {
48
+ assertNoRemovedManifestFilename(dir);
49
+ return existsSync(join(dir, MANIFEST_FILENAME));
50
+ }
51
+
52
+ /** Where a manifest gets WRITTEN in `dir`. There is one answer. */
53
+ export function manifestWritePath(dir: string): string {
54
+ return join(dir, MANIFEST_FILENAME);
55
+ }