@vgai/engine 0.5.13 → 0.5.14

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 +64 -7
  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 +41 -2
  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 +76 -7
  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
@@ -25,6 +25,20 @@
25
25
  * other direction: a wrong "present" claim is as much a defect as a wrong
26
26
  * "empty" one.
27
27
  *
28
+ * ## Why `physics` also gets a KEYING check
29
+ *
30
+ * The physics slot is the one whose vocabulary follows the SURFACE
31
+ * ({@link PHYSICS_KEYING_BY_SURFACE}): a three-surface game keys by host node
32
+ * id, a canvas-surface game by the display object it already holds. Both shapes
33
+ * carry the same four member names, so presence-and-typeof cannot tell them
34
+ * apart — a carrier keyed for the other surface would pass every check here and
35
+ * then be called with values it has none of, which is the "green coverage row
36
+ * bought with four methods that have no caller" defect this whole slot was left
37
+ * open to avoid. So a mount that KNOWS its surface says so, and a mismatch is
38
+ * `malformed` by name. A caller with no surface fact (`undefined`) does not
39
+ * check — it has nothing to check against, and inventing a default would be a
40
+ * guess wearing a verdict.
41
+ *
28
42
  * Nothing here calls a declared method. Presence and shape only.
29
43
  */
30
44
 
@@ -60,9 +74,30 @@ const REQUIRED_MEMBERS: {
60
74
  ],
61
75
  navigation: ['hasNavMesh', 'findPath', 'debugMesh'],
62
76
  audio: ['setMuted', 'isMuted'],
77
+ camera: ['snapshot', 'subscribe'],
63
78
  renderDebug: ['captureFrame'],
64
79
  };
65
80
 
81
+ /**
82
+ * The render surface a declaration is being read ON — the same three words
83
+ * `IngestKind`/`AdapterSurface` already use, so nothing has to be declared
84
+ * twice.
85
+ */
86
+ export type ContractSurface = 'three' | 'canvas' | 'dom';
87
+
88
+ /**
89
+ * THE surface→physics-vocabulary rule, stated once.
90
+ *
91
+ * `null` for `dom`: that surface has no physics consumer at all, so nothing
92
+ * here can say which keying would be right for it and the check stands down
93
+ * rather than inventing an answer.
94
+ */
95
+ const PHYSICS_KEYING_BY_SURFACE: Readonly<Record<ContractSurface, 'node-id' | 'display' | null>> = {
96
+ three: 'node-id',
97
+ canvas: 'display',
98
+ dom: null,
99
+ };
100
+
66
101
  /** Fixed slot order, so two reports of the same mount are diffable line for
67
102
  * line (the same rule the coverage report's row order follows). */
68
103
  export const CONTRACT_SYSTEM_SLOTS: readonly ContractSystemSlot[] = [
@@ -70,6 +105,7 @@ export const CONTRACT_SYSTEM_SLOTS: readonly ContractSystemSlot[] = [
70
105
  'networking',
71
106
  'navigation',
72
107
  'audio',
108
+ 'camera',
73
109
  'renderDebug',
74
110
  ];
75
111
 
@@ -115,14 +151,90 @@ function missingMembers(slot: ContractSystemSlot, value: object): string[] {
115
151
  return missing;
116
152
  }
117
153
 
154
+ /**
155
+ * The reason a declared physics carrier cannot serve `surface`, or `null` when
156
+ * it can (including when the caller supplied no surface to check against).
157
+ *
158
+ * The keying a carrier actually has is its tag's own answer, and an ABSENT tag
159
+ * IS `'node-id'` — see `PhysicsAdapter`'s comment for why the original
160
+ * vocabulary is the untagged one.
161
+ */
162
+ function physicsKeyingMismatch(value: object, surface: ContractSurface | undefined): string | null {
163
+ const want = surface ? PHYSICS_KEYING_BY_SURFACE[surface] : null;
164
+ if (want === null) return null;
165
+ const got = (value as { keyedBy?: unknown }).keyedBy === 'display' ? 'display' : 'node-id';
166
+ if (got === want) return null;
167
+ return (
168
+ `declared a ${got}-keyed physics carrier on the ${surface} surface, whose gizmo path ` +
169
+ `addresses bodies by ${want} — the four members are all present but nothing on this lane ` +
170
+ `can call them with values they accept (a ${surface}-surface carrier declares ` +
171
+ `\`keyedBy: '${want}'\`)`
172
+ );
173
+ }
174
+
175
+ /** One slot's verdict, before it is filed into the three lists. */
176
+ type SlotVerdict =
177
+ | { readonly kind: 'skip' }
178
+ | { readonly kind: 'bound' }
179
+ | { readonly kind: 'empty'; readonly evidence: string }
180
+ | { readonly kind: 'malformed'; readonly reason: string };
181
+
182
+ /**
183
+ * THE per-slot decision, in the order the module header states: is it declared
184
+ * at all, is it the positively-answered empty, does it carry the members its
185
+ * interface requires, and (physics only) is it keyed for this surface.
186
+ *
187
+ * Split out of {@link projectContractSystemAdapters} so the projection itself
188
+ * is a loop over verdicts — one place decides, one place files.
189
+ */
190
+ function classifyDeclaredSlot(
191
+ slot: ContractSystemSlot,
192
+ value: unknown,
193
+ surface: ContractSurface | undefined,
194
+ ): SlotVerdict {
195
+ if (value === undefined || value === null) return { kind: 'skip' };
196
+ if (typeof value !== 'object') {
197
+ return {
198
+ kind: 'malformed',
199
+ reason: `declared as ${typeof value}; expected an adapter object or { present: false, evidence }`,
200
+ };
201
+ }
202
+ if (isEmptyRecord(value)) {
203
+ const evidence = (value as VgaiGameSystemEmpty).evidence;
204
+ // An empty claim with no evidence is exactly the "wrong empty claim" the
205
+ // bar calls a defect — it asserts an absence nobody can re-check.
206
+ if (typeof evidence !== 'string' || evidence.trim() === '') {
207
+ return {
208
+ kind: 'malformed',
209
+ reason: 'declared `present: false` with no `evidence` string to back the absence',
210
+ };
211
+ }
212
+ return { kind: 'empty', evidence };
213
+ }
214
+ const missing = missingMembers(slot, value);
215
+ if (missing.length > 0) {
216
+ return {
217
+ kind: 'malformed',
218
+ reason: `declared an adapter missing required member(s): ${missing.join(', ')}`,
219
+ };
220
+ }
221
+ const mismatch = slot === 'physics' ? physicsKeyingMismatch(value, surface) : null;
222
+ if (mismatch !== null) return { kind: 'malformed', reason: mismatch };
223
+ return { kind: 'bound' };
224
+ }
225
+
118
226
  /**
119
227
  * Read the declared slots and sort each into exactly one verdict. A game that
120
228
  * declared no `systemAdapters` at all projects to three empty lists — which is
121
229
  * the "nobody answered" case, and is deliberately NOT the same as declaring
122
230
  * every slot empty.
231
+ *
232
+ * `surface` is the mount's own surface fact when it has one; see the module
233
+ * header for what it buys and why its absence checks nothing.
123
234
  */
124
235
  export function projectContractSystemAdapters(
125
236
  systems: VgaiGameSystems | undefined | null,
237
+ surface?: ContractSurface | undefined,
126
238
  ): ContractSystemAdapterProjection {
127
239
  const declared = systems?.systemAdapters;
128
240
  if (!declared) return EMPTY_PROJECTION;
@@ -132,38 +244,10 @@ export function projectContractSystemAdapters(
132
244
  const malformed: ContractSystemMalformedSlot[] = [];
133
245
 
134
246
  for (const slot of CONTRACT_SYSTEM_SLOTS) {
135
- const value: unknown = declared[slot];
136
- if (value === undefined || value === null) continue;
137
- if (typeof value !== 'object') {
138
- malformed.push({
139
- slot,
140
- reason: `declared as ${typeof value}; expected an adapter object or { present: false, evidence }`,
141
- });
142
- continue;
143
- }
144
- if (isEmptyRecord(value)) {
145
- const evidence = (value as VgaiGameSystemEmpty).evidence;
146
- if (typeof evidence !== 'string' || evidence.trim() === '') {
147
- // An empty claim with no evidence is exactly the "wrong empty claim"
148
- // the bar calls a defect — it asserts an absence nobody can re-check.
149
- malformed.push({
150
- slot,
151
- reason: 'declared `present: false` with no `evidence` string to back the absence',
152
- });
153
- continue;
154
- }
155
- empty.push({ slot, evidence });
156
- continue;
157
- }
158
- const missing = missingMembers(slot, value);
159
- if (missing.length > 0) {
160
- malformed.push({
161
- slot,
162
- reason: `declared an adapter missing required member(s): ${missing.join(', ')}`,
163
- });
164
- continue;
165
- }
166
- bound[slot] = value;
247
+ const verdict = classifyDeclaredSlot(slot, declared[slot], surface);
248
+ if (verdict.kind === 'bound') bound[slot] = declared[slot];
249
+ else if (verdict.kind === 'empty') empty.push({ slot, evidence: verdict.evidence });
250
+ else if (verdict.kind === 'malformed') malformed.push({ slot, reason: verdict.reason });
167
251
  }
168
252
 
169
253
  return {
@@ -21,9 +21,10 @@
21
21
 
22
22
  import type {
23
23
  AudioAdapter,
24
+ CameraAdapter,
24
25
  NavigationAdapter,
25
26
  NetworkingAdapter,
26
- PhysicsAdapter,
27
+ PhysicsCarrier,
27
28
  RenderDebugAdapter,
28
29
  } from '../system-adapter';
29
30
 
@@ -43,6 +44,45 @@ export interface VgaiGameLifecycle {
43
44
  resume?(): void;
44
45
  }
45
46
 
47
+ /** One of the game's own scenes, as {@link VgaiGameScenes} lists it. `id` is the
48
+ * game's OWN identifier for it — the string `current()` reports and `goTo()`
49
+ * takes — and `label` is what a person reads. */
50
+ export interface VgaiGameScene {
51
+ id: string;
52
+ label: string;
53
+ }
54
+
55
+ /**
56
+ * The game's SCENES: the whole-world states it swaps between — screens, levels,
57
+ * rooms — named by the game itself.
58
+ *
59
+ * A multi-screen game's screens ARE its scenes, and the editor already has a
60
+ * vocabulary for "the design-time states a world can be put into": stories.
61
+ * So this is projected onto the ordinary `StoriesProvider`
62
+ * (`adapter/authoring.ts`) by the host's canvas ingest mount
63
+ * (`packages/editor/src/authoring/contract-scenes-stories.ts`), WORLD-LEVEL —
64
+ * the same scope the react world adapter's stories have — so the inspector's
65
+ * story picker lists a game's screens and switching one runs the game's own
66
+ * navigation call. Nothing else is invented: there is no host-side scene model,
67
+ * no ordering rule, and no "no scene" state.
68
+ *
69
+ * Presence-only, like the rest of this contract. A game that declares nothing
70
+ * here reports no stories provider at all — a named absence in the coverage
71
+ * report, never an empty picker.
72
+ */
73
+ export interface VgaiGameScenes {
74
+ /** Every scene a player can be sent to right now, in the game's own order. */
75
+ list(): VgaiGameScene[];
76
+ /** The scene the game currently has up, by its own id; `null` when it has
77
+ * none yet (a game whose entry has not resolved, a boot screen the game does
78
+ * not name). */
79
+ current(): string | null;
80
+ /** Put the game in that scene the way the game's OWN navigation does. May be
81
+ * async — a scene commonly loads its own assets first — and the host awaits
82
+ * whatever it returns before reporting the switch. */
83
+ goTo(id: string): Promise<unknown> | unknown;
84
+ }
85
+
46
86
  /**
47
87
  * One invokable verb the game exposes. Deliberately the SAME listing shape a
48
88
  * first-party game gets from `ctx.debug.registerCommand`
@@ -172,13 +212,48 @@ export interface VgaiGameSystemEmpty {
172
212
  * for which wins.
173
213
  */
174
214
  export interface VgaiGameSystemAdapters {
175
- physics?: PhysicsAdapter | VgaiGameSystemEmpty;
215
+ /**
216
+ * In the SURFACE'S OWN vocabulary: a three-surface game declares the node-id
217
+ * keyed `PhysicsAdapter`, a canvas-surface game the display-object keyed
218
+ * `PhysicsAdapter2D` (`keyedBy: 'display'` — the tag is what makes the two
219
+ * tellable apart, and `contract-system-adapters.ts` reports a carrier keyed
220
+ * for the wrong surface as MALFORMED by name rather than binding a shape
221
+ * nothing on that lane can call).
222
+ */
223
+ physics?: PhysicsCarrier | VgaiGameSystemEmpty;
176
224
  networking?: NetworkingAdapter | VgaiGameSystemEmpty;
177
225
  navigation?: NavigationAdapter | VgaiGameSystemEmpty;
178
226
  audio?: AudioAdapter | VgaiGameSystemEmpty;
227
+ camera?: CameraAdapter | VgaiGameSystemEmpty;
179
228
  renderDebug?: RenderDebugAdapter | VgaiGameSystemEmpty;
180
229
  }
181
230
 
231
+ /**
232
+ * The game's own READINESS signal — "my world is built; what you see now is
233
+ * the game".
234
+ *
235
+ * A host-mounted (exported-composition) root needs none of this: the host runs
236
+ * the mount, so mount completion IS readiness and the host answers the question
237
+ * itself. A SELF-BOOTING game owns its own boot, and the host has no way to see
238
+ * the end of it — which is why every reader used to measure instead: poll the
239
+ * draw count, watch `scene.children.length` stop growing, wait a fixed window.
240
+ * Each of those is a guess at a fact the game's author can state, which is
241
+ * exactly the shape zero inference names (ARCHITECTURE-CORE §The editor
242
+ * protocol), so the fix is this declaration slot rather than a better poll.
243
+ *
244
+ * Either form is legal, because both are how games already write it: a promise
245
+ * the game resolves when its boot finishes, or a function returning one (a
246
+ * callback that resolves/returns when ready). The host normalizes both through
247
+ * {@link readGameReady} and awaits exactly once per mount.
248
+ *
249
+ * Declaring nothing is a supported, first-class answer: the measured waits
250
+ * remain as the documented fallback. What changes is that they stop being
251
+ * SILENT — a root with no declaration reports `source: 'measured'` wherever
252
+ * readiness is reported, so "nobody stated it" is a visible fact rather than
253
+ * an invisible default.
254
+ */
255
+ export type VgaiGameReady = Promise<unknown> | (() => Promise<unknown> | unknown);
256
+
182
257
  export interface VgaiGameContract {
183
258
  /** Bump only on breaking shape changes; additive endpoints keep version 1. */
184
259
  contractVersion: 1;
@@ -189,7 +264,43 @@ export interface VgaiGameContract {
189
264
  * verifies it actually contains the captured canvas before adopting.
190
265
  */
191
266
  root?: HTMLElement;
267
+ /**
268
+ * The canvas the game PRESENTS on — the one whose pixels are "what the game
269
+ * looks like".
270
+ *
271
+ * `root` above answers a different question (which element owns the game's
272
+ * whole DOM), and a game routinely has more than one canvas inside it: an
273
+ * offscreen buffer it composites from, a minimap, a 2D overlay. The host's
274
+ * standing answer was "the first `<canvas>` in DOM order", which is a guess
275
+ * about authoring order, and the game's author knows the real answer.
276
+ *
277
+ * Declaring nothing keeps the measured answer, reported as `measured` (see
278
+ * `packages/editor/src/presentation-surface.ts`, the one reader).
279
+ */
280
+ presentation?: HTMLCanvasElement;
281
+ /**
282
+ * The game's own WORLD — the `THREE.Scene` the editor should adopt.
283
+ *
284
+ * Without it the host adopts whichever scene renders first that it did not
285
+ * draw itself (`scene-capture.ts`), which is a good measured default and a
286
+ * permanent, silent commitment: a splash screen, a shadow pre-pass or a
287
+ * render-to-texture warm-up that draws one frame earlier is adopted as the
288
+ * game forever. Declaring this makes the choice the game's, not a race's.
289
+ *
290
+ * Typed `object` rather than `THREE.Scene` on purpose — this file is the
291
+ * contract's shape and imports no renderer library; the host checks
292
+ * structurally (`isScene`) in {@link readGameWorld}.
293
+ */
294
+ world?: object;
295
+ /** The game's own readiness signal — see {@link VgaiGameReady}. */
296
+ ready?: VgaiGameReady;
192
297
  lifecycle?: VgaiGameLifecycle;
298
+ /**
299
+ * The game's own scenes. Absent means "this game declared no scene surface" —
300
+ * the mount then supplies no stories provider at all, and the coverage report
301
+ * says so by name.
302
+ */
303
+ scenes?: VgaiGameScenes;
193
304
  /**
194
305
  * The game's own systems. Absent means "this game declared no system
195
306
  * surface" — every dependent editor surface then shows a named absence, not
@@ -217,3 +328,155 @@ export function readGameContract(
217
328
  if (!declared || declared.contractVersion !== 1) return null;
218
329
  return declared;
219
330
  }
331
+
332
+ /** What {@link readGameScenes} found. Both fields `null` ⇒ the game declared no
333
+ * scene surface at all, which is a different answer from declaring an unusable
334
+ * one (the same distinction `contract-system-adapters.ts` draws between an
335
+ * omitted slot and a malformed one). */
336
+ export interface GameScenesReading {
337
+ /** The callable surface, or `null` when nothing usable was declared. */
338
+ readonly scenes: VgaiGameScenes | null;
339
+ /** Why a DECLARED surface was refused, in the words the host prints. */
340
+ readonly malformed: string | null;
341
+ }
342
+
343
+ /** The endpoints a scene surface must carry: every one of them is called by the
344
+ * projection, so a declaration missing any cannot be honoured. */
345
+ const SCENES_REQUIRED_MEMBERS: readonly (keyof VgaiGameScenes)[] = ['list', 'current', 'goTo'];
346
+
347
+ /**
348
+ * Read the declared {@link VgaiGameScenes}, refusing a malformed one BY NAME.
349
+ *
350
+ * The shape check is the same honesty `projectContractSystemAdapters` applies to
351
+ * a declared system slot: a `scenes: {}` would otherwise register as a
352
+ * capability and light up a story picker over an object with no methods. Nothing
353
+ * here CALLS a declared endpoint — presence and type only.
354
+ */
355
+ export function readGameScenes(contract: VgaiGameContract | null | undefined): GameScenesReading {
356
+ const declared: unknown = contract?.scenes;
357
+ if (declared === undefined || declared === null) return { scenes: null, malformed: null };
358
+ if (typeof declared !== 'object') {
359
+ return {
360
+ scenes: null,
361
+ malformed: `declared as ${typeof declared}; expected an object with list(), current() and goTo()`,
362
+ };
363
+ }
364
+ const missing = SCENES_REQUIRED_MEMBERS.filter(
365
+ (member) => typeof (declared as Record<string, unknown>)[member] !== 'function',
366
+ );
367
+ if (missing.length > 0) {
368
+ return {
369
+ scenes: null,
370
+ malformed: `declared a scenes surface missing required member(s): ${missing.join(', ')}`,
371
+ };
372
+ }
373
+ return { scenes: declared as VgaiGameScenes, malformed: null };
374
+ }
375
+
376
+ /** What {@link readGameReady} found. `ready: null` with `malformed: null` ⇒ the
377
+ * game declared no readiness signal, which is a different answer from
378
+ * declaring an unusable one — the same distinction {@link readGameScenes}
379
+ * draws, and the reason the readiness facet can report `declared` vs
380
+ * `measured` honestly instead of collapsing both into "not ready yet". */
381
+ export interface GameReadyReading {
382
+ /** The normalized signal: awaiting it once resolves when the game says it is
383
+ * ready. `null` when nothing usable was declared. */
384
+ readonly ready: (() => Promise<unknown>) | null;
385
+ /** Why a DECLARED signal was refused, in the words the host prints. */
386
+ readonly malformed: string | null;
387
+ }
388
+
389
+ /**
390
+ * Read the declared {@link VgaiGameReady}, refusing a malformed one BY NAME.
391
+ *
392
+ * Both legal forms normalize to one call: a promise is wrapped, a function is
393
+ * invoked lazily (once, when the host asks — which in the ingest mount is only
394
+ * AFTER the first captured render, so a `ready` that itself triggers the boot
395
+ * would deadlock the capture window; games self-boot on this lane),
396
+ * and whatever the function returns is coerced through `Promise.resolve` —
397
+ * a synchronous return means "already ready", which is a real declaration.
398
+ *
399
+ * A rejected/throwing signal is NOT swallowed here: the host awaits it and its
400
+ * failure is what separates "crashed before ready" from "never became ready"
401
+ * in the mount-failure report (`mount-readiness.ts`).
402
+ */
403
+ export function readGameReady(contract: VgaiGameContract | null | undefined): GameReadyReading {
404
+ const declared: unknown = contract?.ready;
405
+ if (declared === undefined || declared === null) return { ready: null, malformed: null };
406
+ if (typeof declared === 'function') {
407
+ const fn = declared as () => unknown;
408
+ return { ready: () => Promise.resolve(fn()), malformed: null };
409
+ }
410
+ if (typeof (declared as { then?: unknown }).then === 'function') {
411
+ const thenable = declared as Promise<unknown>;
412
+ return { ready: () => Promise.resolve(thenable), malformed: null };
413
+ }
414
+ return {
415
+ ready: null,
416
+ malformed: `declared \`ready\` as ${typeof declared}; expected a promise or a function returning one`,
417
+ };
418
+ }
419
+
420
+ /** What {@link readGameWorld} found — same two-field honesty as
421
+ * {@link readGameScenes}. */
422
+ export interface GameWorldReading {
423
+ readonly world: object | null;
424
+ readonly malformed: string | null;
425
+ }
426
+
427
+ /**
428
+ * Read the declared {@link VgaiGameContract.world}, refusing a malformed one BY
429
+ * NAME.
430
+ *
431
+ * STRUCTURAL (`isScene === true`), for two reasons: this file imports no
432
+ * renderer library, and an `instanceof THREE.Scene` across the realm the game
433
+ * runs in is exactly the check that silently answers `false` for a good scene —
434
+ * turning a correct declaration into an unexplained fall-back to first-render.
435
+ */
436
+ export function readGameWorld(contract: VgaiGameContract | null | undefined): GameWorldReading {
437
+ const declared: unknown = contract?.world;
438
+ if (declared === undefined || declared === null) return { world: null, malformed: null };
439
+ if (typeof declared !== 'object' || (declared as { isScene?: unknown }).isScene !== true) {
440
+ return {
441
+ world: null,
442
+ malformed: `declared \`world\` as ${typeof declared}; expected the game's own THREE.Scene`,
443
+ };
444
+ }
445
+ return { world: declared as object, malformed: null };
446
+ }
447
+
448
+ /** What {@link readGamePresentation} found — same two-field honesty as
449
+ * {@link readGameScenes}. */
450
+ export interface GamePresentationReading {
451
+ readonly canvas: HTMLCanvasElement | null;
452
+ readonly malformed: string | null;
453
+ }
454
+
455
+ /**
456
+ * Read the declared {@link VgaiGameContract.presentation} canvas, refusing a
457
+ * malformed one BY NAME.
458
+ *
459
+ * The shape check is STRUCTURAL (`tagName === 'CANVAS'` plus a `getContext`),
460
+ * not `instanceof`: the contract is read out of the realm the game runs in, and
461
+ * a realm-crossing `instanceof` is exactly the check that silently answers
462
+ * `false` for a perfectly good element. A wrong declaration must be refused by
463
+ * name, never by accident.
464
+ */
465
+ export function readGamePresentation(
466
+ contract: VgaiGameContract | null | undefined,
467
+ ): GamePresentationReading {
468
+ const declared: unknown = contract?.presentation;
469
+ if (declared === undefined || declared === null) return { canvas: null, malformed: null };
470
+ const el = declared as { tagName?: unknown; getContext?: unknown };
471
+ if (typeof el !== 'object' || el.tagName !== 'CANVAS' || typeof el.getContext !== 'function') {
472
+ return {
473
+ canvas: null,
474
+ malformed: `declared \`presentation\` as ${
475
+ typeof declared === 'object'
476
+ ? String((el as { tagName?: unknown }).tagName ?? 'object')
477
+ : typeof declared
478
+ }; expected the game's own <canvas> element`,
479
+ };
480
+ }
481
+ return { canvas: declared as HTMLCanvasElement, malformed: null };
482
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * READINESS, declared vs measured — and the three sentences a failed mount is
3
+ * allowed to say (M29).
4
+ *
5
+ * ARCHITECTURE-CORE §The editor protocol, "Zero inference": the system answers
6
+ * a question by reading a DECLARATION, by walking ground truth, or by
7
+ * diagnosing declared-vs-measured drift — and a guess at a fact the game's
8
+ * author could have stated is a defect whose fix is a declaration slot. "Is
9
+ * this game ready?" was the guess: the host polled draw counts, watched a
10
+ * scene's child count stop growing, and waited fixed windows.
11
+ *
12
+ * The declaration is `window.vgaiGame.ready` ({@link VgaiGameReady} in
13
+ * `game-contract.ts`) for a self-booting game, and MOUNT COMPLETION for a
14
+ * host-mounted (exported-composition) root — the host runs that mount, so it
15
+ * answers the question without the game writing a line. The measured waits
16
+ * stay for everything else; what they stop being is silent, which is what
17
+ * {@link ReadinessSource} is for.
18
+ *
19
+ * M29's defect: a game that threw during its own async init reported as
20
+ * "rendered no capturable frame within timeout" — the wait's expiry, which is a
21
+ * CONSEQUENCE of the crash, presented as the blocker. It misdirected two sweep
22
+ * measurements. {@link describeMountFailure} is the fix: three different
23
+ * sentences pointing at three different blockers, chosen from facts the host
24
+ * already has (did the game declare readiness; did anything throw during its
25
+ * boot window).
26
+ */
27
+
28
+ /**
29
+ * WHERE a readiness answer came from.
30
+ *
31
+ * `declared` — the game (or, for a host-mounted root, the host's own completed
32
+ * mount) STATED it. `measured` — nobody stated it, so a host-side wait stood in.
33
+ * Both are legitimate; only one of them is a fact about the game, which is why
34
+ * every reporting surface carries this field instead of presenting the two as
35
+ * the same answer.
36
+ */
37
+ export type ReadinessSource = 'declared' | 'measured';
38
+
39
+ /**
40
+ * The three blockers a mount that never reached a live world can have. Carried
41
+ * on the report so a reader (status bar, `vgai status`, the Console line) can
42
+ * branch without re-parsing the sentence.
43
+ */
44
+ export type MountFailureKind =
45
+ /** Something threw during the game's own boot window. Fix that first. */
46
+ | 'crashed-before-ready'
47
+ /** The game DECLARED `ready`, yet no capturable render arrived and nothing
48
+ * was thrown. (The host awaits `ready` only after the first captured
49
+ * render, so whether it resolved was not observed — the sentence states
50
+ * exactly that.) */
51
+ | 'declared-ready-never-resolved'
52
+ /** No declaration at all: the measured fallback wait is what expired. */
53
+ | 'no-readiness-declaration';
54
+
55
+ export interface MountFailureDescription {
56
+ readonly kind: MountFailureKind;
57
+ /** The one sentence every surface prints. */
58
+ readonly message: string;
59
+ /** The errors thrown during the boot window, ATTACHED rather than left for
60
+ * the reader to correlate by timestamp in a console they may not have open.
61
+ * Empty for the two non-crash kinds. */
62
+ readonly pageErrors: readonly string[];
63
+ }
64
+
65
+ export interface MountFailureInput {
66
+ /** The game's manifest/root id, as every other report names it. */
67
+ readonly gameId: string;
68
+ /** The capture window that expired, in VISIBLE ms. */
69
+ readonly timeoutMs: number;
70
+ /** Where the readiness answer would have come from — see {@link ReadinessSource}. */
71
+ readonly readinessSource: ReadinessSource;
72
+ /** Errors observed during the game's boot window, oldest first. */
73
+ readonly pageErrors: readonly string[];
74
+ /** The underlying rejection's own text, kept verbatim at the end. */
75
+ readonly cause: string;
76
+ }
77
+
78
+ /** The clause every kind shares: hidden time was not charged to the game, so
79
+ * nobody reads this as a backgrounded-tab failure (it was, twice). */
80
+ const VISIBLE_TIME_CLAUSE =
81
+ 'Time the tab spent hidden was NOT counted against it, so this is not a backgrounded-tab ' +
82
+ 'failure.';
83
+
84
+ /**
85
+ * Which of the three sentences this failure gets.
86
+ *
87
+ * A CRASH wins over everything: if something threw during the boot window,
88
+ * every other reading is downstream of it, and the old single sentence's whole
89
+ * defect was reporting the downstream one. After that the split is the
90
+ * declaration itself — a game that stated `ready` and never resolved it has a
91
+ * different blocker (and a different owner) from a game that stated nothing and
92
+ * outlasted a host-side guess.
93
+ */
94
+ export function describeMountFailure(input: MountFailureInput): MountFailureDescription {
95
+ const { gameId, timeoutMs, readinessSource, pageErrors, cause } = input;
96
+ const budget = `${timeoutMs}ms of VISIBLE time`;
97
+
98
+ if (pageErrors.length > 0) {
99
+ const listed = pageErrors.map((error) => ` · ${error}`).join('\n');
100
+ return {
101
+ kind: 'crashed-before-ready',
102
+ pageErrors: [...pageErrors],
103
+ message:
104
+ `Ingest game "${gameId}" CRASHED BEFORE IT BECAME READY: ${pageErrors.length} error(s) ` +
105
+ `were thrown while it was booting, and the capture window (${budget}) then expired. ` +
106
+ 'The expiry is a CONSEQUENCE of the crash, not the blocker — fix these first:\n' +
107
+ `${listed}\n` +
108
+ `${VISIBLE_TIME_CLAUSE} Underlying wait: ${cause}`,
109
+ };
110
+ }
111
+
112
+ if (readinessSource === 'declared') {
113
+ return {
114
+ kind: 'declared-ready-never-resolved',
115
+ pageErrors: [],
116
+ message:
117
+ `Ingest game "${gameId}" DECLARED A READINESS SIGNAL AND STILL PRODUCED NO CAPTURABLE ` +
118
+ `RENDER: it stated \`window.vgaiGame.ready\`, but the capture window (${budget}) expired ` +
119
+ "before the editor's three saw a render, and nothing was thrown. (The host awaits " +
120
+ '`ready` only after the first captured render, so whether it resolved was not observed.) ' +
121
+ "Nothing is wrong with the host's wait — the likely blocker is whatever the game's own " +
122
+ 'boot is waiting on (an asset fetch, a socket, a user gesture the editor never makes), ' +
123
+ 'or a game that draws through a renderer the capture trap cannot see. ' +
124
+ `${VISIBLE_TIME_CLAUSE} Underlying wait: ${cause}`,
125
+ };
126
+ }
127
+
128
+ return {
129
+ kind: 'no-readiness-declaration',
130
+ pageErrors: [],
131
+ message:
132
+ `Ingest game "${gameId}" DECLARED NO READINESS SIGNAL, and the MEASURED fallback wait ` +
133
+ `expired: the editor's three saw no render within ${budget} and nothing was thrown, so ` +
134
+ 'the host has no way to tell a slow boot from a stopped one. Recovery: declare ' +
135
+ '`window.vgaiGame.ready` (a promise resolved when the game has built its world) so this ' +
136
+ 'question stops being measured — or, if the game genuinely needs longer on screen, raise ' +
137
+ `"captureTimeoutMs" in the root's ingest block. ${VISIBLE_TIME_CLAUSE} ` +
138
+ `Underlying wait: ${cause}`,
139
+ };
140
+ }