@vgai/engine 0.5.13 → 0.5.15

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 (192) hide show
  1. package/dist/adapter/adapter-module.d.ts +447 -0
  2. package/dist/adapter/adapter-module.d.ts.map +1 -0
  3. package/dist/adapter/adapter-module.js +308 -0
  4. package/dist/adapter/authoring.d.ts +357 -10
  5. package/dist/adapter/authoring.d.ts.map +1 -1
  6. package/dist/adapter/authoring.js +87 -1
  7. package/dist/adapter/constraint.d.ts +9 -3
  8. package/dist/adapter/constraint.d.ts.map +1 -1
  9. package/dist/adapter/finders/finder-result.d.ts +28 -0
  10. package/dist/adapter/finders/finder-result.d.ts.map +1 -0
  11. package/dist/adapter/finders/finder-result.js +10 -0
  12. package/dist/adapter/finders/index.d.ts +38 -0
  13. package/dist/adapter/finders/index.d.ts.map +1 -0
  14. package/dist/adapter/finders/index.js +41 -0
  15. package/dist/adapter/finders/prefabs-from-stories.d.ts +64 -0
  16. package/dist/adapter/finders/prefabs-from-stories.d.ts.map +1 -0
  17. package/dist/adapter/finders/prefabs-from-stories.js +92 -0
  18. package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts +67 -0
  19. package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -0
  20. package/dist/adapter/finders/scenes-from-entrypoint-selection.js +226 -0
  21. package/dist/adapter/first-party-systems.d.ts.map +1 -1
  22. package/dist/adapter/first-party-systems.js +29 -0
  23. package/dist/adapter/index.d.ts +7 -3
  24. package/dist/adapter/index.d.ts.map +1 -1
  25. package/dist/adapter/index.js +5 -2
  26. package/dist/adapter/ingest/contract-system-adapters.d.ts +24 -1
  27. package/dist/adapter/ingest/contract-system-adapters.d.ts.map +1 -1
  28. package/dist/adapter/ingest/contract-system-adapters.js +100 -34
  29. package/dist/adapter/ingest/game-contract.d.ts +188 -2
  30. package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
  31. package/dist/adapter/ingest/game-contract.js +107 -0
  32. package/dist/adapter/ingest/mount-readiness.d.ts +84 -0
  33. package/dist/adapter/ingest/mount-readiness.d.ts.map +1 -0
  34. package/dist/adapter/ingest/mount-readiness.js +81 -0
  35. package/dist/adapter/ingest/scene-capture.d.ts +91 -2
  36. package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
  37. package/dist/adapter/ingest/scene-capture.js +110 -18
  38. package/dist/adapter/ingest/structural-ids.d.ts +11 -5
  39. package/dist/adapter/ingest/structural-ids.d.ts.map +1 -1
  40. package/dist/adapter/ingest/structural-ids.js +18 -9
  41. package/dist/adapter/ingest/upstream-pin.d.ts +4 -2
  42. package/dist/adapter/ingest/upstream-pin.d.ts.map +1 -1
  43. package/dist/adapter/ingest/upstream-pin.js +4 -2
  44. package/dist/adapter/ingest/visible-capture-window.d.ts +116 -0
  45. package/dist/adapter/ingest/visible-capture-window.d.ts.map +1 -0
  46. package/dist/adapter/ingest/visible-capture-window.js +141 -0
  47. package/dist/adapter/loop-gate-report.d.ts +3 -4
  48. package/dist/adapter/loop-gate-report.d.ts.map +1 -1
  49. package/dist/adapter/loop-gate-report.js +3 -4
  50. package/dist/adapter/root-adapter.d.ts +22 -2
  51. package/dist/adapter/root-adapter.d.ts.map +1 -1
  52. package/dist/adapter/setup-three-root-adapter.d.ts.map +1 -1
  53. package/dist/adapter/setup-three-root-adapter.js +15 -0
  54. package/dist/adapter/system-adapter.d.ts +161 -2
  55. package/dist/adapter/system-adapter.d.ts.map +1 -1
  56. package/dist/adapter/system-adapter.js +18 -1
  57. package/dist/animation/camera-ownership.d.ts +4 -2
  58. package/dist/animation/camera-ownership.d.ts.map +1 -1
  59. package/dist/animation/camera-ownership.js +58 -1
  60. package/dist/animation/runtime-inspection.d.ts +70 -0
  61. package/dist/animation/runtime-inspection.d.ts.map +1 -0
  62. package/dist/animation/runtime-inspection.js +25 -0
  63. package/dist/animation/xstate-animation-binding.d.ts +9 -1
  64. package/dist/animation/xstate-animation-binding.d.ts.map +1 -1
  65. package/dist/animation/xstate-animation-binding.js +48 -1
  66. package/dist/asset-formats/material.d.ts +1 -1
  67. package/dist/asset-formats/mesh.d.ts +1 -1
  68. package/dist/asset-formats/particles.d.ts +13 -0
  69. package/dist/asset-formats/particles.d.ts.map +1 -1
  70. package/dist/asset-formats/particles.js +28 -0
  71. package/dist/behavior/xstate-inspection.d.ts +24 -0
  72. package/dist/behavior/xstate-inspection.d.ts.map +1 -0
  73. package/dist/behavior/xstate-inspection.js +25 -0
  74. package/dist/core/game-loop.d.ts +7 -5
  75. package/dist/core/game-loop.d.ts.map +1 -1
  76. package/dist/core/game-loop.js +39 -18
  77. package/dist/core/types.d.ts +11 -12
  78. package/dist/core/types.d.ts.map +1 -1
  79. package/dist/data/data-asset.js +1 -1
  80. package/dist/dev/instruments.d.ts.map +1 -1
  81. package/dist/dev/instruments.js +5 -1
  82. package/dist/dev/render-debug-adapter.d.ts +19 -2
  83. package/dist/dev/render-debug-adapter.d.ts.map +1 -1
  84. package/dist/dev/render-debug-adapter.js +10 -0
  85. package/dist/ecs/user-data.d.ts +10 -2
  86. package/dist/ecs/user-data.d.ts.map +1 -1
  87. package/dist/ecs/user-data.js +6 -2
  88. package/dist/input/binding-identity.d.ts +10 -0
  89. package/dist/input/binding-identity.d.ts.map +1 -0
  90. package/dist/input/binding-identity.js +36 -0
  91. package/dist/input/input-manager.d.ts +18 -185
  92. package/dist/input/input-manager.d.ts.map +1 -1
  93. package/dist/input/input-manager.js +78 -68
  94. package/dist/manifest/load.d.ts +14 -0
  95. package/dist/manifest/load.d.ts.map +1 -1
  96. package/dist/manifest/load.js +6 -1
  97. package/dist/manifest/schema.d.ts +18 -0
  98. package/dist/manifest/schema.d.ts.map +1 -1
  99. package/dist/manifest/schema.js +65 -8
  100. package/dist/pixi/authoring.d.ts +27 -0
  101. package/dist/pixi/authoring.d.ts.map +1 -1
  102. package/dist/pixi/authoring.js +10 -3
  103. package/dist/pixi/index.d.ts +1 -1
  104. package/dist/pixi/index.d.ts.map +1 -1
  105. package/dist/pixi/index.js +1 -1
  106. package/dist/pixi/ingest.d.ts +8 -0
  107. package/dist/pixi/ingest.d.ts.map +1 -1
  108. package/dist/pixi/ingest.js +10 -1
  109. package/dist/pixi/render-pass-bracket.d.ts +56 -0
  110. package/dist/pixi/render-pass-bracket.d.ts.map +1 -0
  111. package/dist/pixi/render-pass-bracket.js +98 -0
  112. package/dist/pixi/scene-capture.d.ts +32 -1
  113. package/dist/pixi/scene-capture.d.ts.map +1 -1
  114. package/dist/pixi/scene-capture.js +21 -9
  115. package/dist/pixi/system-adapters.d.ts +29 -0
  116. package/dist/pixi/system-adapters.d.ts.map +1 -1
  117. package/dist/pixi/system-adapters.js +27 -0
  118. package/dist/render/particles-factory.d.ts +16 -2
  119. package/dist/render/particles-factory.d.ts.map +1 -1
  120. package/dist/render/particles-factory.js +88 -5
  121. package/dist/render/soft-particle-depth.d.ts +149 -0
  122. package/dist/render/soft-particle-depth.d.ts.map +1 -0
  123. package/dist/render/soft-particle-depth.js +305 -0
  124. package/dist/render/voxel-surface.d.ts +117 -0
  125. package/dist/render/voxel-surface.d.ts.map +1 -0
  126. package/dist/render/voxel-surface.js +263 -0
  127. package/dist/runtime/create-runtime.js +7 -0
  128. package/dist/runtime/debug-bridge.d.ts +15 -15
  129. package/dist/runtime/debug-bridge.d.ts.map +1 -1
  130. package/dist/runtime/debug-bridge.js +27 -26
  131. package/dist/runtime/game.d.ts.map +1 -1
  132. package/dist/runtime/game.js +1 -2
  133. package/dist/setup/setup-particles.d.ts.map +1 -1
  134. package/dist/setup/setup-particles.js +9 -0
  135. package/dist/world3d-react/r3f-adapter.d.ts.map +1 -1
  136. package/dist/world3d-react/r3f-adapter.js +42 -3
  137. package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
  138. package/dist/world3d-react/rapier-physics-bridge.js +201 -11
  139. package/dist-config/config.js.map +1 -1
  140. package/package.json +4 -2
  141. package/schemas/engine-capabilities.json +12 -12
  142. package/schemas/vgai-project.schema.json +42 -3
  143. package/src/adapter/adapter-module.ts +607 -0
  144. package/src/adapter/authoring.ts +440 -10
  145. package/src/adapter/constraint.ts +10 -3
  146. package/src/adapter/finders/finder-result.ts +29 -0
  147. package/src/adapter/finders/index.ts +62 -0
  148. package/src/adapter/finders/prefabs-from-stories.ts +142 -0
  149. package/src/adapter/finders/scenes-from-entrypoint-selection.ts +304 -0
  150. package/src/adapter/first-party-systems.ts +30 -0
  151. package/src/adapter/index.ts +32 -1
  152. package/src/adapter/ingest/contract-system-adapters.ts +116 -32
  153. package/src/adapter/ingest/game-contract.ts +265 -2
  154. package/src/adapter/ingest/mount-readiness.ts +140 -0
  155. package/src/adapter/ingest/scene-capture.ts +199 -22
  156. package/src/adapter/ingest/structural-ids.ts +21 -9
  157. package/src/adapter/ingest/upstream-pin.ts +4 -2
  158. package/src/adapter/ingest/visible-capture-window.ts +213 -0
  159. package/src/adapter/loop-gate-report.ts +3 -4
  160. package/src/adapter/root-adapter.ts +22 -2
  161. package/src/adapter/setup-three-root-adapter.ts +16 -0
  162. package/src/adapter/system-adapter.ts +188 -2
  163. package/src/animation/camera-ownership.ts +68 -3
  164. package/src/animation/runtime-inspection.ts +104 -0
  165. package/src/animation/xstate-animation-binding.ts +53 -1
  166. package/src/asset-formats/particles.ts +31 -0
  167. package/src/behavior/xstate-inspection.ts +51 -0
  168. package/src/core/game-loop.ts +39 -17
  169. package/src/core/types.ts +11 -12
  170. package/src/data/data-asset.ts +1 -1
  171. package/src/dev/instruments.ts +5 -1
  172. package/src/dev/render-debug-adapter.ts +22 -2
  173. package/src/ecs/user-data.ts +10 -2
  174. package/src/input/binding-identity.ts +37 -0
  175. package/src/input/input-manager.ts +93 -68
  176. package/src/manifest/load.ts +23 -2
  177. package/src/manifest/schema.ts +77 -8
  178. package/src/pixi/authoring.ts +37 -5
  179. package/src/pixi/index.ts +2 -0
  180. package/src/pixi/ingest.ts +15 -2
  181. package/src/pixi/render-pass-bracket.ts +117 -0
  182. package/src/pixi/scene-capture.ts +63 -13
  183. package/src/pixi/system-adapters.ts +41 -0
  184. package/src/render/particles-factory.ts +89 -6
  185. package/src/render/soft-particle-depth.ts +336 -0
  186. package/src/render/voxel-surface.ts +358 -0
  187. package/src/runtime/create-runtime.ts +7 -0
  188. package/src/runtime/debug-bridge.ts +32 -31
  189. package/src/runtime/game.ts +1 -2
  190. package/src/setup/setup-particles.ts +9 -0
  191. package/src/world3d-react/r3f-adapter.tsx +48 -2
  192. package/src/world3d-react/rapier-physics-bridge.tsx +249 -11
@@ -0,0 +1,607 @@
1
+ /**
2
+ * `vgai.adapter.ts` — THE ADAPTER MODULE CONTRACT.
3
+ *
4
+ * ARCHITECTURE-CORE §The editor protocol, "The adapter is the game's own
5
+ * module": the editor is a universal CLIENT of a fixed protocol and every game
6
+ * supplies a SERVER — its adapter. That adapter is the game's OWN module,
7
+ * `vgai.adapter.ts`, sitting beside `vgai.project.json`, default-exporting
8
+ * {@link defineAdapter}'s result.
9
+ *
10
+ * It is CODE (bindings are expressions — a scene entry may carry a closure the
11
+ * host evaluates against the mounted game) with a CONFIG's discipline: the top
12
+ * level is a STATICALLY EVALUABLE BINDING TABLE, readable without booting the
13
+ * game. Concretely, that is the split this module enforces:
14
+ *
15
+ * - the TABLE SHAPE — regions, the scene table, observation declarations —
16
+ * is plain data, validated by {@link AdapterDefinitionSchema} the moment
17
+ * the module is evaluated. `.strict()` throughout: an unrecognized key is
18
+ * an error naming the key, never a silent pass-through (CLAUDE.md,
19
+ * "Unknown input must REJECT LOUDLY").
20
+ * - individual FIELDS may be closures, evaluated lazily against the mounted
21
+ * game (`ObservationDeclaration.answer`). A closure is never required to
22
+ * read the table.
23
+ *
24
+ * **Zero inference.** A binding is a DECLARATION or a finder SELECTION the
25
+ * adapter makes — never something the host sniffs out. Finder algorithms
26
+ * (scenes from an entrypoint selection, prefabs from story registrations, …)
27
+ * ship host-side under `adapter/finders/`, and this module names them by
28
+ * SELECTION only ({@link FinderSelection}). That is deliberate and load-bearing
29
+ * twice over:
30
+ * 1. the engine never runs a finder nobody selected — enforced mechanically
31
+ * by `packages/engine/test/finder-import-boundary.test.ts`, whose only
32
+ * sanctioned importer of the finder namespace is the editor's adapter
33
+ * loader;
34
+ * 2. this module therefore imports NOTHING from `adapter/finders/`, so a
35
+ * game's `vgai.adapter.ts` pulls no finder implementation (and no source
36
+ * parser) into its own bundle. The dependency runs one way: finders
37
+ * import their parameter types from HERE.
38
+ *
39
+ * The manifest keeps only what must be readable without evaluating any module
40
+ * — identity, roots, boot mode, `server.room`. Everything that binds behavior
41
+ * is the adapter's. This wave adds NO manifest field.
42
+ */
43
+
44
+ import { z } from 'zod';
45
+ import type { ResolvedAdapterRoot } from '../manifest/load';
46
+ import type { AdapterSurface } from './adapter-surface';
47
+ import { WRITE_ANCHOR_KINDS, type WriteAnchorKind } from './authoring';
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Regions
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /**
54
+ * The projector serving a surface — one per surface, host-owned
55
+ * (ARCHITECTURE-CORE: "Projection — nodes, stable identity, bounds, picking …
56
+ * one projector per surface"). Named, never imported: the adapter states WHICH
57
+ * library serves the region; the host binds the implementation.
58
+ */
59
+ export const PROJECTOR_NAMES = ['three', 'pixi', 'dom'] as const;
60
+ export type ProjectorName = (typeof PROJECTOR_NAMES)[number];
61
+
62
+ /**
63
+ * A region's WORLD BASIS — which axis points up, and where its ground sits.
64
+ *
65
+ * The editor needs a ground plane to place a dropped asset or a probe point on.
66
+ * That plane used to be the constant `y = 0` compiled into `editor-viewport.ts`,
67
+ * which is a guess about the GAME's convention: a Z-up world (the CAD/Blender
68
+ * idiom foreign R3F games routinely carry) got its drops silently projected onto
69
+ * the wrong plane. It is exactly the shape "zero inference" names — a fact the
70
+ * game's author can state, so the fix is a declaration slot rather than a
71
+ * smarter sniff (ARCHITECTURE-CORE §The editor protocol).
72
+ */
73
+ export interface AdapterRegionBasis {
74
+ /** World axis pointing away from the ground. */
75
+ readonly up: 'y' | 'z';
76
+ /** Ground offset along {@link up}, in world units. */
77
+ readonly groundHeight: number;
78
+ }
79
+
80
+ /**
81
+ * The NATIVE basis — what {@link regionsFromManifestRoots} emits and what a
82
+ * region that declares none is read as. +Y up, ground at 0: three.js' own
83
+ * convention, which every first-party root is authored in. Stated as a value so
84
+ * "the default" has one home instead of a copy at each reader.
85
+ */
86
+ export const NATIVE_REGION_BASIS: AdapterRegionBasis = Object.freeze({
87
+ up: 'y',
88
+ groundHeight: 0,
89
+ });
90
+
91
+ /**
92
+ * One matrix REGION: a root, its surface, and the library selections that
93
+ * serve it. Universality lives at region granularity — which libraries serve a
94
+ * surface is a per-surface ENGINE fact, so a new surface root in an existing
95
+ * game is one manifest line and one derived region, never new adapter code
96
+ * (that is exactly what {@link regionsFromManifestRoots} does).
97
+ */
98
+ export interface AdapterRegion {
99
+ /** The manifest root this region grades. */
100
+ readonly id: string;
101
+ /** What the host hands this root. */
102
+ readonly surface: AdapterSurface;
103
+ /**
104
+ * `true` = this region grades the game's own DEV LAYER (the manifest root's
105
+ * `dev: true`), not shipped game content.
106
+ *
107
+ * Carried here because a dev root occupies the same MEDIUM as the content
108
+ * root beside it — a dev GUI is a `dom` root next to the HUD's `dom` root —
109
+ * and anything that maps a subject to "the region on this surface" would
110
+ * otherwise be choosing between them by array order. A region consumer that
111
+ * means shipped content says so by skipping `dev` regions (the loader's
112
+ * `regionForSurface` does), rather than by guessing which one matched.
113
+ */
114
+ readonly dev: boolean;
115
+ /** Projection library, by name. */
116
+ readonly projector: ProjectorName;
117
+ /**
118
+ * Dialect writer, by name (`r3f`, `jsx`, `pixi-react`, …) — the truth
119
+ * family's source writer for this region. `null` states outright that the
120
+ * host writes no source here: the region's truth is the game's own code,
121
+ * which the host does not author (a module or ingest root).
122
+ */
123
+ readonly dialect: string | null;
124
+ /**
125
+ * Anchor kinds this region's writer serves, drawn from the compile-pinned
126
+ * {@link WRITE_ANCHOR_KINDS} vocabulary. A kind absent here is a red cell in
127
+ * the matrix, which is a work order — never a silently unsupported edit.
128
+ */
129
+ readonly anchors: readonly WriteAnchorKind[];
130
+ /**
131
+ * This region's world basis. Omitted = {@link NATIVE_REGION_BASIS}, which is
132
+ * a DECLARED default (the native adapter emits it outright), not a silent
133
+ * fallback — same shape as an absent `vgai.adapter.ts` meaning
134
+ * `nativeAdapter()`.
135
+ */
136
+ readonly basis?: AdapterRegionBasis;
137
+ /**
138
+ * Project-relative globs naming source files this region OWNS, for the
139
+ * ambiguous remainder that import reach cannot place.
140
+ *
141
+ * A file's surface/dialect is derived from WHICH REGION'S IMPORT CLOSURE
142
+ * reaches it — a load-bearing fact, because the region's manifest entry is
143
+ * what the host actually executes at mount (the game would break first if it
144
+ * lied). Reach through several regions of the SAME surface is unambiguous;
145
+ * reach through regions of DIFFERING surfaces, or through none at all, is
146
+ * genuinely undecidable from outside, and this is where the game's author
147
+ * states the answer instead of the host guessing it from the file's bytes.
148
+ *
149
+ * READER: `packages/editor/src/ui-source/file-region-resolver.ts`'s
150
+ * `resolveFileRegion`, consulted BEFORE reach — a declared include wins,
151
+ * because it is a first-party statement about this exact file. Every OID
152
+ * stamping tier, the HMR classifier, the R3F authoring diagnostics and
153
+ * Content's component grouping read that one resolver.
154
+ *
155
+ * {@link regionsFromManifestRoots} emits none: the native default is PURE
156
+ * reach, and a first-party project that needs an include has a module its
157
+ * roots do not reach, which is a fact worth stating rather than absorbing.
158
+ *
159
+ * Glob vocabulary is deliberately the small one every reader already knows:
160
+ * `**` (any depth, including none), `*` (one segment, no `/`), `?` (one
161
+ * character). No brace expansion, no negation — a second syntax to learn is
162
+ * how a declaration slot turns back into a language.
163
+ */
164
+ readonly include?: readonly string[];
165
+ }
166
+
167
+ const AdapterRegionBasisSchema = z
168
+ .object({
169
+ up: z.enum(['y', 'z']).describe('World axis pointing away from the ground'),
170
+ groundHeight: z.number().describe('Ground offset along `up`, in world units'),
171
+ })
172
+ .strict();
173
+
174
+ const AdapterRegionSchema = z
175
+ .object({
176
+ id: z.string().min(1).describe('Manifest root id this region grades'),
177
+ surface: z.enum(['three', 'canvas', 'dom']).describe('Render surface the host hands this root'),
178
+ dev: z
179
+ .boolean()
180
+ .describe(
181
+ "true = this region grades the game's own dev layer, not shipped content (the " +
182
+ "manifest root's `dev: true`)",
183
+ ),
184
+ projector: z.enum(PROJECTOR_NAMES).describe('Projection library serving this surface, by name'),
185
+ dialect: z
186
+ .string()
187
+ .min(1)
188
+ .nullable()
189
+ .describe(
190
+ 'Dialect writer serving this region, by name; null states outright that the host writes ' +
191
+ "no source here (the region's truth is the game's own code)",
192
+ ),
193
+ anchors: z
194
+ .array(z.enum(WRITE_ANCHOR_KINDS as [WriteAnchorKind, ...WriteAnchorKind[]]))
195
+ .describe('Anchor kinds this region’s writer serves'),
196
+ basis: AdapterRegionBasisSchema.optional().describe(
197
+ 'World basis (up axis + ground height); omit for the native +Y / 0 default',
198
+ ),
199
+ include: z
200
+ .array(z.string().min(1))
201
+ .min(1)
202
+ .optional()
203
+ .describe(
204
+ 'Project-relative globs (`**`, `*`, `?`) naming source files this region owns, for files ' +
205
+ 'no root entry’s import closure reaches; a declared include wins over reach',
206
+ ),
207
+ })
208
+ .strict();
209
+
210
+ /**
211
+ * How a definition supplies its regions: the RULE `'manifest-roots'` (the
212
+ * native default — a declaration may be a rule rather than a list, the way
213
+ * `roots[]` never enumerates a world's contents), or an explicit list a game
214
+ * with novel bindings states outright.
215
+ */
216
+ export type RegionBinding = 'manifest-roots' | readonly AdapterRegion[];
217
+
218
+ const RegionBindingSchema = z.union([
219
+ z.literal('manifest-roots'),
220
+ z.array(AdapterRegionSchema).min(1),
221
+ ]);
222
+
223
+ /**
224
+ * The MECHANICAL derivation behind the `'manifest-roots'` rule: one region per
225
+ * declared root, its surface and adapter identity read straight off the
226
+ * RESOLVED manifest (`@engine/manifest/load` already decided both — this never
227
+ * re-derives them).
228
+ *
229
+ * The per-surface library table below is an ENGINE fact, stated once:
230
+ *
231
+ * | root `adapter` | projector | dialect | anchors |
232
+ * |-----------------------|-----------|---------------|------------------------------------|
233
+ * | `three` | `three` | `r3f` | source-prop, construction-literal |
234
+ * | `canvas` | `pixi` | `pixi-react` | source-prop, construction-literal |
235
+ * | `dom` | `dom` | `jsx` | source-prop |
236
+ * | `{module,surface}` | (surface) | `null` | live-only |
237
+ * | `{ingest,surface}` | (surface) | `null` | live-only |
238
+ *
239
+ * A module/ingest root's truth is the game's own source, which no first-party
240
+ * dialect writer authors — so it declares `live-only` rather than implying a
241
+ * write path it does not have. That is a red cell and therefore a work order,
242
+ * which is the point of stating it.
243
+ *
244
+ * Every derived region carries {@link NATIVE_REGION_BASIS} outright, so the
245
+ * editor's ground plane READS a declaration on the native path too — the
246
+ * default is published, never assumed at the read site.
247
+ */
248
+ export function regionsFromManifestRoots(roots: readonly ResolvedAdapterRoot[]): AdapterRegion[] {
249
+ return roots.map((root) => {
250
+ const surface = root.surface;
251
+ const dev = root.dev;
252
+ const firstParty = root.adapter.type === 'builtin';
253
+ const projector: ProjectorName = surface === 'canvas' ? 'pixi' : surface;
254
+ const basis = NATIVE_REGION_BASIS;
255
+ if (!firstParty) {
256
+ return {
257
+ id: root.id,
258
+ surface,
259
+ dev,
260
+ projector,
261
+ dialect: null,
262
+ anchors: ['live-only'] as const,
263
+ basis,
264
+ };
265
+ }
266
+ if (surface === 'dom') {
267
+ return {
268
+ id: root.id,
269
+ surface,
270
+ dev,
271
+ projector,
272
+ dialect: 'jsx',
273
+ anchors: ['source-prop'] as const,
274
+ basis,
275
+ };
276
+ }
277
+ return {
278
+ id: root.id,
279
+ surface,
280
+ dev,
281
+ projector,
282
+ dialect: surface === 'three' ? 'r3f' : 'pixi-react',
283
+ anchors: ['source-prop', 'construction-literal'] as const,
284
+ basis,
285
+ };
286
+ });
287
+ }
288
+
289
+ // ---------------------------------------------------------------------------
290
+ // Finder selections
291
+ // ---------------------------------------------------------------------------
292
+
293
+ /**
294
+ * The rule form of a finder's region targeting: every region the host mounts
295
+ * from an exported composition (a root with an `entry`) that is not a declared
296
+ * DEV layer. A dev root is the game's own dev GUI — the manifest already says
297
+ * so (`dev: true`), so skipping it reads a declaration rather than guessing.
298
+ */
299
+ export const EXPORTED_COMPOSITION_REGIONS = 'exported-composition-regions';
300
+
301
+ /**
302
+ * `scenesFromEntrypointSelection` — read the entrypoint's ACTIVE-SCENE
303
+ * SELECTION, the load-bearing reference (the game itself executes it, so it
304
+ * cannot drift: the game would break first).
305
+ */
306
+ export interface ScenesFromEntrypointSelectionParams {
307
+ readonly finder: 'scenesFromEntrypointSelection';
308
+ /**
309
+ * Which regions' entrypoints to read: named region ids, or the rule
310
+ * {@link EXPORTED_COMPOSITION_REGIONS}.
311
+ */
312
+ readonly regions: readonly string[] | typeof EXPORTED_COMPOSITION_REGIONS;
313
+ /**
314
+ * Identifier of the entrypoint's module-level selection table — the
315
+ * `const <selection> = { <sceneId>: <Component>, … }` binding the entrypoint
316
+ * renders at its swap slot.
317
+ *
318
+ * OMIT for an entrypoint that mounts ONE composition: the finder then answers
319
+ * with the single-scene degenerate table (the root's own composition, opened
320
+ * by mounting the region). Only legal alongside exactly one named region —
321
+ * a selection identifier is a fact about ONE entrypoint's source, so pairing
322
+ * it with the rule form is rejected by name rather than applied to whichever
323
+ * region happened to match.
324
+ */
325
+ readonly selection?: string;
326
+ }
327
+
328
+ /**
329
+ * `prefabsFromStories` — read the colocated portable-CSF story registrations.
330
+ * A prefab is an ordinary source component a designer independently places,
331
+ * declared by a story whose `meta.component` names it; the story registration
332
+ * is the load-bearing reference here (removing it removes the declaration).
333
+ * Zero parameters: the selection IS the whole configuration.
334
+ */
335
+ export interface PrefabsFromStoriesParams {
336
+ readonly finder: 'prefabsFromStories';
337
+ }
338
+
339
+ /** Every finder this engine ships, as the adapter's concrete SELECTION. */
340
+ export type FinderSelection = ScenesFromEntrypointSelectionParams | PrefabsFromStoriesParams;
341
+
342
+ /** The finder names, as a value — what the loader's dispatcher switches on. */
343
+ export const FINDER_NAMES = ['scenesFromEntrypointSelection', 'prefabsFromStories'] as const;
344
+ export type FinderName = (typeof FINDER_NAMES)[number];
345
+
346
+ const ScenesFromEntrypointSelectionSchema = z
347
+ .object({
348
+ finder: z.literal('scenesFromEntrypointSelection'),
349
+ regions: z
350
+ .union([z.array(z.string().min(1)).min(1), z.literal(EXPORTED_COMPOSITION_REGIONS)])
351
+ .describe('Region ids whose entrypoints carry the selection, or the rule form'),
352
+ selection: z
353
+ .string()
354
+ .min(1)
355
+ .optional()
356
+ .describe(
357
+ "Identifier of the entrypoint's module-level scene selection table; omit for a " +
358
+ 'single-composition entrypoint',
359
+ ),
360
+ })
361
+ .strict()
362
+ .refine((value) => value.selection === undefined || Array.isArray(value.regions), {
363
+ message:
364
+ 'scenesFromEntrypointSelection: `selection` names an identifier in ONE entrypoint’s ' +
365
+ `source, so it cannot be paired with the \`${EXPORTED_COMPOSITION_REGIONS}\` rule — ` +
366
+ 'name the region explicitly (`regions: ["<rootId>"]`).',
367
+ });
368
+
369
+ const PrefabsFromStoriesSchema = z.object({ finder: z.literal('prefabsFromStories') }).strict();
370
+
371
+ const FinderSelectionSchema = z.union([
372
+ ScenesFromEntrypointSelectionSchema,
373
+ PrefabsFromStoriesSchema,
374
+ ]);
375
+
376
+ // ---------------------------------------------------------------------------
377
+ // The scene table
378
+ // ---------------------------------------------------------------------------
379
+
380
+ /**
381
+ * How the RUNNING game is navigated to an entry — the `reachable` honesty
382
+ * clause, stated rather than assumed. An entry whose reach is `none` is a
383
+ * declared-but-unreachable scene: a red cell carrying its own reason.
384
+ */
385
+ export type SceneReach =
386
+ /** Mounting the region IS opening it — the single-composition degenerate case. */
387
+ | { readonly kind: 'root-mount' }
388
+ /** The entrypoint's own selection table reaches it under `key`. */
389
+ | { readonly kind: 'entrypoint-selection'; readonly selection: string; readonly key: string }
390
+ /** A portable story mounts it in isolation. */
391
+ | { readonly kind: 'story'; readonly storyId: string }
392
+ /** Declared, with no code path that opens it. */
393
+ | { readonly kind: 'none'; readonly reason: string };
394
+
395
+ const SceneReachSchema = z.union([
396
+ z.object({ kind: z.literal('root-mount') }).strict(),
397
+ z
398
+ .object({
399
+ kind: z.literal('entrypoint-selection'),
400
+ selection: z.string().min(1),
401
+ key: z.string().min(1),
402
+ })
403
+ .strict(),
404
+ z.object({ kind: z.literal('story'), storyId: z.string().min(1) }).strict(),
405
+ z.object({ kind: z.literal('none'), reason: z.string().min(1) }).strict(),
406
+ ]);
407
+
408
+ /** Where an entry's composition lives in the game's own source. */
409
+ export interface SceneSource {
410
+ /** Project-relative path. */
411
+ readonly path: string;
412
+ /** Named export; omit when the module default-exports it. */
413
+ readonly export?: string;
414
+ }
415
+
416
+ const SceneSourceSchema = z
417
+ .object({ path: z.string().min(1), export: z.string().min(1).optional() })
418
+ .strict();
419
+
420
+ /**
421
+ * One entry of the scene table. Scenes and prefabs are SIBLINGS here — both
422
+ * are registered compositions the editor opens in isolation through the same
423
+ * verb, differing only in instance site (ARCHITECTURE-CORE §Roots: "A scene is
424
+ * a ROLE, not a kind … the same kind of thing as a prefab").
425
+ */
426
+ export interface SceneEntry {
427
+ readonly id: string;
428
+ readonly label: string;
429
+ /** `scene` (swaps at the entrypoint's slot) or `prefab` (placed by a designer). */
430
+ readonly kind: 'scene' | 'prefab';
431
+ /** Owning region id; `null` for a project-scoped entry bound to no one root. */
432
+ readonly region: string | null;
433
+ /**
434
+ * `true` = a composition a designer authors (it earns a matrix column).
435
+ * `false` = TRAVERSAL: boot/loading choreography, which gets no column.
436
+ */
437
+ readonly authorable: boolean;
438
+ readonly reach: SceneReach;
439
+ readonly source?: SceneSource;
440
+ /** Which finder produced it; absent = the adapter stated it outright. */
441
+ readonly finder?: FinderName;
442
+ }
443
+
444
+ const SceneEntrySchema = z
445
+ .object({
446
+ id: z.string().min(1),
447
+ label: z.string().min(1),
448
+ kind: z.enum(['scene', 'prefab']),
449
+ region: z.string().min(1).nullable(),
450
+ authorable: z.boolean(),
451
+ reach: SceneReachSchema,
452
+ source: SceneSourceSchema.optional(),
453
+ finder: z.enum(FINDER_NAMES).optional(),
454
+ })
455
+ .strict();
456
+
457
+ /**
458
+ * The adapter's scene table: entries stated outright, finder selections the
459
+ * host runs to produce the rest, and the entry open by default.
460
+ */
461
+ export interface AdapterSceneTable {
462
+ readonly entries?: readonly SceneEntry[];
463
+ readonly find?: readonly FinderSelection[];
464
+ /**
465
+ * Id of the entry open by default. Omit to let the finder's own answer
466
+ * stand — the host never picks one on its own, so "no default" stays a fact
467
+ * rather than becoming whichever entry sorted first.
468
+ */
469
+ readonly default?: string;
470
+ }
471
+
472
+ const AdapterSceneTableSchema = z
473
+ .object({
474
+ entries: z.array(SceneEntrySchema).optional(),
475
+ find: z.array(FinderSelectionSchema).optional(),
476
+ default: z.string().min(1).optional(),
477
+ })
478
+ .strict();
479
+
480
+ // ---------------------------------------------------------------------------
481
+ // Observation
482
+ // ---------------------------------------------------------------------------
483
+
484
+ /**
485
+ * One statically declared observation slot. Declaring a slot and ANSWERING it
486
+ * live are separate facts (the two-state rule): a slot declared here whose
487
+ * `answer` throws or whose game never booted reads as broken, never as
488
+ * "the game has none".
489
+ */
490
+ export interface ObservationDeclaration {
491
+ readonly id: string;
492
+ readonly kind: 'system' | 'command' | 'state';
493
+ /**
494
+ * Lazily evaluated against the mounted game. The field may be a closure; the
495
+ * TABLE around it stays statically readable, which is the whole discipline.
496
+ */
497
+ readonly answer?: (game: unknown) => unknown;
498
+ }
499
+
500
+ const ObservationDeclarationSchema = z
501
+ .object({
502
+ id: z.string().min(1),
503
+ kind: z.enum(['system', 'command', 'state']),
504
+ answer: z
505
+ .custom<(game: unknown) => unknown>((value) => typeof value === 'function', {
506
+ message: 'observation.answer must be a function evaluated against the mounted game',
507
+ })
508
+ .optional(),
509
+ })
510
+ .strict();
511
+
512
+ // ---------------------------------------------------------------------------
513
+ // The definition
514
+ // ---------------------------------------------------------------------------
515
+
516
+ /** The adapter module's default export, after {@link defineAdapter}. */
517
+ export interface AdapterDefinition {
518
+ readonly regions: RegionBinding;
519
+ readonly scenes: AdapterSceneTable;
520
+ readonly observation: readonly ObservationDeclaration[];
521
+ }
522
+
523
+ /** What a game writes. Every field optional — the near-empty adapter is the
524
+ * first-party case, and an adapter's SIZE measures the game's distance from
525
+ * native. */
526
+ export interface AdapterDefinitionInput {
527
+ readonly regions?: RegionBinding;
528
+ readonly scenes?: AdapterSceneTable;
529
+ readonly observation?: readonly ObservationDeclaration[];
530
+ }
531
+
532
+ export const AdapterDefinitionSchema = z
533
+ .object({
534
+ regions: RegionBindingSchema.optional(),
535
+ scenes: AdapterSceneTableSchema.optional(),
536
+ observation: z.array(ObservationDeclarationSchema).optional(),
537
+ })
538
+ .strict();
539
+
540
+ /**
541
+ * Validate and freeze a game's binding table.
542
+ *
543
+ * Called TWICE by design and with one schema: once here, in the game's own
544
+ * module, so a malformed table fails at the author's own file with the key
545
+ * named; and once in the host's loader, because the loaded default export is
546
+ * untrusted input crossing a module boundary. Two call sites, one rule — not
547
+ * two ways to say the same thing.
548
+ */
549
+ export function defineAdapter(input: AdapterDefinitionInput = {}): AdapterDefinition {
550
+ const parsed = AdapterDefinitionSchema.safeParse(input);
551
+ if (!parsed.success) {
552
+ throw new Error(
553
+ `vgai.adapter.ts: invalid adapter definition — ${parsed.error.issues
554
+ .map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
555
+ .join('; ')}`,
556
+ );
557
+ }
558
+ return Object.freeze({
559
+ regions: input.regions ?? 'manifest-roots',
560
+ scenes: Object.freeze({ ...(input.scenes ?? {}) }),
561
+ observation: Object.freeze([...(input.observation ?? [])]),
562
+ });
563
+ }
564
+
565
+ /** Extra bindings a near-native game adds without restating the native ones. */
566
+ export interface NativeAdapterOptions {
567
+ /** Scene-table entries this game states outright, beside what the finders find. */
568
+ readonly scenes?: readonly SceneEntry[];
569
+ /** Observation slots this game declares. */
570
+ readonly observation?: readonly ObservationDeclaration[];
571
+ }
572
+
573
+ /**
574
+ * THE NATIVE DEFAULT — regions derived mechanically from the manifest's
575
+ * `roots[]`, and a scene table produced by the two shipped finders. This is
576
+ * what a first-party project ships (`export default nativeAdapter()`), and it
577
+ * is also what a project with NO `vgai.adapter.ts` gets: that absence is the
578
+ * declared native default, not a silent fallback.
579
+ */
580
+ export function nativeAdapter(options: NativeAdapterOptions = {}): AdapterDefinition {
581
+ return defineAdapter({
582
+ regions: 'manifest-roots',
583
+ scenes: {
584
+ ...(options.scenes ? { entries: options.scenes } : {}),
585
+ find: [
586
+ { finder: 'scenesFromEntrypointSelection', regions: EXPORTED_COMPOSITION_REGIONS },
587
+ { finder: 'prefabsFromStories' },
588
+ ],
589
+ },
590
+ ...(options.observation ? { observation: options.observation } : {}),
591
+ });
592
+ }
593
+
594
+ /**
595
+ * Parse an untrusted default export (the loaded `vgai.adapter.ts`) into a
596
+ * definition. Rejects loudly — an unknown key, a bad finder name, a
597
+ * `selection` paired with the rule form all fail by name.
598
+ */
599
+ export function parseAdapterDefinition(value: unknown): AdapterDefinition {
600
+ if (value === null || typeof value !== 'object') {
601
+ throw new Error(
602
+ 'vgai.adapter.ts must default-export defineAdapter({…}) / nativeAdapter() — ' +
603
+ `got ${value === null ? 'null' : typeof value}`,
604
+ );
605
+ }
606
+ return defineAdapter(value as AdapterDefinitionInput);
607
+ }