@volter/project 0.5.211 → 0.5.213
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 +208 -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 +98 -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 +33 -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 +8 -32
- 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,46 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `editor/volter.adapter.ts`
|
|
2
|
+
* The project’s `editor/volter.adapter.ts` default-exports a definition created
|
|
3
|
+
* by {@link defineAdapter}. Regions and documents are strict, validated data;
|
|
4
|
+
* editor Layout, style and keymap are imported runtime values. Unknown keys
|
|
5
|
+
* are rejected.
|
|
3
6
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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. `editor.Layout` is an imported React component: the
|
|
23
|
-
* editor invokes it to compose surfaces and edit/play behavior. It is a
|
|
24
|
-
* runtime value, never a serialized manifest setting or a registry key.
|
|
25
|
-
*
|
|
26
|
-
* **Zero inference.** A binding is a DECLARATION or a finder SELECTION the
|
|
27
|
-
* adapter makes — never something the host sniffs out. Finder algorithms
|
|
28
|
-
* (scenes from an entrypoint selection, prefabs from story registrations, …)
|
|
29
|
-
* ship host-side under `adapter/finders/`, and this module names them by
|
|
30
|
-
* SELECTION only ({@link FinderSelection}). That is deliberate and load-bearing
|
|
31
|
-
* twice over:
|
|
32
|
-
* 1. the engine never runs a finder nobody selected — enforced mechanically
|
|
33
|
-
* by `packages/engine/test/finder-import-boundary.test.ts`, whose only
|
|
34
|
-
* sanctioned importer of the finder namespace is the editor's adapter
|
|
35
|
-
* loader;
|
|
36
|
-
* 2. this module therefore imports NOTHING from `adapter/finders/`, so a
|
|
37
|
-
* game's `editor/volter.adapter.ts` pulls no finder implementation (and no source
|
|
38
|
-
* parser) into its own bundle. The dependency runs one way: finders
|
|
39
|
-
* import their parameter types from HERE.
|
|
40
|
-
*
|
|
41
|
-
* The manifest keeps only what must be readable without evaluating any module
|
|
42
|
-
* — identity, roots, boot mode, `server.room`. Everything that binds behavior
|
|
43
|
-
* is the adapter's. This wave adds NO manifest field.
|
|
7
|
+
* Bindings explicitly declare values or select host-side finders. This module
|
|
8
|
+
* contains finder parameter types and selections without importing finder
|
|
9
|
+
* implementations, so project bundles do not pull in host source parsers.
|
|
10
|
+
* The manifest retains project identity and roots; the
|
|
11
|
+
* adapter binds behavior.
|
|
44
12
|
*/
|
|
45
13
|
|
|
46
14
|
import type { KeymapContribution, StyleContribution } from './editor-looks';
|
|
@@ -50,52 +18,46 @@ import type { ResolvedAdapterRoot } from '../manifest/load';
|
|
|
50
18
|
import type { AdapterSurface } from './adapter-surface';
|
|
51
19
|
import { WRITE_ANCHOR_KINDS, type WriteAnchorKind } from './authoring';
|
|
52
20
|
|
|
21
|
+
/** Where the adapter module lives, relative to the project's `volter.project.json`. */
|
|
22
|
+
export const ADAPTER_MODULE_FILENAME = 'editor/volter.adapter.ts';
|
|
23
|
+
|
|
53
24
|
// ---------------------------------------------------------------------------
|
|
54
25
|
// Regions
|
|
55
26
|
// ---------------------------------------------------------------------------
|
|
56
27
|
|
|
57
28
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* A region's WORLD BASIS — which axis points up, and where its ground sits.
|
|
67
|
-
*
|
|
68
|
-
* The editor needs a ground plane to place a dropped asset or a probe point on.
|
|
69
|
-
* That plane used to be the constant `y = 0` compiled into `editor-viewport.ts`,
|
|
70
|
-
* which is a guess about the GAME's convention: a Z-up world (the CAD/Blender
|
|
71
|
-
* idiom foreign R3F games routinely carry) got its drops silently projected onto
|
|
72
|
-
* the wrong plane. It is exactly the shape "zero inference" names — a fact the
|
|
73
|
-
* game's author can state, so the fix is a declaration slot rather than a
|
|
74
|
-
* smarter sniff (ARCHITECTURE-CORE §The editor protocol).
|
|
29
|
+
* A substrate an installed integration serves, as it registers it through its serving half
|
|
30
|
+
* (`ProjectServingModule.substrates`, `@volter/sdk/session/project-serving`). A surface is only
|
|
31
|
+
* the host-provided medium (`canvas`); the substrate is the library a project declares on it, and
|
|
32
|
+
* this registration is what makes that declaration known to the editor. `project` names no
|
|
33
|
+
* substrate itself: a region's declared {@link AdapterRegion.substrate} is validated against the
|
|
34
|
+
* registered names ({@link parseAdapterDefinition}), and a `manifest-roots` region takes its
|
|
35
|
+
* surface's {@link SubstrateRegistration.default} row ({@link regionsFromManifestRoots}).
|
|
75
36
|
*/
|
|
76
|
-
export interface
|
|
77
|
-
/**
|
|
78
|
-
readonly
|
|
79
|
-
/**
|
|
80
|
-
readonly
|
|
37
|
+
export interface SubstrateRegistration {
|
|
38
|
+
/** The name a region declares in `substrate`. */
|
|
39
|
+
readonly substrate: string;
|
|
40
|
+
/** The surface this substrate serves. */
|
|
41
|
+
readonly surface: AdapterSurface;
|
|
42
|
+
/** The source dialects, by name (a spelling of source on this substrate), that a region on it
|
|
43
|
+
* may declare in {@link AdapterRegion.dialect}. */
|
|
44
|
+
readonly dialects: readonly string[];
|
|
45
|
+
/**
|
|
46
|
+
* The default row for {@link surface}: the dialect (one of {@link dialects}) and anchors a
|
|
47
|
+
* first-party root of that surface gets when no explicit `regions` list says otherwise. Absent
|
|
48
|
+
* when this substrate is not the surface's default.
|
|
49
|
+
*/
|
|
50
|
+
readonly default?: {
|
|
51
|
+
readonly dialect: string;
|
|
52
|
+
readonly anchors: readonly WriteAnchorKind[];
|
|
53
|
+
};
|
|
81
54
|
}
|
|
82
55
|
|
|
83
|
-
/**
|
|
84
|
-
* The NATIVE basis — what {@link regionsFromManifestRoots} emits and what a
|
|
85
|
-
* region that declares none is read as. +Y up, ground at 0: three.js' own
|
|
86
|
-
* convention, which every first-party root is authored in. Stated as a value so
|
|
87
|
-
* "the default" has one home instead of a copy at each reader.
|
|
88
|
-
*/
|
|
89
|
-
export const NATIVE_REGION_BASIS: AdapterRegionBasis = Object.freeze({
|
|
90
|
-
up: 'y',
|
|
91
|
-
groundHeight: 0,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
56
|
/**
|
|
95
57
|
* One matrix REGION: a root, its surface, and the library selections that
|
|
96
58
|
* serve it. Universality lives at region granularity — which libraries serve a
|
|
97
|
-
* surface is a per-surface
|
|
98
|
-
*
|
|
59
|
+
* surface is a per-surface fact its integration registers, so a new surface root in an existing
|
|
60
|
+
* project is one manifest line and one derived region, never new adapter code
|
|
99
61
|
* (that is exactly what {@link regionsFromManifestRoots} does).
|
|
100
62
|
*/
|
|
101
63
|
export interface AdapterRegion {
|
|
@@ -103,13 +65,17 @@ export interface AdapterRegion {
|
|
|
103
65
|
readonly id: string;
|
|
104
66
|
/** What the host hands this root. */
|
|
105
67
|
readonly surface: AdapterSurface;
|
|
106
|
-
/** Native substrate, by name. */
|
|
107
|
-
readonly substrate: SubstrateName;
|
|
108
68
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
|
|
69
|
+
* Native substrate, by a registered name ({@link SubstrateRegistration}). `null` when the
|
|
70
|
+
* derivation names none: no installed integration gives this region's surface a default row, or
|
|
71
|
+
* the registrations are unknown.
|
|
72
|
+
*/
|
|
73
|
+
readonly substrate: string | null;
|
|
74
|
+
/**
|
|
75
|
+
* The source dialect, by name: the spelling of source the host writes for this region, one of
|
|
76
|
+
* its substrate's registered {@link SubstrateRegistration.dialects}. `null` states outright
|
|
77
|
+
* that the host writes no source here: the region's truth is the project's own code, which the
|
|
78
|
+
* host does not author (a module or ingest root).
|
|
113
79
|
*/
|
|
114
80
|
readonly dialect: string | null;
|
|
115
81
|
/**
|
|
@@ -118,29 +84,22 @@ export interface AdapterRegion {
|
|
|
118
84
|
* the matrix, which is a work order — never a silently unsupported edit.
|
|
119
85
|
*/
|
|
120
86
|
readonly anchors: readonly WriteAnchorKind[];
|
|
121
|
-
/**
|
|
122
|
-
* This region's world basis. Omitted = {@link NATIVE_REGION_BASIS}, which is
|
|
123
|
-
* a DECLARED default (the native adapter emits it outright), not a silent
|
|
124
|
-
* fallback — same shape as an absent `editor/volter.adapter.ts` meaning
|
|
125
|
-
* `nativeAdapter()`.
|
|
126
|
-
*/
|
|
127
|
-
readonly basis?: AdapterRegionBasis;
|
|
128
87
|
/**
|
|
129
88
|
* Project-relative globs naming source files this region OWNS, for the
|
|
130
89
|
* ambiguous remainder that import reach cannot place.
|
|
131
90
|
*
|
|
132
91
|
* A file's surface/dialect is derived from WHICH REGION'S IMPORT CLOSURE
|
|
133
92
|
* reaches it — a load-bearing fact, because the region's manifest entry is
|
|
134
|
-
* what the host actually executes at mount (the
|
|
93
|
+
* what the host actually executes at mount (the project would break first if it
|
|
135
94
|
* lied). Reach through several regions of the SAME surface is unambiguous;
|
|
136
95
|
* reach through regions of DIFFERING surfaces, or through none at all, is
|
|
137
|
-
* genuinely undecidable from outside, and this is where the
|
|
96
|
+
* genuinely undecidable from outside, and this is where the project's author
|
|
138
97
|
* states the answer instead of the host guessing it from the file's bytes.
|
|
139
98
|
*
|
|
140
|
-
* READER: `packages/sdk/src/
|
|
99
|
+
* READER: `packages/sdk/src/ui-source/file-region-resolver.ts`'s
|
|
141
100
|
* `resolveFileRegion`, consulted BEFORE reach — a declared include wins,
|
|
142
101
|
* because it is a first-party statement about this exact file. Every OID
|
|
143
|
-
* stamping tier, the HMR classifier, the
|
|
102
|
+
* stamping tier, the HMR classifier, the dialect's authoring diagnostics and
|
|
144
103
|
* Content's component grouping read that one resolver.
|
|
145
104
|
*
|
|
146
105
|
* {@link regionsFromManifestRoots} emits none: the native default is PURE
|
|
@@ -157,18 +116,13 @@ export interface AdapterRegion {
|
|
|
157
116
|
* Surfaces this root mounts BESIDE {@link AdapterRegion.surface}, each with
|
|
158
117
|
* the source files that render on it. Together with `surface` — the manifest
|
|
159
118
|
* root's own adapter, which is always the PRIMARY — this states the root's
|
|
160
|
-
* FULL mounted surface set
|
|
161
|
-
* editor protocol).
|
|
119
|
+
* FULL mounted surface set.
|
|
162
120
|
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* half. Before this slot there was nowhere to say so — `id` names the
|
|
169
|
-
* manifest root this region grades, so a second `dom` region for such a root
|
|
170
|
-
* would either name a root the host would try to MOUNT (fabrication, which
|
|
171
|
-
* the anti-shim rule forbids) or name no root at all.
|
|
121
|
+
* One root may mount several surfaces, such as a canvas and a DOM overlay.
|
|
122
|
+
* The declared surface determines how each source file is stamped: React DOM
|
|
123
|
+
* elements need DOM OIDs, while `userData-oid` belongs to a stage element.
|
|
124
|
+
* Additional surfaces belong to this root's `mounts`; a separate region would
|
|
125
|
+
* identify another manifest root for the host to mount.
|
|
172
126
|
*
|
|
173
127
|
* Each mount's `include` is REQUIRED and carries the same glob vocabulary as
|
|
174
128
|
* {@link AdapterRegion.include} (`**`, `*`, `?`). A mount with no files would
|
|
@@ -176,7 +130,7 @@ export interface AdapterRegion {
|
|
|
176
130
|
* no question any reader asks.
|
|
177
131
|
*
|
|
178
132
|
* READER: the same one — `resolveFileRegion`'s rung 1. A file claimed by a
|
|
179
|
-
* region's own `include` AND by a mount is a contradiction in the
|
|
133
|
+
* region's own `include` AND by a mount is a contradiction in the project's own
|
|
180
134
|
* declaration and settles nothing, exactly as two regions claiming one file
|
|
181
135
|
* do; it falls through to reach, and failing that to the caller's loud
|
|
182
136
|
* ambiguous path.
|
|
@@ -203,7 +157,7 @@ export interface AdapterRegionMount {
|
|
|
203
157
|
const AdapterRegionMountSchema = z
|
|
204
158
|
.object({
|
|
205
159
|
surface: z
|
|
206
|
-
.enum(['
|
|
160
|
+
.enum(['stage', 'canvas', 'dom'])
|
|
207
161
|
.describe('An additional surface this root mounts, beside the manifest root’s own adapter'),
|
|
208
162
|
include: z
|
|
209
163
|
.array(z.string().min(1))
|
|
@@ -215,32 +169,28 @@ const AdapterRegionMountSchema = z
|
|
|
215
169
|
})
|
|
216
170
|
.strict();
|
|
217
171
|
|
|
218
|
-
const AdapterRegionBasisSchema = z
|
|
219
|
-
.object({
|
|
220
|
-
up: z.enum(['y', 'z']).describe('World axis pointing away from the ground'),
|
|
221
|
-
groundHeight: z.number().describe('Ground offset along `up`, in world units'),
|
|
222
|
-
})
|
|
223
|
-
.strict();
|
|
224
|
-
|
|
225
172
|
const AdapterRegionSchema = z
|
|
226
173
|
.object({
|
|
227
174
|
id: z.string().min(1).describe('Manifest root id this region grades'),
|
|
228
|
-
surface: z.enum(['
|
|
229
|
-
substrate: z
|
|
175
|
+
surface: z.enum(['stage', 'canvas', 'dom']).describe('Render surface the host hands this root'),
|
|
176
|
+
substrate: z
|
|
177
|
+
.string()
|
|
178
|
+
.min(1)
|
|
179
|
+
.describe(
|
|
180
|
+
'Native substrate serving this region, by name; the editor validates it against the ' +
|
|
181
|
+
'substrates its installed integrations register',
|
|
182
|
+
),
|
|
230
183
|
dialect: z
|
|
231
184
|
.string()
|
|
232
185
|
.min(1)
|
|
233
186
|
.nullable()
|
|
234
187
|
.describe(
|
|
235
|
-
|
|
236
|
-
"no source here (the region's truth is the
|
|
188
|
+
"Source dialect of this region, by name, one of its substrate's registered dialects; null " +
|
|
189
|
+
"states outright that the host writes no source here (the region's truth is the project's own code)",
|
|
237
190
|
),
|
|
238
191
|
anchors: z
|
|
239
192
|
.array(z.enum(WRITE_ANCHOR_KINDS as [WriteAnchorKind, ...WriteAnchorKind[]]))
|
|
240
193
|
.describe('Anchor kinds this region’s writer serves'),
|
|
241
|
-
basis: AdapterRegionBasisSchema.optional().describe(
|
|
242
|
-
'World basis (up axis + ground height); omit for the native +Y / 0 default',
|
|
243
|
-
),
|
|
244
194
|
include: z
|
|
245
195
|
.array(z.string().min(1))
|
|
246
196
|
.min(1)
|
|
@@ -263,7 +213,7 @@ const AdapterRegionSchema = z
|
|
|
263
213
|
/**
|
|
264
214
|
* How a definition supplies its regions: the RULE `'manifest-roots'` (the
|
|
265
215
|
* native default — a declaration may be a rule rather than a list, the way
|
|
266
|
-
* `roots[]` never enumerates a world's contents), or an explicit list a
|
|
216
|
+
* `roots[]` never enumerates a world's contents), or an explicit list a project
|
|
267
217
|
* with novel bindings states outright.
|
|
268
218
|
*/
|
|
269
219
|
export type RegionBinding = 'manifest-roots' | readonly AdapterRegion[];
|
|
@@ -280,32 +230,9 @@ const RegionBindingSchema = z.union([
|
|
|
280
230
|
* neither is a fact the manifest states: which files a region owns beyond
|
|
281
231
|
* its entry's reach, and which surfaces its root mounts beside its own.
|
|
282
232
|
*/
|
|
283
|
-
export
|
|
284
|
-
/** See {@link AdapterRegion.include}. */
|
|
285
|
-
readonly include?: readonly string[];
|
|
286
|
-
/** See {@link AdapterRegion.mounts}. */
|
|
287
|
-
readonly mounts?: readonly AdapterRegionMount[];
|
|
288
|
-
}
|
|
233
|
+
export type AdapterRegionOverlay = Pick<AdapterRegion, 'include' | 'mounts'>;
|
|
289
234
|
|
|
290
|
-
const AdapterRegionOverlaySchema =
|
|
291
|
-
.object({
|
|
292
|
-
include: z
|
|
293
|
-
.array(z.string().min(1))
|
|
294
|
-
.min(1)
|
|
295
|
-
.optional()
|
|
296
|
-
.describe(
|
|
297
|
-
'Project-relative globs (`**`, `*`, `?`) naming source files this derived region owns, ' +
|
|
298
|
-
'for files no root entry’s import closure reaches; a declared include wins over reach',
|
|
299
|
-
),
|
|
300
|
-
mounts: z
|
|
301
|
-
.array(AdapterRegionMountSchema)
|
|
302
|
-
.min(1)
|
|
303
|
-
.optional()
|
|
304
|
-
.describe(
|
|
305
|
-
'Surfaces this root mounts BESIDE its own adapter, each with the files that render on ' +
|
|
306
|
-
'them; with the derived region’s own surface, the root’s full mounted surface set',
|
|
307
|
-
),
|
|
308
|
-
})
|
|
235
|
+
const AdapterRegionOverlaySchema = AdapterRegionSchema.pick({ include: true, mounts: true })
|
|
309
236
|
.strict()
|
|
310
237
|
.refine((value) => value.include !== undefined || value.mounts !== undefined, {
|
|
311
238
|
message:
|
|
@@ -314,22 +241,10 @@ const AdapterRegionOverlaySchema = z
|
|
|
314
241
|
});
|
|
315
242
|
|
|
316
243
|
/**
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
* The finder-selection shape, one level up: `regions: 'manifest-roots'` SELECTS
|
|
322
|
-
* the mechanical derivation, and this table DECLARES the per-region parameters
|
|
323
|
-
* it cannot derive. The two compose — {@link regionsFromManifestRoots} merges
|
|
324
|
-
* them — so a first-party project states one include without hand-writing its
|
|
325
|
-
* whole region table, and adding a root still grows the table for free.
|
|
326
|
-
*
|
|
327
|
-
* Illegal beside an explicit `regions` LIST, and rejected by name: that form is
|
|
328
|
-
* the full REPLACEMENT, and its entries carry `include`/`mounts` themselves.
|
|
329
|
-
* Two homes for one fact is how a declaration goes stale in one of them.
|
|
330
|
-
*
|
|
331
|
-
* MEASURED BASIS: a replace-only region binding means no `nativeAdapter()`
|
|
332
|
-
* project can declare a single include without restating every region.
|
|
244
|
+
* Per-root parameters for the `manifest-roots` region rule.
|
|
245
|
+
* {@link regionsFromManifestRoots} merges these includes and mounts into its
|
|
246
|
+
* derived regions. An explicit region list carries its own parameters and
|
|
247
|
+
* cannot be combined with this table.
|
|
333
248
|
*/
|
|
334
249
|
const RegionIncludesSchema = z
|
|
335
250
|
.record(z.string().min(1), AdapterRegionOverlaySchema)
|
|
@@ -344,48 +259,43 @@ const RegionIncludesSchema = z
|
|
|
344
259
|
* RESOLVED manifest (`@volter/project/manifest/load` already decided both — this never
|
|
345
260
|
* re-derives them).
|
|
346
261
|
*
|
|
347
|
-
*
|
|
262
|
+
* Which library serves a surface is not this module's to say: each surface's
|
|
263
|
+
* row is the {@link SubstrateRegistration.default} an installed integration
|
|
264
|
+
* registers, passed in as `registered`:
|
|
348
265
|
*
|
|
349
|
-
* | root `adapter`
|
|
350
|
-
*
|
|
351
|
-
* |
|
|
352
|
-
* |
|
|
353
|
-
* |
|
|
354
|
-
* | `{module,surface}`
|
|
355
|
-
* | `{ingest,surface}`
|
|
266
|
+
* | root `adapter` | substrate | dialect | anchors |
|
|
267
|
+
* |--------------------|-------------------|-------------------|-------------------|
|
|
268
|
+
* | a surface | its default row's | the row's dialect | the row's anchors |
|
|
269
|
+
* | a surface, no row | `null` | `null` | live-only |
|
|
270
|
+
* | unknown rows | `null` | `null` | none |
|
|
271
|
+
* | `{module,surface}` | its default row's | `null` | live-only |
|
|
272
|
+
* | `{ingest,surface}` | its default row's | `null` | live-only |
|
|
356
273
|
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
* tree. It is listed separately rather than folded into `source-prop` because
|
|
361
|
-
* they are different doors: a lane could lose one and keep the other, and the
|
|
362
|
-
* matrix has to be able to say so.
|
|
274
|
+
* A root whose surface no integration serves gets a region all the same,
|
|
275
|
+
* because the region's id and surface are what file placement reads; it names
|
|
276
|
+
* no substrate and declares `live-only`, since no dialect writer authors it.
|
|
363
277
|
*
|
|
364
|
-
* A module/ingest root's truth is the
|
|
278
|
+
* A module/ingest root's truth is the project's own source, which no first-party
|
|
365
279
|
* dialect writer authors — so it declares `live-only` rather than implying a
|
|
366
280
|
* write path it does not have. That is a red cell and therefore a work order,
|
|
367
281
|
* which is the point of stating it.
|
|
368
282
|
*
|
|
369
|
-
* Every derived region carries {@link NATIVE_REGION_BASIS} outright, so the
|
|
370
|
-
* editor's ground plane READS a declaration on the native path too — the
|
|
371
|
-
* default is published, never assumed at the read site.
|
|
372
|
-
*
|
|
373
283
|
* It DERIVES no {@link AdapterRegion.include} and no {@link AdapterRegion.mounts},
|
|
374
284
|
* and there is no derivation that could: the manifest states ONE adapter per
|
|
375
285
|
* root and names only its entry, so a root's extra owned files and its second
|
|
376
|
-
* mounted surface are facts only the
|
|
286
|
+
* mounted surface are facts only the project can state. It states them as
|
|
377
287
|
* `regionIncludes` PARAMETERS, which this function merges onto the region it
|
|
378
|
-
* derives for that root — the rule stays selected, the parameters are declared
|
|
379
|
-
*
|
|
380
|
-
* manifest-roots rule"). A key naming no declared root merges onto nothing;
|
|
288
|
+
* derives for that root — the rule stays selected, the parameters are declared.
|
|
289
|
+
* A key naming no declared root merges onto nothing;
|
|
381
290
|
* the LOADER reports that, because only it can see both sides.
|
|
382
291
|
*/
|
|
383
292
|
export function regionsFromManifestRoots(
|
|
384
293
|
roots: readonly ResolvedAdapterRoot[],
|
|
294
|
+
registered: readonly SubstrateRegistration[],
|
|
385
295
|
regionIncludes: Readonly<Record<string, AdapterRegionOverlay>> = {},
|
|
386
296
|
): AdapterRegion[] {
|
|
387
297
|
return roots.map((root) => ({
|
|
388
|
-
...derivedRegion(root),
|
|
298
|
+
...derivedRegion(root, registered),
|
|
389
299
|
...overlayOf(regionIncludes[root.id]),
|
|
390
300
|
}));
|
|
391
301
|
}
|
|
@@ -412,38 +322,22 @@ export function unmatchedRegionIncludeIds(
|
|
|
412
322
|
}
|
|
413
323
|
|
|
414
324
|
/** One root's region, before any declared parameters are merged onto it. */
|
|
415
|
-
function derivedRegion(
|
|
325
|
+
function derivedRegion(
|
|
326
|
+
root: ResolvedAdapterRoot,
|
|
327
|
+
registered: readonly SubstrateRegistration[],
|
|
328
|
+
): AdapterRegion {
|
|
416
329
|
const surface = root.surface;
|
|
330
|
+
const row = registered.find((entry) => entry.surface === surface && entry.default !== undefined);
|
|
417
331
|
const firstParty = root.adapter.type === 'builtin';
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (!firstParty) {
|
|
421
|
-
return {
|
|
422
|
-
id: root.id,
|
|
423
|
-
surface,
|
|
424
|
-
substrate,
|
|
425
|
-
dialect: null,
|
|
426
|
-
anchors: ['live-only'] as const,
|
|
427
|
-
basis,
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
if (surface === 'dom') {
|
|
431
|
-
return {
|
|
432
|
-
id: root.id,
|
|
433
|
-
surface,
|
|
434
|
-
substrate,
|
|
435
|
-
dialect: 'jsx',
|
|
436
|
-
anchors: ['source-prop', 'source-structure'] as const,
|
|
437
|
-
basis,
|
|
438
|
-
};
|
|
332
|
+
if (!row?.default) {
|
|
333
|
+
return { id: root.id, surface, substrate: null, dialect: null, anchors: ['live-only'] as const };
|
|
439
334
|
}
|
|
440
335
|
return {
|
|
441
336
|
id: root.id,
|
|
442
337
|
surface,
|
|
443
|
-
substrate,
|
|
444
|
-
dialect:
|
|
445
|
-
anchors: [
|
|
446
|
-
basis,
|
|
338
|
+
substrate: row.substrate,
|
|
339
|
+
dialect: firstParty ? row.default.dialect : null,
|
|
340
|
+
anchors: firstParty ? [...row.default.anchors] : (['live-only'] as const),
|
|
447
341
|
};
|
|
448
342
|
}
|
|
449
343
|
|
|
@@ -459,8 +353,8 @@ export const EXPORTED_COMPOSITION_REGIONS = 'exported-composition-regions';
|
|
|
459
353
|
|
|
460
354
|
/**
|
|
461
355
|
* `scenesFromEntrypointSelection` — read the entrypoint's ACTIVE-SCENE
|
|
462
|
-
* SELECTION, the load-bearing reference (the
|
|
463
|
-
* cannot drift: the
|
|
356
|
+
* SELECTION, the load-bearing reference (the project itself executes it, so it
|
|
357
|
+
* cannot drift: the project would break first).
|
|
464
358
|
*/
|
|
465
359
|
export interface ScenesFromEntrypointSelectionParams {
|
|
466
360
|
readonly finder: 'scenesFromEntrypointSelection';
|
|
@@ -484,21 +378,9 @@ export interface ScenesFromEntrypointSelectionParams {
|
|
|
484
378
|
readonly selection?: string;
|
|
485
379
|
}
|
|
486
380
|
|
|
487
|
-
/**
|
|
488
|
-
* `prefabsFromStories` — read the colocated portable-CSF story registrations.
|
|
489
|
-
* A prefab is an ordinary source component a designer independently places,
|
|
490
|
-
* declared by a story whose `meta.component` names it; the story registration
|
|
491
|
-
* is the load-bearing reference here (removing it removes the declaration).
|
|
492
|
-
* Zero parameters: the selection IS the whole configuration.
|
|
493
|
-
*/
|
|
494
|
-
export interface PrefabsFromStoriesParams {
|
|
495
|
-
readonly finder: 'prefabsFromStories';
|
|
496
|
-
}
|
|
497
|
-
|
|
498
381
|
/**
|
|
499
382
|
* A finder selection: the finder's registered NAME plus its own parameters.
|
|
500
|
-
* The set of finders is OPEN
|
|
501
|
-
* "Documents, not scenes"): the engine's own register in
|
|
383
|
+
* The set of finders is OPEN: the engine's own register in
|
|
502
384
|
* `adapter/finders/registry.ts`, a contribution registers more, and this
|
|
503
385
|
* module validates only the envelope — each finder's parameters are checked
|
|
504
386
|
* against ITS schema the moment the loader runs it, loudly and by name.
|
|
@@ -517,70 +399,72 @@ const FinderSelectionSchema = z
|
|
|
517
399
|
// ---------------------------------------------------------------------------
|
|
518
400
|
|
|
519
401
|
/**
|
|
520
|
-
*
|
|
402
|
+
* Where a `root-mount` entry sits in its region's entrypoint: the key it holds in
|
|
403
|
+
* the entrypoint's module-level selection table.
|
|
404
|
+
*/
|
|
405
|
+
interface RootMountSelection {
|
|
406
|
+
/** Identifier of the table (`const <table> = { <key>: <Component>, … }`). */
|
|
407
|
+
readonly table: string;
|
|
408
|
+
readonly key: string;
|
|
409
|
+
/**
|
|
410
|
+
* The entrypoint indexes its table with this key right now
|
|
411
|
+
* (`scenes[activeScene]`), so mounting the region renders this entry: it is the
|
|
412
|
+
* region's standing document in Edit. Absent when the key cannot be read
|
|
413
|
+
* statically; never a guess.
|
|
414
|
+
*/
|
|
415
|
+
readonly active?: boolean;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* How the RUNNING project is navigated to an entry — the `reachable` honesty
|
|
521
420
|
* clause, stated rather than assumed. An entry whose reach is `none` is a
|
|
522
421
|
* declared-but-unreachable scene: a red cell carrying its own reason.
|
|
523
422
|
*/
|
|
524
423
|
export type SceneReach =
|
|
525
|
-
/** Mounting the region IS opening it — the single-composition degenerate case. */
|
|
526
|
-
| { readonly kind: 'root-mount' }
|
|
527
424
|
/**
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
* `
|
|
531
|
-
*
|
|
532
|
-
* and that key's composition is literally what mounting the region renders.
|
|
533
|
-
* So an active entry is the region's own standing document — it NAMES that
|
|
534
|
-
* document rather than earning a second one beside it, exactly as
|
|
535
|
-
* `root-mount` does. Absent means "not known to be the one at the slot",
|
|
536
|
-
* which is the honest answer whenever the key cannot be read statically; it
|
|
537
|
-
* is never a guess, and it is per-entry because two regions each have their
|
|
538
|
-
* own slot with its own occupant.
|
|
425
|
+
* Mounting the entry's region opens it. Without `selection`, the region mounts
|
|
426
|
+
* one composition and this entry is it. With `selection`, the region's
|
|
427
|
+
* entrypoint renders `table[key]` at its swap slot, so the region mounted at
|
|
428
|
+
* that key opens this entry.
|
|
539
429
|
*/
|
|
540
|
-
| {
|
|
541
|
-
readonly kind: 'entrypoint-selection';
|
|
542
|
-
readonly selection: string;
|
|
543
|
-
readonly key: string;
|
|
544
|
-
readonly active?: boolean;
|
|
545
|
-
}
|
|
430
|
+
| { readonly kind: 'root-mount'; readonly selection?: RootMountSelection }
|
|
546
431
|
/** A portable story mounts it in isolation. */
|
|
547
432
|
| { readonly kind: 'story'; readonly storyId: string }
|
|
548
433
|
/**
|
|
549
|
-
* The RUNNING
|
|
550
|
-
* `sceneId` (
|
|
434
|
+
* The RUNNING project's own published scenes contract reaches it, under
|
|
435
|
+
* `sceneId` (one of the ids its `scenes.list()` publishes).
|
|
551
436
|
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
* `source` can independently own an Edit isolation document; reach never
|
|
555
|
-
* decides whether a source composition can be mounted as a piece.
|
|
437
|
+
* An authorable entry that also names `source` owns an isolation document;
|
|
438
|
+
* reach never decides whether a source composition can be mounted as a piece.
|
|
556
439
|
*
|
|
557
440
|
* A DECLARATION, never an inference: the host does not sniff a mapping
|
|
558
441
|
* between a table entry and a contract id, and it does not assume the two
|
|
559
|
-
* vocabularies coincide. The adapter states which contract id this entry is
|
|
560
|
-
* and the host validates that id against the game's OWN `list()` at the
|
|
561
|
-
* moment it navigates — so a game that stops publishing the scene refuses by
|
|
562
|
-
* name instead of navigating somewhere else.
|
|
442
|
+
* vocabularies coincide. The adapter states which contract id this entry is.
|
|
563
443
|
*/
|
|
564
|
-
| { readonly kind: '
|
|
444
|
+
| { readonly kind: 'project-contract'; readonly sceneId: string }
|
|
565
445
|
/** Declared, with no code path that opens it. */
|
|
566
446
|
| { readonly kind: 'none'; readonly reason: string };
|
|
567
447
|
|
|
568
|
-
const SceneReachSchema = z.
|
|
569
|
-
z.object({ kind: z.literal('root-mount') }).strict(),
|
|
448
|
+
const SceneReachSchema = z.discriminatedUnion('kind', [
|
|
570
449
|
z
|
|
571
450
|
.object({
|
|
572
|
-
kind: z.literal('
|
|
573
|
-
selection: z
|
|
574
|
-
|
|
575
|
-
|
|
451
|
+
kind: z.literal('root-mount'),
|
|
452
|
+
selection: z
|
|
453
|
+
.object({
|
|
454
|
+
table: z.string().min(1),
|
|
455
|
+
key: z.string().min(1),
|
|
456
|
+
active: z.boolean().optional(),
|
|
457
|
+
})
|
|
458
|
+
.strict()
|
|
459
|
+
.optional(),
|
|
576
460
|
})
|
|
577
461
|
.strict(),
|
|
578
462
|
z.object({ kind: z.literal('story'), storyId: z.string().min(1) }).strict(),
|
|
579
|
-
z.object({ kind: z.literal('
|
|
463
|
+
z.object({ kind: z.literal('project-contract'), sceneId: z.string().min(1) }).strict(),
|
|
580
464
|
z.object({ kind: z.literal('none'), reason: z.string().min(1) }).strict(),
|
|
581
465
|
]);
|
|
582
466
|
|
|
583
|
-
/** Where an entry's composition lives in the
|
|
467
|
+
/** Where an entry's composition lives in the project's own source. */
|
|
584
468
|
export interface SceneSource {
|
|
585
469
|
/** Project-relative path. */
|
|
586
470
|
readonly path: string;
|
|
@@ -595,15 +479,12 @@ const SceneSourceSchema = z
|
|
|
595
479
|
/**
|
|
596
480
|
* One entry of the scene table. Scenes and prefabs are SIBLINGS here — both
|
|
597
481
|
* are registered compositions the editor opens in isolation through the same
|
|
598
|
-
* verb, differing only in instance site
|
|
599
|
-
* a ROLE, not a kind … the same kind of thing as a prefab").
|
|
482
|
+
* verb, differing only in instance site.
|
|
600
483
|
*/
|
|
601
484
|
export interface DocumentEntry {
|
|
602
485
|
readonly id: string;
|
|
603
486
|
readonly label: string;
|
|
604
|
-
/**
|
|
605
|
-
/** OPEN: `scene` and `prefab` are the first two kinds; a page, a component,
|
|
606
|
-
* a model, a bake are kinds the same way. */
|
|
487
|
+
/** Open document kind, such as scene, prefab, page, component, model or bake. */
|
|
607
488
|
readonly kind: string;
|
|
608
489
|
/** Owning region id; `null` for a project-scoped entry bound to no one root. */
|
|
609
490
|
readonly region: string | null;
|
|
@@ -614,12 +495,11 @@ export interface DocumentEntry {
|
|
|
614
495
|
readonly authorable: boolean;
|
|
615
496
|
readonly reach: SceneReach;
|
|
616
497
|
readonly source?: SceneSource;
|
|
617
|
-
/** Optional adapter-declared prerequisite run before `source` is constructed
|
|
618
|
-
* in Edit isolation. The export is a zero-argument function; the host never
|
|
619
|
-
* infers setup from neighboring filenames or game globals. */
|
|
620
|
-
readonly isolationSetup?: SceneSource;
|
|
621
498
|
/** Which finder produced it; absent = the adapter stated it outright. */
|
|
622
499
|
readonly finder?: string;
|
|
500
|
+
/** `true` = its editor opens it before its file is written, and its first save writes the file: what `editor.open`
|
|
501
|
+
* of its id opens when the file is not there yet. */
|
|
502
|
+
readonly opensUnwritten?: boolean;
|
|
623
503
|
}
|
|
624
504
|
|
|
625
505
|
const DocumentEntrySchema = z
|
|
@@ -631,8 +511,8 @@ const DocumentEntrySchema = z
|
|
|
631
511
|
authorable: z.boolean(),
|
|
632
512
|
reach: SceneReachSchema,
|
|
633
513
|
source: SceneSourceSchema.optional(),
|
|
634
|
-
isolationSetup: SceneSourceSchema.optional(),
|
|
635
514
|
finder: z.string().min(1).optional(),
|
|
515
|
+
opensUnwritten: z.boolean().optional(),
|
|
636
516
|
})
|
|
637
517
|
.strict();
|
|
638
518
|
|
|
@@ -659,171 +539,37 @@ const AdapterDocumentTableSchema = z
|
|
|
659
539
|
})
|
|
660
540
|
.strict();
|
|
661
541
|
|
|
662
|
-
// ---------------------------------------------------------------------------
|
|
663
|
-
// Observation
|
|
664
|
-
// ---------------------------------------------------------------------------
|
|
665
|
-
|
|
666
|
-
/**
|
|
667
|
-
* The two faces a declaration projects onto. They are exactly the two
|
|
668
|
-
* `DebugAdapter` halves (`adapter/system-adapter.ts`): `state` becomes a
|
|
669
|
-
* provider (`providers()` / `state(name)`), `command` becomes a verb
|
|
670
|
-
* (`commands()` / `invoke(name, args)`). There is deliberately no third kind —
|
|
671
|
-
* see {@link OBSERVATION_KIND_REFUSAL}.
|
|
672
|
-
*/
|
|
673
|
-
export const OBSERVATION_KINDS = ['command', 'state'] as const;
|
|
674
|
-
export type ObservationKind = (typeof OBSERVATION_KINDS)[number];
|
|
675
|
-
|
|
676
|
-
/**
|
|
677
|
-
* Why a declaration may not name a `SystemAdapters` slot here.
|
|
678
|
-
*
|
|
679
|
-
* A game's system slots are already declared through the game contract's own
|
|
680
|
-
* `systemAdapters` carrier (`adapter/ingest/game-contract.ts`), which validates
|
|
681
|
-
* each slot per-surface and reports a malformed one BY NAME
|
|
682
|
-
* (`contract-system-adapters.ts`). A second door onto the same slots is exactly
|
|
683
|
-
* the two-declarations-one-slot problem `game-contract.ts` records for `debug`,
|
|
684
|
-
* where it says outright that a second door "would let a game declare two
|
|
685
|
-
* different debug planes with no rule for which wins".
|
|
686
|
-
*/
|
|
687
|
-
const OBSERVATION_KIND_REFUSAL =
|
|
688
|
-
"observation.kind must be 'command' or 'state' — the two DebugAdapter faces a declaration " +
|
|
689
|
-
'projects onto. A SystemAdapters slot is declared through the game contract’s `systemAdapters` ' +
|
|
690
|
-
'carrier, which validates it per-surface; declaring one here would be a second door onto the ' +
|
|
691
|
-
'same slot with no rule for which wins.';
|
|
692
|
-
|
|
693
|
-
/**
|
|
694
|
-
* One statically declared observation slot. Declaring a slot and ANSWERING it
|
|
695
|
-
* live are separate facts (the two-state rule): a slot declared here whose
|
|
696
|
-
* `answer` throws, whose `answer` is absent, or whose game never booted reads as
|
|
697
|
-
* broken, never as "the game has none". The projector
|
|
698
|
-
* (`adapter/ingest/observation-debug-adapter.ts`) keeps that distinction: an
|
|
699
|
-
* unanswerable slot is still LISTED, and refuses by name when read.
|
|
700
|
-
*/
|
|
701
|
-
export interface ObservationDeclaration {
|
|
702
|
-
readonly id: string;
|
|
703
|
-
readonly kind: ObservationKind;
|
|
704
|
-
readonly description?: string | undefined;
|
|
705
|
-
/** Command execution side; omitted means a local app-owned command. */
|
|
706
|
-
readonly locus?: 'client' | 'server' | undefined;
|
|
707
|
-
/**
|
|
708
|
-
* Lazily evaluated against the mounted game. The field may be a closure; the
|
|
709
|
-
* TABLE around it stays statically readable, which is the whole discipline.
|
|
710
|
-
*
|
|
711
|
-
* What `game` IS is the host's to hand over and is stated at the projector:
|
|
712
|
-
* for an ingest mount it is the REALM the game's own modules ran in, because
|
|
713
|
-
* a foreign game's public handles are the only thing the host can honestly
|
|
714
|
-
* pass (`observation-debug-adapter.ts`).
|
|
715
|
-
*/
|
|
716
|
-
readonly answer?: (game: unknown, ...args: unknown[]) => unknown;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
const ObservationDeclarationSchema = z
|
|
720
|
-
.object({
|
|
721
|
-
id: z.string().min(1),
|
|
722
|
-
kind: z.enum(OBSERVATION_KINDS, { error: () => OBSERVATION_KIND_REFUSAL }),
|
|
723
|
-
description: z.string().min(1).optional(),
|
|
724
|
-
locus: z.enum(['client', 'server']).optional(),
|
|
725
|
-
answer: z
|
|
726
|
-
.custom<(game: unknown) => unknown>((value) => typeof value === 'function', {
|
|
727
|
-
message: 'observation.answer must be a function evaluated against the mounted game',
|
|
728
|
-
})
|
|
729
|
-
.optional(),
|
|
730
|
-
})
|
|
731
|
-
.strict()
|
|
732
|
-
.refine((value) => value.kind === 'command' || value.locus === undefined, {
|
|
733
|
-
path: ['locus'],
|
|
734
|
-
message: 'observation.locus is valid only for a command declaration',
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
// ---------------------------------------------------------------------------
|
|
738
|
-
// Native input
|
|
739
|
-
// ---------------------------------------------------------------------------
|
|
740
|
-
|
|
741
|
-
/** The action value shapes the session input door already speaks. */
|
|
742
|
-
export const ADAPTER_INPUT_VALUE_TYPES = [
|
|
743
|
-
'digital',
|
|
744
|
-
'scalar',
|
|
745
|
-
'vector2',
|
|
746
|
-
'pointerDelta',
|
|
747
|
-
'pointerPosition',
|
|
748
|
-
] as const;
|
|
749
|
-
export type AdapterInputValueType = (typeof ADAPTER_INPUT_VALUE_TYPES)[number];
|
|
750
|
-
export type AdapterInputValue = boolean | number | { readonly x: number; readonly y: number };
|
|
751
|
-
|
|
752
|
-
/** One action in an app-owned input store. */
|
|
753
|
-
export interface AdapterInputAction {
|
|
754
|
-
readonly name: string;
|
|
755
|
-
readonly valueType: AdapterInputValueType;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
/**
|
|
759
|
-
* Adapter-side binding from the universal session input door to a game's
|
|
760
|
-
* ordinary input store. The game owns both its physical-device listeners and
|
|
761
|
-
* its state; this table only tells the host how to drive that same state for a
|
|
762
|
-
* bot or live REPL session. No component imports this contract.
|
|
763
|
-
*/
|
|
764
|
-
export interface AdapterInputBinding {
|
|
765
|
-
/** Manifest root whose native scheduler consumes these actions. */
|
|
766
|
-
readonly root: string;
|
|
767
|
-
/** Current action vocabulary. A closure permits an app-owned dynamic map. */
|
|
768
|
-
readonly actions: (game: unknown) => readonly AdapterInputAction[];
|
|
769
|
-
/** Apply one action through the app's own input-store write path. */
|
|
770
|
-
readonly set: (game: unknown, action: string, value: AdapterInputValue) => void;
|
|
771
|
-
/** Release every app-owned virtual action. */
|
|
772
|
-
readonly clear: (game: unknown) => void;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
const AdapterInputBindingSchema = z
|
|
776
|
-
.object({
|
|
777
|
-
root: z.string().min(1),
|
|
778
|
-
actions: z.custom<AdapterInputBinding['actions']>((value) => typeof value === 'function', {
|
|
779
|
-
message: 'input.actions must be a function returning the app-owned action vocabulary',
|
|
780
|
-
}),
|
|
781
|
-
set: z.custom<AdapterInputBinding['set']>((value) => typeof value === 'function', {
|
|
782
|
-
message: 'input.set must be a function writing the app-owned input store',
|
|
783
|
-
}),
|
|
784
|
-
clear: z.custom<AdapterInputBinding['clear']>((value) => typeof value === 'function', {
|
|
785
|
-
message: 'input.clear must be a function releasing app-owned virtual actions',
|
|
786
|
-
}),
|
|
787
|
-
})
|
|
788
|
-
.strict();
|
|
789
|
-
|
|
790
542
|
// ---------------------------------------------------------------------------
|
|
791
543
|
// The definition
|
|
792
544
|
// ---------------------------------------------------------------------------
|
|
793
545
|
|
|
794
546
|
/** Project-owned editor defaults. User layout choices remain session state. */
|
|
795
|
-
export interface AdapterEditorLayoutProps {
|
|
796
|
-
readonly playing: boolean;
|
|
797
|
-
readonly paused: boolean;
|
|
798
|
-
}
|
|
799
547
|
export interface AdapterEditorConfiguration {
|
|
800
|
-
/** Imported React
|
|
801
|
-
readonly Layout?: ComponentType
|
|
548
|
+
/** Imported React component the editor renders as its composition. */
|
|
549
|
+
readonly Layout?: ComponentType;
|
|
802
550
|
|
|
803
551
|
/**
|
|
804
|
-
*
|
|
805
|
-
* (
|
|
806
|
-
*
|
|
807
|
-
* `
|
|
808
|
-
*
|
|
809
|
-
*
|
|
552
|
+
* A style this project PINS, imported from the package that carries it
|
|
553
|
+
* (`<package>`'s exported style): the same `StyleContribution` the package's
|
|
554
|
+
* `workspace.style` contribution registers, with the colour theme it was drawn with. Set, it
|
|
555
|
+
* outranks a person's cross-project choice (`settings-store.ts`); left out, the project wears
|
|
556
|
+
* the product's default, its built-in extension's `configurationDefaults`, and a person chooses
|
|
557
|
+
* freely. Projects `create` scaffolds leave it out.
|
|
810
558
|
*/
|
|
811
559
|
readonly style?: StyleContribution;
|
|
812
|
-
/** The project's KEY BINDINGS, imported the same way (
|
|
560
|
+
/** The project's KEY BINDINGS, imported the same way (`<package>`'s exported keymap). */
|
|
813
561
|
readonly keymap?: KeymapContribution;
|
|
814
562
|
|
|
815
563
|
/** Registered workspace to open when this checkout has no saved workspace. */
|
|
816
564
|
readonly workspace?: string;
|
|
817
565
|
/** Inspector layout beneath the active workspace and the user's override. */
|
|
818
566
|
readonly inspector?: 'column' | 'properties' | 'card';
|
|
819
|
-
/** Utility registry ids to reveal when Play settles; omitted means none. */
|
|
820
|
-
readonly playUtilities?: readonly string[];
|
|
821
567
|
}
|
|
822
568
|
|
|
823
569
|
const AdapterEditorConfigurationSchema = z
|
|
824
570
|
.object({
|
|
825
571
|
Layout: z
|
|
826
|
-
.custom<ComponentType
|
|
572
|
+
.custom<ComponentType>(
|
|
827
573
|
(value) => typeof value === 'function',
|
|
828
574
|
'Layout must be an imported React component',
|
|
829
575
|
)
|
|
@@ -848,7 +594,6 @@ const AdapterEditorConfigurationSchema = z
|
|
|
848
594
|
.optional(),
|
|
849
595
|
workspace: z.string().min(1).optional(),
|
|
850
596
|
inspector: z.enum(['column', 'properties', 'card']).optional(),
|
|
851
|
-
playUtilities: z.array(z.string().min(1)).optional(),
|
|
852
597
|
})
|
|
853
598
|
.strict();
|
|
854
599
|
|
|
@@ -864,24 +609,20 @@ export interface AdapterDefinition {
|
|
|
864
609
|
*/
|
|
865
610
|
readonly regionIncludes: Readonly<Record<string, AdapterRegionOverlay>>;
|
|
866
611
|
readonly documents: AdapterDocumentTable;
|
|
867
|
-
readonly observation: readonly ObservationDeclaration[];
|
|
868
|
-
readonly input?: AdapterInputBinding | undefined;
|
|
869
612
|
}
|
|
870
613
|
|
|
871
|
-
/** What a
|
|
872
|
-
* first-party case, and an adapter's SIZE measures the
|
|
614
|
+
/** What a project writes. Every field optional — the near-empty adapter is the
|
|
615
|
+
* first-party case, and an adapter's SIZE measures the project's distance from
|
|
873
616
|
* native. */
|
|
874
617
|
export interface AdapterDefinitionInput {
|
|
875
618
|
readonly editor?: AdapterEditorConfiguration;
|
|
876
619
|
readonly regions?: RegionBinding;
|
|
877
620
|
readonly regionIncludes?: Readonly<Record<string, AdapterRegionOverlay>>;
|
|
878
621
|
readonly documents?: AdapterDocumentTable;
|
|
879
|
-
readonly observation?: readonly ObservationDeclaration[];
|
|
880
|
-
readonly input?: AdapterInputBinding | undefined;
|
|
881
622
|
}
|
|
882
623
|
|
|
883
624
|
/**
|
|
884
|
-
* Why the two forms may not be combined, said in the error a
|
|
625
|
+
* Why the two forms may not be combined, said in the error a project's author
|
|
885
626
|
* reads. An explicit region list REPLACES the derivation and its entries carry
|
|
886
627
|
* `include`/`mounts` themselves; layering a second table over it would put one
|
|
887
628
|
* fact in two homes, with no rule for which wins.
|
|
@@ -892,14 +633,12 @@ const REGION_INCLUDES_WITH_LIST_REFUSAL =
|
|
|
892
633
|
'its own `include`/`mounts`. Move these globs onto the matching `regions[]` entry, or drop the ' +
|
|
893
634
|
'list and let the derivation run.';
|
|
894
635
|
|
|
895
|
-
|
|
636
|
+
const AdapterDefinitionSchema = z
|
|
896
637
|
.object({
|
|
897
638
|
editor: AdapterEditorConfigurationSchema.optional(),
|
|
898
639
|
regions: RegionBindingSchema.optional(),
|
|
899
640
|
regionIncludes: RegionIncludesSchema.optional(),
|
|
900
641
|
documents: AdapterDocumentTableSchema.optional(),
|
|
901
|
-
observation: z.array(ObservationDeclarationSchema).optional(),
|
|
902
|
-
input: AdapterInputBindingSchema.optional(),
|
|
903
642
|
})
|
|
904
643
|
.strict()
|
|
905
644
|
// An EMPTY table beside a list is fine and must stay fine: `defineAdapter`
|
|
@@ -914,10 +653,19 @@ export const AdapterDefinitionSchema = z
|
|
|
914
653
|
{ message: REGION_INCLUDES_WITH_LIST_REFUSAL },
|
|
915
654
|
);
|
|
916
655
|
|
|
656
|
+
/** The error a malformed definition throws, naming each key. */
|
|
657
|
+
function invalidDefinition(issues: z.ZodError['issues']): Error {
|
|
658
|
+
return new Error(
|
|
659
|
+
`editor/volter.adapter.ts: invalid adapter definition — ${issues
|
|
660
|
+
.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
|
|
661
|
+
.join('; ')}`,
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
917
665
|
/**
|
|
918
|
-
* Validate and freeze a
|
|
666
|
+
* Validate and freeze a project's binding table.
|
|
919
667
|
*
|
|
920
|
-
* Called TWICE by design and with one schema: once here, in the
|
|
668
|
+
* Called TWICE by design and with one schema: once here, in the project's own
|
|
921
669
|
* module, so a malformed table fails at the author's own file with the key
|
|
922
670
|
* named; and once in the host's loader, because the loaded default export is
|
|
923
671
|
* untrusted input crossing a module boundary. Two call sites, one rule — not
|
|
@@ -925,41 +673,19 @@ export const AdapterDefinitionSchema = z
|
|
|
925
673
|
*/
|
|
926
674
|
export function defineAdapter(input: AdapterDefinitionInput = {}): AdapterDefinition {
|
|
927
675
|
const parsed = AdapterDefinitionSchema.safeParse(input);
|
|
928
|
-
if (!parsed.success)
|
|
929
|
-
throw new Error(
|
|
930
|
-
`editor/volter.adapter.ts: invalid adapter definition — ${parsed.error.issues
|
|
931
|
-
.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
|
|
932
|
-
.join('; ')}`,
|
|
933
|
-
);
|
|
934
|
-
}
|
|
676
|
+
if (!parsed.success) throw invalidDefinition(parsed.error.issues);
|
|
935
677
|
return Object.freeze({
|
|
936
678
|
...(input.editor
|
|
937
679
|
? {
|
|
938
|
-
editor: Object.freeze({
|
|
939
|
-
...input.editor,
|
|
940
|
-
...(input.editor.playUtilities
|
|
941
|
-
? { playUtilities: Object.freeze([...input.editor.playUtilities]) }
|
|
942
|
-
: {}),
|
|
943
|
-
}),
|
|
680
|
+
editor: Object.freeze({ ...input.editor }),
|
|
944
681
|
}
|
|
945
682
|
: {}),
|
|
946
683
|
regions: input.regions ?? 'manifest-roots',
|
|
947
684
|
regionIncludes: Object.freeze({ ...(input.regionIncludes ?? {}) }),
|
|
948
685
|
documents: Object.freeze({ ...(input.documents ?? {}) }),
|
|
949
|
-
observation: Object.freeze([...(input.observation ?? [])]),
|
|
950
|
-
...(input.input ? { input: input.input } : {}),
|
|
951
686
|
});
|
|
952
687
|
}
|
|
953
688
|
|
|
954
|
-
/** Extra bindings a near-native game adds without restating the native ones. */
|
|
955
|
-
export interface NativeAdapterOptions {
|
|
956
|
-
/** Scene-table entries this game states outright, beside what the finders find. */
|
|
957
|
-
readonly scenes?: readonly DocumentEntry[];
|
|
958
|
-
/** Per-region `include`/`mounts` parameters for the derivation — see
|
|
959
|
-
* {@link AdapterDefinition.regionIncludes}. */
|
|
960
|
-
readonly regionIncludes?: Readonly<Record<string, AdapterRegionOverlay>>;
|
|
961
|
-
}
|
|
962
|
-
|
|
963
689
|
/**
|
|
964
690
|
* THE NATIVE DEFAULT — regions derived mechanically from the manifest's
|
|
965
691
|
* `roots[]`, and a scene table produced by the two shipped finders. This is
|
|
@@ -967,12 +693,10 @@ export interface NativeAdapterOptions {
|
|
|
967
693
|
* is also what a project with NO `editor/volter.adapter.ts` gets: that absence is the
|
|
968
694
|
* declared native default, not a silent fallback.
|
|
969
695
|
*/
|
|
970
|
-
export function nativeAdapter(
|
|
696
|
+
export function nativeAdapter(): AdapterDefinition {
|
|
971
697
|
return defineAdapter({
|
|
972
698
|
regions: 'manifest-roots',
|
|
973
|
-
...(options.regionIncludes ? { regionIncludes: options.regionIncludes } : {}),
|
|
974
699
|
documents: {
|
|
975
|
-
...(options.scenes ? { entries: options.scenes } : {}),
|
|
976
700
|
find: [
|
|
977
701
|
{ finder: 'scenesFromEntrypointSelection', regions: EXPORTED_COMPOSITION_REGIONS },
|
|
978
702
|
{ finder: 'prefabsFromStories' },
|
|
@@ -982,16 +706,35 @@ export function nativeAdapter(options: NativeAdapterOptions = {}): AdapterDefini
|
|
|
982
706
|
}
|
|
983
707
|
|
|
984
708
|
/**
|
|
985
|
-
* Parse an untrusted default export
|
|
986
|
-
*
|
|
987
|
-
*
|
|
709
|
+
* Parse an untrusted adapter-module default export through {@link defineAdapter}.
|
|
710
|
+
* Validate explicit regions against installed substrate registrations, including
|
|
711
|
+
* each substrate’s allowed dialects. Finder names and selection parameters are
|
|
712
|
+
* validated by the loader that resolves those selections.
|
|
988
713
|
*/
|
|
989
|
-
export function parseAdapterDefinition(
|
|
714
|
+
export function parseAdapterDefinition(
|
|
715
|
+
value: unknown,
|
|
716
|
+
registered: readonly SubstrateRegistration[],
|
|
717
|
+
): AdapterDefinition {
|
|
990
718
|
if (value === null || typeof value !== 'object') {
|
|
991
719
|
throw new Error(
|
|
992
720
|
'editor/volter.adapter.ts must default-export defineAdapter({…}) / nativeAdapter() — ' +
|
|
993
721
|
`got ${value === null ? 'null' : typeof value}`,
|
|
994
722
|
);
|
|
995
723
|
}
|
|
996
|
-
|
|
724
|
+
const definition = defineAdapter(value as AdapterDefinitionInput);
|
|
725
|
+
if (definition.regions === 'manifest-roots') return definition;
|
|
726
|
+
const substrates = z.object({
|
|
727
|
+
regions: z.array(z.object({ substrate: z.enum(registered.map((entry) => entry.substrate)) })),
|
|
728
|
+
});
|
|
729
|
+
const parsed = substrates.safeParse({ regions: definition.regions });
|
|
730
|
+
if (!parsed.success) throw invalidDefinition(parsed.error.issues);
|
|
731
|
+
const dialectIssues = definition.regions.flatMap((region, index) => {
|
|
732
|
+
const dialects = registered.find((entry) => entry.substrate === region.substrate)?.dialects ?? [];
|
|
733
|
+
const dialect = z.enum(dialects).nullable().safeParse(region.dialect);
|
|
734
|
+
return dialect.success
|
|
735
|
+
? []
|
|
736
|
+
: dialect.error.issues.map((issue) => ({ ...issue, path: ['regions', index, 'dialect', ...issue.path] }));
|
|
737
|
+
});
|
|
738
|
+
if (dialectIssues.length > 0) throw invalidDefinition(dialectIssues);
|
|
739
|
+
return definition;
|
|
997
740
|
}
|