@volter/project 0.5.211 → 0.5.212
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.
- package/NOTICE +2 -2
- package/README.md +8 -15
- package/package.json +5 -26
- package/schemas/volter-project.schema.json +33 -145
- package/schemas/volter-settings.schema.json +11 -49
- package/src/adapter/adapter-module.ts +204 -465
- package/src/adapter/adapter-surface.ts +4 -32
- package/src/adapter/authoring-seam-contract.ts +7 -24
- package/src/adapter/authoring.ts +126 -265
- package/src/adapter/editor-looks.ts +96 -127
- package/src/adapter/finders/registry.ts +6 -21
- package/src/adapter/host-context.ts +17 -72
- package/src/adapter/index.ts +4 -77
- package/src/adapter/manifest-interpreter.ts +7 -50
- package/src/adapter/physics-adapter-2d.ts +4 -21
- package/src/adapter/root-adapter.ts +37 -111
- package/src/adapter/root-seam-contract.ts +3 -6
- package/src/adapter/root-stacking.ts +4 -46
- package/src/adapter/seam-evidence.ts +0 -16
- package/src/adapter/system-adapter.ts +24 -656
- package/src/adapter/system-seam-contract.ts +1 -91
- package/src/adapter/transform.ts +4 -3
- package/src/adapter/workspace-arrangement.ts +29 -49
- package/src/manifest/editor-port.ts +10 -36
- package/src/manifest/filename.ts +3 -42
- package/src/manifest/load-file.ts +9 -15
- package/src/manifest/load.ts +44 -179
- package/src/manifest/locate.ts +6 -29
- package/src/manifest/schema.ts +155 -427
- package/src/semver.ts +16 -0
- package/src/settings/keys.ts +9 -11
- package/src/settings/schema.ts +55 -44
- package/schemas/engine-api.json +0 -57
- package/schemas/engine-api.md +0 -22
- package/src/adapter/binding.ts +0 -445
- package/src/adapter/debug-registry-ref.ts +0 -19
- package/src/adapter/frame-capture.ts +0 -80
- package/src/adapter/ingest/contract-system-slots.ts +0 -28
- package/src/adapter/ingest/game-contract-seams.ts +0 -86
- package/src/adapter/ingest/game-contract.ts +0 -506
- package/src/adapter/ingest/mount-readiness.ts +0 -140
- package/src/adapter/loop-gate-report.ts +0 -53
- package/src/adapter/native-entry-surface.ts +0 -100
- package/src/adapter/render-audio.ts +0 -20
- package/src/adapter/render-memory.ts +0 -34
- package/src/adapter/renderer-config.ts +0 -84
- package/src/adapter/system-slot.ts +0 -301
- package/src/core/system-phase.ts +0 -52
- package/src/inspection.ts +0 -281
- package/src/manifest/configuration-kinds.ts +0 -241
- package/src/manifest/kind-modules.ts +0 -58
- package/src/manifest/runtime-environment.ts +0 -48
|
@@ -1,506 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The game→host contract: ONE pre-defined interface an external game may
|
|
3
|
-
* declare on `window.volterGame` to let the editor work with it like native
|
|
4
|
-
* content. Capabilities are by PRESENCE — every field is optional, and a game
|
|
5
|
-
* that declares nothing runs exactly as before (doctrine: adaptation enables
|
|
6
|
-
* editor features; it never bends a game around host internals). This file is
|
|
7
|
-
* the single source of truth for the contract's shape — host code reads it
|
|
8
|
-
* through {@link readGameContract} instead of ad-hoc `window` casts, so the
|
|
9
|
-
* spec and the implementation cannot drift apart.
|
|
10
|
-
*
|
|
11
|
-
* Relationship to the native engine: first-party content implements the SAME
|
|
12
|
-
* conceptual surface (mount root, lifecycle, loop gating) through
|
|
13
|
-
* `RootAdapter`/`MountedThreeRoot` — the native engine is the premade 100%
|
|
14
|
-
* implementation of this contract. An ingested game climbs the same ladder
|
|
15
|
-
* endpoint by endpoint: capture infers what it can (the scene), the game
|
|
16
|
-
* declares what inference can't reach (its DOM root, its session lifecycle).
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import type {
|
|
20
|
-
AudioAdapter,
|
|
21
|
-
CameraAdapter,
|
|
22
|
-
NavigationAdapter,
|
|
23
|
-
NetworkingAdapter,
|
|
24
|
-
PhysicsCarrier,
|
|
25
|
-
RenderDebugAdapter,
|
|
26
|
-
} from '../system-adapter';
|
|
27
|
-
import { GAME_SCENES_SHAPE } from './game-contract-seams';
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Session lifecycle endpoints. Declaring `start` means "I support cold
|
|
31
|
-
* mount": when the host sets `window.__volterMountCold` before the game's entry
|
|
32
|
-
* executes, the game may defer its session side-effects (backend connections,
|
|
33
|
-
* narrative, audio) and render a quiet, inspectable scene; the host calls
|
|
34
|
-
* `start()` — at most once per mount — when the user presses ▶. Declaring
|
|
35
|
-
* `pause`/`resume` means the game can genuinely freeze/unfreeze itself. They
|
|
36
|
-
* are one indivisible pair: the ingest adapter binds that pair as its mounted
|
|
37
|
-
* lifecycle control, or binds its outside-in loop gate when neither is
|
|
38
|
-
* declared. Host consumers see only the bound control; they never mix one
|
|
39
|
-
* declared half with a fallback half.
|
|
40
|
-
*/
|
|
41
|
-
export interface VolterGameLifecycle {
|
|
42
|
-
start?(): void;
|
|
43
|
-
pause?(): void;
|
|
44
|
-
resume?(): void;
|
|
45
|
-
/**
|
|
46
|
-
* END this session: unmount the game's root, stop its frame loop, release
|
|
47
|
-
* what its boot created. The host calls it FIRST at ingest teardown, before
|
|
48
|
-
* its own reclamation — the game's own way out always outranks the host's.
|
|
49
|
-
*
|
|
50
|
-
* Why this slot exists (measured on racing-game): a self-booting R3F game's
|
|
51
|
-
* fiber loop is not the host's to stop — the game never calls
|
|
52
|
-
* `renderer.setAnimationLoop` (the loop-gate's own warning says it cannot
|
|
53
|
-
* hold it) and fiber is a shared bare dependency the realm scheduler does
|
|
54
|
-
* not wrap — so after teardown the DEAD mount's `useFrame` kept ticking
|
|
55
|
-
* against React-nulled refs, one `TypeError` per frame (~490 per walk)
|
|
56
|
-
* until the page reloaded. Only the code that created the root can end it,
|
|
57
|
-
* and for a vendored game that code is the host-added entry shim, which
|
|
58
|
-
* publishes the unmount for the contract shim to declare here.
|
|
59
|
-
*
|
|
60
|
-
* A game that declares nothing keeps today's behavior — host-side
|
|
61
|
-
* reclamation only, with its loop's ungovernability remaining the
|
|
62
|
-
* loop-gate's named standing warning. A CANVAS ingest whose module-scope
|
|
63
|
-
* `Application` survives teardown BY DESIGN (the warm-remount contract —
|
|
64
|
-
* see `resolve-three.ts`'s `remountEpochs` doc) simply does not declare it.
|
|
65
|
-
*/
|
|
66
|
-
dispose?(): void;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** One of the game's own scenes, as {@link VolterGameScenes} lists it. `id` is the
|
|
70
|
-
* game's OWN identifier for it — the string `current()` reports and `goTo()`
|
|
71
|
-
* takes — and `label` is what a person reads. */
|
|
72
|
-
export interface VolterGameScene {
|
|
73
|
-
id: string;
|
|
74
|
-
label: string;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* The game's SCENES: the whole-world states it swaps between — screens, levels,
|
|
79
|
-
* rooms — named by the game itself.
|
|
80
|
-
*
|
|
81
|
-
* A multi-screen game's screens ARE its scenes, and the editor already has a
|
|
82
|
-
* vocabulary for "the design-time states a world can be put into": stories.
|
|
83
|
-
* So this is projected onto the ordinary `StoriesProvider`
|
|
84
|
-
* (`adapter/authoring.ts`) by the host's canvas ingest mount
|
|
85
|
-
* (`packages/editor-game/src/host/authoring/contract-scenes-stories.ts`), WORLD-LEVEL —
|
|
86
|
-
* the same scope the react world adapter's stories have — so the inspector's
|
|
87
|
-
* story picker lists a game's screens and switching one runs the game's own
|
|
88
|
-
* navigation call. Nothing else is invented: there is no host-side scene model,
|
|
89
|
-
* no ordering rule, and no "no scene" state.
|
|
90
|
-
*
|
|
91
|
-
* Presence-only, like the rest of this contract. A game that declares nothing
|
|
92
|
-
* here reports no stories provider at all — a named absence in the coverage
|
|
93
|
-
* report, never an empty picker.
|
|
94
|
-
*/
|
|
95
|
-
export interface VolterGameScenes {
|
|
96
|
-
/** Every scene a player can be sent to right now, in the game's own order. */
|
|
97
|
-
list(): VolterGameScene[];
|
|
98
|
-
/** The scene the game currently has up, by its own id; `null` when it has
|
|
99
|
-
* none yet (a game whose entry has not resolved, a boot screen the game does
|
|
100
|
-
* not name). */
|
|
101
|
-
current(): string | null;
|
|
102
|
-
/** Put the game in that scene the way the game's OWN navigation does. May be
|
|
103
|
-
* async — a scene commonly loads its own assets first — and the host awaits
|
|
104
|
-
* whatever it returns before reporting the switch. */
|
|
105
|
-
goTo(id: string): Promise<unknown> | unknown;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* One invokable verb the game exposes. Deliberately the SAME listing shape a
|
|
110
|
-
* first-party game gets from `ctx.debug.registerCommand`
|
|
111
|
-
* (`DebugCommandInfo`/`DebugAdapter.invoke` in `adapter/system-adapter.ts`) —
|
|
112
|
-
* an ingested game's verbs must enumerate through `game.commands()` and run
|
|
113
|
-
* through `game.command(...)` with no second vocabulary for agents to learn.
|
|
114
|
-
*
|
|
115
|
-
* `run` receives the positional argument list exactly as invoked. There is no
|
|
116
|
-
* host-side validation: the contract is plain JS declared beside a foreign
|
|
117
|
-
* game, so it cannot carry a Zod tuple the way `registerCommand` does.
|
|
118
|
-
* `argsJsonSchema` is therefore DOCUMENTATION for listing surfaces, not a
|
|
119
|
-
* gate — a verb validates its own arguments and throws its own errors.
|
|
120
|
-
*/
|
|
121
|
-
export interface VolterGameCommand {
|
|
122
|
-
name: string;
|
|
123
|
-
description?: string;
|
|
124
|
-
/** JSON-Schema projection of the argument list, when the game declares one. */
|
|
125
|
-
argsJsonSchema?: unknown;
|
|
126
|
-
run(...args: unknown[]): unknown | Promise<unknown>;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* One named, JSON-serializable state read — the `ctx.debug.registerStateProvider`
|
|
131
|
-
* shape. `tier` carries the same meaning as the first-party one: `observable`
|
|
132
|
-
* is a value the game already computes, `assisted` is one the shim derives.
|
|
133
|
-
*/
|
|
134
|
-
export interface VolterGameStateProvider {
|
|
135
|
-
name: string;
|
|
136
|
-
tier?: 'observable' | 'assisted';
|
|
137
|
-
read(): unknown;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* One top-level semantic hierarchy group from the game's OWN model. `count`
|
|
142
|
-
* is recomputed by the game when the host reads the hierarchy; it is not
|
|
143
|
-
* inferred from render objects. The editor presents these rows beside the
|
|
144
|
-
* captured render tree, so a system-driven game can say what its content is
|
|
145
|
-
* without pretending its scene graph is authored truth.
|
|
146
|
-
*/
|
|
147
|
-
export interface VolterGameHierarchyGroup {
|
|
148
|
-
id: string;
|
|
149
|
-
label: string;
|
|
150
|
-
count: number;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Synchronous read plus an optional game-owned invalidation signal. The
|
|
155
|
-
* editor never polls or guesses when a game's model changed; a dynamic game
|
|
156
|
-
* subscribes its ordinary authoring hierarchy to the same truth it reads.
|
|
157
|
-
*/
|
|
158
|
-
export interface VolterGameHierarchyProvider {
|
|
159
|
-
(): VolterGameHierarchyGroup[];
|
|
160
|
-
subscribe?(listener: () => void): () => void;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* The game's SYSTEM surface: what its content actually IS, as opposed to how
|
|
165
|
-
* it renders. An ingested game's content is not always its scene graph — in a
|
|
166
|
-
* system-driven game the authorable content lives in the game's own model and
|
|
167
|
-
* is reached through the game's own verbs, which `root`/`lifecycle` could not
|
|
168
|
-
* express.
|
|
169
|
-
*
|
|
170
|
-
* Every member is read by a concrete consumer: commands/state are projected
|
|
171
|
-
* by `adapter/ingest/contract-debug-adapter.ts` onto the host's ordinary
|
|
172
|
-
* `DebugAdapter` (`adapter/system-adapter.ts`), while `hierarchy` is projected
|
|
173
|
-
* by the editor onto the ordinary `AuthoringAdapter.hierarchy` surface. Thus
|
|
174
|
-
* `game.commands()`/`game.state()` reach an ingested game through the same
|
|
175
|
-
* bridge first-party content uses, with no second vocabulary for agents to
|
|
176
|
-
* learn. That is deliberate scope: this
|
|
177
|
-
* interface describes what a foreign game's entry shim may declare, so every
|
|
178
|
-
* member here is a promise the host must already keep. A member nothing reads
|
|
179
|
-
* would be a shim author implementing a hook that is never called.
|
|
180
|
-
*/
|
|
181
|
-
export interface VolterGameSystems {
|
|
182
|
-
/** Verbs, enumerated by `game.commands()` and run by `game.command(...)`. */
|
|
183
|
-
commands?: VolterGameCommand[];
|
|
184
|
-
/** State reads, enumerated by `game.providers()` and read by `game.state(...)`. */
|
|
185
|
-
state?: VolterGameStateProvider[];
|
|
186
|
-
/** Semantic hierarchy groups from the game's own model, recomputed on read. */
|
|
187
|
-
hierarchy?: VolterGameHierarchyProvider;
|
|
188
|
-
/**
|
|
189
|
-
* The game's own {@link SystemAdapters} slots — the ingest-realm equivalent
|
|
190
|
-
* of a first-party world's `ctx.registerSystemAdapter`. Read and projected by
|
|
191
|
-
* `contract-system-adapters.ts` onto the mounted root's `systems` bag, so an
|
|
192
|
-
* ingested game's physics/audio/navigation/networking reach the editor's
|
|
193
|
-
* ordinary panels through the SAME registry a native world publishes to.
|
|
194
|
-
*/
|
|
195
|
-
systemAdapters?: VolterGameSystemAdapters;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* A slot's IMPLEMENTED-EMPTY answer: "this game genuinely has no X", carried
|
|
200
|
-
* with the source-level evidence that establishes it.
|
|
201
|
-
*
|
|
202
|
-
* This exists because `SystemAdapters` is capability-by-PRESENCE, and presence
|
|
203
|
-
* has only two values while the ingestion bar (docs/BRINGING-AN-EXISTING-GAME.md
|
|
204
|
-
* §The ingestion checklist) has three inputs: bound, positively-absent, and
|
|
205
|
-
* not-yet-answered. Omitting a slot cannot tell the last two apart — an editor
|
|
206
|
-
* reading a bare absence cannot know whether anyone ever looked. Declaring this
|
|
207
|
-
* record says someone looked, says what they found, and turns a work-order row
|
|
208
|
-
* into a terminal one.
|
|
209
|
-
*
|
|
210
|
-
* It is deliberately NOT an adapter whose methods return zeros: a
|
|
211
|
-
* `getConnectionState()` of `'disconnected'` on a game with no transport at all
|
|
212
|
-
* is a fabrication (it implies a connection that could exist), which the
|
|
213
|
-
* anti-shim rule forbids. The absence is stated, never simulated.
|
|
214
|
-
*/
|
|
215
|
-
export interface VolterGameSystemEmpty {
|
|
216
|
-
/** Always `false` — the discriminant that separates this from an adapter. */
|
|
217
|
-
present: false;
|
|
218
|
-
/**
|
|
219
|
-
* Why the slot is empty, in SOURCE terms: what was searched and what was
|
|
220
|
-
* found. "no netcode anywhere in `src/`" is a finished answer; "not
|
|
221
|
-
* implemented yet" is not one, and a reviewer reading this field is meant to
|
|
222
|
-
* be able to re-run the search.
|
|
223
|
-
*/
|
|
224
|
-
evidence: string;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* The declarable `SystemAdapters` slots, each either a real implementation or a
|
|
229
|
-
* positively-answered {@link VolterGameSystemEmpty}.
|
|
230
|
-
*
|
|
231
|
-
* `debug` is deliberately NOT here: it is already the projection of
|
|
232
|
-
* `commands`/`state` above (`contract-debug-adapter.ts`), and a second door onto
|
|
233
|
-
* the same slot would let a game declare two different debug planes with no rule
|
|
234
|
-
* for which wins.
|
|
235
|
-
*/
|
|
236
|
-
export interface VolterGameSystemAdapters {
|
|
237
|
-
/**
|
|
238
|
-
* In the SURFACE'S OWN vocabulary: a three-surface game declares the node-id
|
|
239
|
-
* keyed `PhysicsAdapter`, a canvas-surface game the display-object keyed
|
|
240
|
-
* `PhysicsAdapter2D` (`keyedBy: 'display'` — the tag is what makes the two
|
|
241
|
-
* tellable apart, and `contract-system-adapters.ts` reports a carrier keyed
|
|
242
|
-
* for the wrong surface as MALFORMED by name rather than binding a shape
|
|
243
|
-
* nothing on that lane can call).
|
|
244
|
-
*/
|
|
245
|
-
physics?: PhysicsCarrier | VolterGameSystemEmpty;
|
|
246
|
-
networking?: NetworkingAdapter | VolterGameSystemEmpty;
|
|
247
|
-
navigation?: NavigationAdapter | VolterGameSystemEmpty;
|
|
248
|
-
audio?: AudioAdapter | VolterGameSystemEmpty;
|
|
249
|
-
camera?: CameraAdapter | VolterGameSystemEmpty;
|
|
250
|
-
renderDebug?: RenderDebugAdapter | VolterGameSystemEmpty;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* The game's own READINESS signal — "my world is built; what you see now is
|
|
255
|
-
* the game".
|
|
256
|
-
*
|
|
257
|
-
* A host-mounted (exported-composition) root needs none of this: the host runs
|
|
258
|
-
* the mount, so mount completion IS readiness and the host answers the question
|
|
259
|
-
* itself. A SELF-BOOTING game owns its own boot, and the host has no way to see
|
|
260
|
-
* the end of it — which is why every reader used to measure instead: poll the
|
|
261
|
-
* draw count, watch `scene.children.length` stop growing, wait a fixed window.
|
|
262
|
-
* Each of those is a guess at a fact the game's author can state, which is
|
|
263
|
-
* exactly the shape zero inference names (ARCHITECTURE-CORE §The editor
|
|
264
|
-
* protocol), so the fix is this declaration slot rather than a better poll.
|
|
265
|
-
*
|
|
266
|
-
* Either form is legal, because both are how games already write it: a promise
|
|
267
|
-
* the game resolves when its boot finishes, or a function returning one (a
|
|
268
|
-
* callback that resolves/returns when ready). The host normalizes both through
|
|
269
|
-
* {@link readGameReady} and awaits exactly once per mount.
|
|
270
|
-
*
|
|
271
|
-
* Declaring nothing is a supported, first-class answer: the measured waits
|
|
272
|
-
* remain as the documented fallback. What changes is that they stop being
|
|
273
|
-
* SILENT — a root with no declaration reports `source: 'measured'` wherever
|
|
274
|
-
* readiness is reported, so "nobody stated it" is a visible fact rather than
|
|
275
|
-
* an invisible default.
|
|
276
|
-
*/
|
|
277
|
-
export type VolterGameReady = Promise<unknown> | (() => Promise<unknown> | unknown);
|
|
278
|
-
|
|
279
|
-
export interface VolterGameContract {
|
|
280
|
-
/** Bump only on breaking shape changes; additive endpoints keep version 1. */
|
|
281
|
-
contractVersion: 1;
|
|
282
|
-
/**
|
|
283
|
-
* The element that OWNS the game's whole DOM (canvas + HUD + overlay
|
|
284
|
-
* portals). The host adopts it wholesale into the game pane, so DOM-hybrid
|
|
285
|
-
* games keep their UI instead of stranding it at page level. The host
|
|
286
|
-
* verifies it actually contains the captured canvas before adopting.
|
|
287
|
-
*/
|
|
288
|
-
root?: HTMLElement;
|
|
289
|
-
/**
|
|
290
|
-
* The canvas the game PRESENTS on — the one whose pixels are "what the game
|
|
291
|
-
* looks like".
|
|
292
|
-
*
|
|
293
|
-
* `root` above answers a different question (which element owns the game's
|
|
294
|
-
* whole DOM), and a game routinely has more than one canvas inside it: an
|
|
295
|
-
* offscreen buffer it composites from, a minimap, a 2D overlay. The host's
|
|
296
|
-
* standing answer was "the first `<canvas>` in DOM order", which is a guess
|
|
297
|
-
* about authoring order, and the game's author knows the real answer.
|
|
298
|
-
*
|
|
299
|
-
* Declaring nothing keeps the measured answer, reported as `measured` (see
|
|
300
|
-
* `packages/sdk/src/kit/presentation-surface.ts`, the one reader).
|
|
301
|
-
*/
|
|
302
|
-
presentation?: HTMLCanvasElement;
|
|
303
|
-
/**
|
|
304
|
-
* The game's own WORLD — the `THREE.Scene` the editor should adopt.
|
|
305
|
-
*
|
|
306
|
-
* Without it the host adopts whichever scene renders first that it did not
|
|
307
|
-
* draw itself (`scene-capture.ts`), which is a good measured default and a
|
|
308
|
-
* permanent, silent commitment: a splash screen, a shadow pre-pass or a
|
|
309
|
-
* render-to-texture warm-up that draws one frame earlier is adopted as the
|
|
310
|
-
* game forever. Declaring this makes the choice the game's, not a race's.
|
|
311
|
-
*
|
|
312
|
-
* Typed `object` rather than `THREE.Scene` on purpose — this file is the
|
|
313
|
-
* contract's shape and imports no renderer library; the host checks
|
|
314
|
-
* structurally (`isScene`) in {@link readGameWorld}.
|
|
315
|
-
*/
|
|
316
|
-
world?: object;
|
|
317
|
-
/** The game's own readiness signal — see {@link VolterGameReady}. */
|
|
318
|
-
ready?: VolterGameReady;
|
|
319
|
-
lifecycle?: VolterGameLifecycle;
|
|
320
|
-
/**
|
|
321
|
-
* The game's own scenes. Absent means "this game declared no scene surface" —
|
|
322
|
-
* the mount then supplies no stories provider at all, and the coverage report
|
|
323
|
-
* says so by name.
|
|
324
|
-
*/
|
|
325
|
-
scenes?: VolterGameScenes;
|
|
326
|
-
/**
|
|
327
|
-
* The game's own systems. Absent means "this game declared no system
|
|
328
|
-
* surface" — every dependent editor surface then shows a named absence, not
|
|
329
|
-
* an empty pretend-palette.
|
|
330
|
-
*/
|
|
331
|
-
systems?: VolterGameSystems;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Read the declared contract, if any. The `contractVersion` gate is the
|
|
336
|
-
* forward-compatibility hinge: a future v2 game on a v1 host is ignored
|
|
337
|
-
* (pre-contract fallbacks apply) rather than half-interpreted.
|
|
338
|
-
*
|
|
339
|
-
* `scope` is the REALM to read from, and defaults to this module's own
|
|
340
|
-
* `window` — the realm every ingest mount runs the game's modules in, where the
|
|
341
|
-
* game and the host share one global. It is a parameter at all because the
|
|
342
|
-
* contract is a property of the realm the game runs in, never of the realm that
|
|
343
|
-
* happens to be asking.
|
|
344
|
-
*/
|
|
345
|
-
export function readGameContract(
|
|
346
|
-
scope: Window | null | undefined = typeof window === 'undefined' ? null : window,
|
|
347
|
-
): VolterGameContract | null {
|
|
348
|
-
const declared = (scope as unknown as { volterGame?: VolterGameContract } | null | undefined)
|
|
349
|
-
?.volterGame;
|
|
350
|
-
if (!declared || declared.contractVersion !== 1) return null;
|
|
351
|
-
return declared;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/** What {@link readGameScenes} found. Both fields `null` ⇒ the game declared no
|
|
355
|
-
* scene surface at all, which is a different answer from declaring an unusable
|
|
356
|
-
* one (the same distinction `contract-system-adapters.ts` draws between an
|
|
357
|
-
* omitted slot and a malformed one). */
|
|
358
|
-
export interface GameScenesReading {
|
|
359
|
-
/** The callable surface, or `null` when nothing usable was declared. */
|
|
360
|
-
readonly scenes: VolterGameScenes | null;
|
|
361
|
-
/** Why a DECLARED surface was refused, in the words the host prints. */
|
|
362
|
-
readonly malformed: string | null;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/** The endpoints a scene surface must carry: every one of them is called by the
|
|
366
|
-
* projection, so a declaration missing any cannot be honoured. */
|
|
367
|
-
const SCENES_REQUIRED_MEMBERS = Object.entries(GAME_SCENES_SHAPE)
|
|
368
|
-
.filter(([, spec]) => !spec.optional)
|
|
369
|
-
.map(([member]) => member as keyof VolterGameScenes);
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Read the declared {@link VolterGameScenes}, refusing a malformed one BY NAME.
|
|
373
|
-
*
|
|
374
|
-
* The shape check is the same honesty `projectContractSystemAdapters` applies to
|
|
375
|
-
* a declared system slot: a `scenes: {}` would otherwise register as a
|
|
376
|
-
* capability and light up a story picker over an object with no methods. Nothing
|
|
377
|
-
* here CALLS a declared endpoint — presence and type only.
|
|
378
|
-
*/
|
|
379
|
-
export function readGameScenes(contract: VolterGameContract | null | undefined): GameScenesReading {
|
|
380
|
-
const declared: unknown = contract?.scenes;
|
|
381
|
-
if (declared === undefined || declared === null) return { scenes: null, malformed: null };
|
|
382
|
-
if (typeof declared !== 'object') {
|
|
383
|
-
return {
|
|
384
|
-
scenes: null,
|
|
385
|
-
malformed: `declared as ${typeof declared}; expected an object with list(), current() and goTo()`,
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
const missing = SCENES_REQUIRED_MEMBERS.filter(
|
|
389
|
-
(member) => typeof (declared as Record<string, unknown>)[member] !== 'function',
|
|
390
|
-
);
|
|
391
|
-
if (missing.length > 0) {
|
|
392
|
-
return {
|
|
393
|
-
scenes: null,
|
|
394
|
-
malformed: `declared a scenes surface missing required member(s): ${missing.join(', ')}`,
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
return { scenes: declared as VolterGameScenes, malformed: null };
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/** What {@link readGameReady} found. `ready: null` with `malformed: null` ⇒ the
|
|
401
|
-
* game declared no readiness signal, which is a different answer from
|
|
402
|
-
* declaring an unusable one — the same distinction {@link readGameScenes}
|
|
403
|
-
* draws, and the reason the readiness facet can report `declared` vs
|
|
404
|
-
* `measured` honestly instead of collapsing both into "not ready yet". */
|
|
405
|
-
export interface GameReadyReading {
|
|
406
|
-
/** The normalized signal: awaiting it once resolves when the game says it is
|
|
407
|
-
* ready. `null` when nothing usable was declared. */
|
|
408
|
-
readonly ready: (() => Promise<unknown>) | null;
|
|
409
|
-
/** Why a DECLARED signal was refused, in the words the host prints. */
|
|
410
|
-
readonly malformed: string | null;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Read the declared {@link VolterGameReady}, refusing a malformed one BY NAME.
|
|
415
|
-
*
|
|
416
|
-
* Both legal forms normalize to one call: a promise is wrapped, a function is
|
|
417
|
-
* invoked lazily (once, when the host asks — which in the ingest mount is only
|
|
418
|
-
* AFTER the first captured render, so a `ready` that itself triggers the boot
|
|
419
|
-
* would deadlock the capture window; games self-boot on this lane),
|
|
420
|
-
* and whatever the function returns is coerced through `Promise.resolve` —
|
|
421
|
-
* a synchronous return means "already ready", which is a real declaration.
|
|
422
|
-
*
|
|
423
|
-
* A rejected/throwing signal is NOT swallowed here: the host awaits it and its
|
|
424
|
-
* failure is what separates "crashed before ready" from "never became ready"
|
|
425
|
-
* in the mount-failure report (`mount-readiness.ts`).
|
|
426
|
-
*/
|
|
427
|
-
export function readGameReady(contract: VolterGameContract | null | undefined): GameReadyReading {
|
|
428
|
-
const declared: unknown = contract?.ready;
|
|
429
|
-
if (declared === undefined || declared === null) return { ready: null, malformed: null };
|
|
430
|
-
if (typeof declared === 'function') {
|
|
431
|
-
const fn = declared as () => unknown;
|
|
432
|
-
return { ready: () => Promise.resolve(fn()), malformed: null };
|
|
433
|
-
}
|
|
434
|
-
if (typeof (declared as { then?: unknown }).then === 'function') {
|
|
435
|
-
const thenable = declared as Promise<unknown>;
|
|
436
|
-
return { ready: () => Promise.resolve(thenable), malformed: null };
|
|
437
|
-
}
|
|
438
|
-
return {
|
|
439
|
-
ready: null,
|
|
440
|
-
malformed: `declared \`ready\` as ${typeof declared}; expected a promise or a function returning one`,
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/** What {@link readGameWorld} found — same two-field honesty as
|
|
445
|
-
* {@link readGameScenes}. */
|
|
446
|
-
export interface GameWorldReading {
|
|
447
|
-
readonly world: object | null;
|
|
448
|
-
readonly malformed: string | null;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Read the declared {@link VolterGameContract.world}, refusing a malformed one BY
|
|
453
|
-
* NAME.
|
|
454
|
-
*
|
|
455
|
-
* STRUCTURAL (`isScene === true`), for two reasons: this file imports no
|
|
456
|
-
* renderer library, and an `instanceof THREE.Scene` across the realm the game
|
|
457
|
-
* runs in is exactly the check that silently answers `false` for a good scene —
|
|
458
|
-
* turning a correct declaration into an unexplained fall-back to first-render.
|
|
459
|
-
*/
|
|
460
|
-
export function readGameWorld(contract: VolterGameContract | null | undefined): GameWorldReading {
|
|
461
|
-
const declared: unknown = contract?.world;
|
|
462
|
-
if (declared === undefined || declared === null) return { world: null, malformed: null };
|
|
463
|
-
if (typeof declared !== 'object' || (declared as { isScene?: unknown }).isScene !== true) {
|
|
464
|
-
return {
|
|
465
|
-
world: null,
|
|
466
|
-
malformed: `declared \`world\` as ${typeof declared}; expected the game's own THREE.Scene`,
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
return { world: declared as object, malformed: null };
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/** What {@link readGamePresentation} found — same two-field honesty as
|
|
473
|
-
* {@link readGameScenes}. */
|
|
474
|
-
export interface GamePresentationReading {
|
|
475
|
-
readonly canvas: HTMLCanvasElement | null;
|
|
476
|
-
readonly malformed: string | null;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Read the declared {@link VolterGameContract.presentation} canvas, refusing a
|
|
481
|
-
* malformed one BY NAME.
|
|
482
|
-
*
|
|
483
|
-
* The shape check is STRUCTURAL (`tagName === 'CANVAS'` plus a `getContext`),
|
|
484
|
-
* not `instanceof`: the contract is read out of the realm the game runs in, and
|
|
485
|
-
* a realm-crossing `instanceof` is exactly the check that silently answers
|
|
486
|
-
* `false` for a perfectly good element. A wrong declaration must be refused by
|
|
487
|
-
* name, never by accident.
|
|
488
|
-
*/
|
|
489
|
-
export function readGamePresentation(
|
|
490
|
-
contract: VolterGameContract | null | undefined,
|
|
491
|
-
): GamePresentationReading {
|
|
492
|
-
const declared: unknown = contract?.presentation;
|
|
493
|
-
if (declared === undefined || declared === null) return { canvas: null, malformed: null };
|
|
494
|
-
const el = declared as { tagName?: unknown; getContext?: unknown };
|
|
495
|
-
if (typeof el !== 'object' || el.tagName !== 'CANVAS' || typeof el.getContext !== 'function') {
|
|
496
|
-
return {
|
|
497
|
-
canvas: null,
|
|
498
|
-
malformed: `declared \`presentation\` as ${
|
|
499
|
-
typeof declared === 'object'
|
|
500
|
-
? String((el as { tagName?: unknown }).tagName ?? 'object')
|
|
501
|
-
: typeof declared
|
|
502
|
-
}; expected the game's own <canvas> element`,
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
return { canvas: declared as HTMLCanvasElement, malformed: null };
|
|
506
|
-
}
|
|
@@ -1,140 +0,0 @@
|
|
|
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.volterGame.ready` ({@link VolterGameReady} 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, the editor's `status` command, 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.volterGame.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.volterGame.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
|
-
}
|