@vgai/engine 0.5.42 → 0.5.45
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/dist/adapter/adapter-module.d.ts +28 -29
- package/dist/adapter/adapter-module.d.ts.map +1 -1
- package/dist/adapter/adapter-module.js +11 -34
- package/dist/adapter/authoring-seam-contract.d.ts +5 -0
- package/dist/adapter/authoring-seam-contract.d.ts.map +1 -1
- package/dist/adapter/authoring-seam-contract.js +1 -0
- package/dist/adapter/authoring.d.ts +16 -0
- package/dist/adapter/authoring.d.ts.map +1 -1
- package/dist/adapter/finders/finder-result.d.ts +2 -2
- package/dist/adapter/finders/finder-result.d.ts.map +1 -1
- package/dist/adapter/finders/index.d.ts +15 -2
- package/dist/adapter/finders/index.d.ts.map +1 -1
- package/dist/adapter/finders/index.js +38 -10
- package/dist/adapter/finders/prefabs-from-stories.d.ts.map +1 -1
- package/dist/adapter/finders/registry.d.ts +45 -0
- package/dist/adapter/finders/registry.d.ts.map +1 -0
- package/dist/adapter/finders/registry.js +56 -0
- package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
- package/dist/adapter/ingest/scene-capture.js +50 -20
- package/dist/adapter/system-adapter.d.ts +3 -1
- package/dist/adapter/system-adapter.d.ts.map +1 -1
- package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -1
- package/dist/canvas-react/pixi-react-root-factory.js +21 -11
- package/dist/data/vite-plugin-data.d.ts +1 -1
- package/dist/data/vite-plugin-data.d.ts.map +1 -1
- package/dist/data/vite-plugin-data.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/input/host-pointer.d.ts +18 -0
- package/dist/input/host-pointer.d.ts.map +1 -1
- package/dist/input/host-pointer.js +54 -5
- package/dist/input/input-manager.d.ts +83 -1
- package/dist/input/input-manager.d.ts.map +1 -1
- package/dist/input/input-manager.js +357 -8
- package/dist/manifest/configuration-kinds.d.ts +97 -0
- package/dist/manifest/configuration-kinds.d.ts.map +1 -0
- package/dist/manifest/configuration-kinds.js +148 -0
- package/dist/manifest/index.d.ts +2 -1
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/manifest/index.js +4 -3
- package/dist/manifest/kind-modules.d.ts +18 -0
- package/dist/manifest/kind-modules.d.ts.map +1 -0
- package/dist/manifest/kind-modules.js +58 -0
- package/dist/manifest/load-file.d.ts +3 -3
- package/dist/manifest/load-file.d.ts.map +1 -1
- package/dist/manifest/load-file.js +4 -4
- package/dist/manifest/load.d.ts +17 -12
- package/dist/manifest/load.d.ts.map +1 -1
- package/dist/manifest/load.js +29 -19
- package/dist/manifest/runtime-environment.d.ts +11 -0
- package/dist/manifest/runtime-environment.d.ts.map +1 -0
- package/dist/manifest/runtime-environment.js +44 -0
- package/dist/manifest/schema.d.ts +10 -9
- package/dist/manifest/schema.d.ts.map +1 -1
- package/dist/manifest/schema.js +32 -27
- package/dist/react/use-data.d.ts +1 -1
- package/dist/react/use-data.js +1 -1
- package/dist/render/matcap-texture.d.ts +5 -0
- package/dist/render/matcap-texture.d.ts.map +1 -0
- package/dist/render/matcap-texture.js +82 -0
- package/dist/render/soft-particle-depth.d.ts +14 -8
- package/dist/render/soft-particle-depth.d.ts.map +1 -1
- package/dist/render/soft-particle-depth.js +37 -14
- package/dist/render/viewport-shading.d.ts +11 -1
- package/dist/render/viewport-shading.d.ts.map +1 -1
- package/dist/render/viewport-shading.js +25 -0
- package/dist/runtime/create-runtime.js +6 -5
- package/dist/runtime/mount-manifest.d.ts.map +1 -1
- package/dist/runtime/mount-manifest.js +11 -2
- package/dist/settings/schema.d.ts +99 -0
- package/dist/settings/schema.d.ts.map +1 -0
- package/dist/settings/schema.js +110 -0
- package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
- package/dist/world3d-react/r3f-root-factory.js +34 -36
- package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
- package/dist/world3d-react/rapier-physics-bridge.js +21 -6
- package/dist-config/config.js +1 -1
- package/dist-config/config.js.map +2 -2
- package/package.json +2 -1
- package/schemas/engine-capabilities.json +4 -4
- package/schemas/vgai-project.schema.json +41 -36
- package/schemas/vgai-settings.schema.json +112 -0
- package/src/adapter/adapter-module.ts +33 -51
- package/src/adapter/authoring-seam-contract.ts +1 -0
- package/src/adapter/authoring.ts +16 -0
- package/src/adapter/finders/finder-result.ts +2 -2
- package/src/adapter/finders/index.ts +68 -12
- package/src/adapter/finders/prefabs-from-stories.ts +2 -2
- package/src/adapter/finders/registry.ts +95 -0
- package/src/adapter/finders/scenes-from-entrypoint-selection.ts +4 -4
- package/src/adapter/ingest/scene-capture.ts +48 -19
- package/src/adapter/system-adapter.ts +3 -1
- package/src/canvas-react/pixi-react-root-factory.tsx +21 -11
- package/src/data/vite-plugin-data.ts +4 -2
- package/src/index.ts +1 -1
- package/src/input/host-pointer.ts +84 -5
- package/src/input/input-manager.ts +404 -7
- package/src/manifest/configuration-kinds.ts +241 -0
- package/src/manifest/index.ts +4 -3
- package/src/manifest/kind-modules.ts +58 -0
- package/src/manifest/load-file.ts +11 -5
- package/src/manifest/load.ts +53 -22
- package/src/manifest/runtime-environment.ts +48 -0
- package/src/manifest/schema.ts +39 -33
- package/src/react/use-data.ts +1 -1
- package/src/render/matcap-texture.ts +92 -0
- package/src/render/soft-particle-depth.ts +43 -14
- package/src/render/viewport-shading.ts +33 -1
- package/src/runtime/create-runtime.ts +6 -5
- package/src/runtime/mount-manifest.ts +13 -2
- package/src/settings/schema.ts +127 -0
- package/src/world3d-react/r3f-root-factory.tsx +45 -39
- package/src/world3d-react/rapier-physics-bridge.tsx +22 -8
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE RUN-CONFIGURATION KIND REGISTRY — the open set of kinds a manifest's
|
|
3
|
+
* `configurations[]` may declare (ARCHITECTURE-CORE §The project model). A run
|
|
4
|
+
* configuration is one of the project's ENTRYPOINTS: something the editor,
|
|
5
|
+
* the CLI or a harness can start by id. The engine registers two kinds
|
|
6
|
+
* here; a capability registers more through the same door; the manifest
|
|
7
|
+
* validates each declaration against its kind's own schema at load, and an
|
|
8
|
+
* unregistered kind refuses loudly naming the registered ones.
|
|
9
|
+
*
|
|
10
|
+
* The implicit configuration every project with roots has is `play` — mount
|
|
11
|
+
* every root in the host — which is never declared and may be named by a
|
|
12
|
+
* `compound`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { type ZodType, z } from 'zod';
|
|
16
|
+
|
|
17
|
+
/** The envelope every declaration carries; the rest is the kind's. */
|
|
18
|
+
export interface ConfigurationEnvelope {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly kind: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ConfigurationRole = 'run' | 'build';
|
|
24
|
+
|
|
25
|
+
export interface ConfigurationKindRegistration<
|
|
26
|
+
T extends ConfigurationEnvelope = ConfigurationEnvelope,
|
|
27
|
+
> {
|
|
28
|
+
readonly kind: T['kind'];
|
|
29
|
+
/** Where a configuration of this kind surfaces and which verb starts it:
|
|
30
|
+
* `run` (the transport, `vgai run`) or `build` (Export, `vgai build`). */
|
|
31
|
+
readonly role: ConfigurationRole;
|
|
32
|
+
/** The declaration's full shape, envelope included; `.strict()`. */
|
|
33
|
+
readonly schema: ZodType<T>;
|
|
34
|
+
/** One line for schema output and refusals. */
|
|
35
|
+
readonly describe: string;
|
|
36
|
+
/** A build-role kind backed by a project TOOL (the operation catalog):
|
|
37
|
+
* the host runs `tool` with `input(configuration)` and reports its
|
|
38
|
+
* outcome as the build's. This is how a capability's bake becomes a
|
|
39
|
+
* build without the engine knowing what a bake is. */
|
|
40
|
+
readonly build?: { readonly tool: string; readonly input: (configuration: T) => unknown };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* What a CONTRIBUTION exports to register a kind — a plain object, so a
|
|
45
|
+
* project module (evaluated with the project's own copy of the engine)
|
|
46
|
+
* hands it to the HOST, which registers it in the host's registry. A module
|
|
47
|
+
* `src/contributions/<name>.kind.ts` exports it as `kind`.
|
|
48
|
+
*/
|
|
49
|
+
export type ConfigurationKindContribution = ConfigurationKindRegistration;
|
|
50
|
+
|
|
51
|
+
const CONFIGURATION_ROLES: readonly ConfigurationRole[] = ['run', 'build'];
|
|
52
|
+
|
|
53
|
+
/** Register a contribution's kind after checking its shape by name. */
|
|
54
|
+
export function registerContributedConfigurationKind(value: unknown, source: string): () => void {
|
|
55
|
+
const record = (value ?? {}) as Partial<ConfigurationKindContribution>;
|
|
56
|
+
const problems: string[] = [];
|
|
57
|
+
if (typeof record.kind !== 'string' || record.kind.length === 0)
|
|
58
|
+
problems.push('`kind` must be a non-empty string');
|
|
59
|
+
if (!CONFIGURATION_ROLES.includes(record.role as ConfigurationRole))
|
|
60
|
+
problems.push("`role` must be 'run' or 'build'");
|
|
61
|
+
if (typeof record.describe !== 'string') problems.push('`describe` must be a string');
|
|
62
|
+
const schema = record.schema as { safeParse?: unknown } | undefined;
|
|
63
|
+
if (!schema || typeof schema.safeParse !== 'function')
|
|
64
|
+
problems.push('`schema` must be a Zod schema');
|
|
65
|
+
const build = record.build as { tool?: unknown; input?: unknown } | undefined;
|
|
66
|
+
if (
|
|
67
|
+
build !== undefined &&
|
|
68
|
+
(typeof build.tool !== 'string' || typeof build.input !== 'function')
|
|
69
|
+
) {
|
|
70
|
+
problems.push('`build` must be `{ tool: string; input(configuration) }`');
|
|
71
|
+
}
|
|
72
|
+
if (problems.length > 0) {
|
|
73
|
+
throw new Error(`${source}: not a configuration kind — ${problems.join('; ')}`);
|
|
74
|
+
}
|
|
75
|
+
return registerConfigurationKind(record as ConfigurationKindContribution);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const registry = new Map<string, ConfigurationKindRegistration>();
|
|
79
|
+
|
|
80
|
+
/** Register a kind. A second registration replaces the first (a module
|
|
81
|
+
* re-evaluated on save); never throws. */
|
|
82
|
+
export function registerConfigurationKind<T extends ConfigurationEnvelope>(
|
|
83
|
+
registration: ConfigurationKindRegistration<T>,
|
|
84
|
+
): () => void {
|
|
85
|
+
const entry = registration as unknown as ConfigurationKindRegistration;
|
|
86
|
+
registry.set(registration.kind, entry);
|
|
87
|
+
return () => {
|
|
88
|
+
if (registry.get(registration.kind) === entry) registry.delete(registration.kind);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function registeredConfigurationKinds(): readonly string[] {
|
|
93
|
+
return [...registry.keys()];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function configurationKind(kind: string): ConfigurationKindRegistration | undefined {
|
|
97
|
+
return registry.get(kind);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The id of the implicit configuration: mount every root in the host. */
|
|
101
|
+
export const PLAY_CONFIGURATION_ID = 'play';
|
|
102
|
+
|
|
103
|
+
/** A process the project runs beside the host: an entry module started
|
|
104
|
+
* with the project's own toolchain, listening on a port when it has one. */
|
|
105
|
+
export interface ProcessConfiguration extends ConfigurationEnvelope {
|
|
106
|
+
readonly kind: 'process';
|
|
107
|
+
/** Project-relative entry module (`server/main.ts`). */
|
|
108
|
+
readonly entry: string;
|
|
109
|
+
/** The command that starts it, run in a shell at `cwd`; `npx tsx <entry>`
|
|
110
|
+
* when omitted. Declare it when the entry needs its own flags. */
|
|
111
|
+
readonly command?: string;
|
|
112
|
+
/** The port it listens on, when readiness means a port. */
|
|
113
|
+
readonly port?: number;
|
|
114
|
+
/** Project-relative working directory; the project root when omitted. */
|
|
115
|
+
readonly cwd?: string;
|
|
116
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
117
|
+
/** How a runner knows it is up: the port accepting connections (the
|
|
118
|
+
* default when `port` is set), or an HTTP path answering 2xx on it. */
|
|
119
|
+
readonly ready?: { readonly http?: string };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Several configurations started together, `play` included by name. */
|
|
123
|
+
export interface CompoundConfiguration extends ConfigurationEnvelope {
|
|
124
|
+
readonly kind: 'compound';
|
|
125
|
+
readonly run: readonly string[];
|
|
126
|
+
/** How many instances of the host mount to run against the rest (the
|
|
127
|
+
* editor's Instances picker is this parameter). */
|
|
128
|
+
readonly instances?: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A packaged build of the project's own web bundle: the project's build
|
|
132
|
+
* script, then `dist/` packaged as the web artifact. The first build-role
|
|
133
|
+
* kind; a capability registers more (a bake, a static site). */
|
|
134
|
+
export interface BundleConfiguration extends ConfigurationEnvelope {
|
|
135
|
+
readonly kind: 'bundle';
|
|
136
|
+
/** The package.json script that builds (`build`). */
|
|
137
|
+
readonly script?: string;
|
|
138
|
+
/** Extra arguments after `--` (`['--base', './']` for an itch-style relative bundle). */
|
|
139
|
+
readonly args?: readonly string[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export type DeclaredConfiguration = ConfigurationEnvelope & Readonly<Record<string, unknown>>;
|
|
143
|
+
|
|
144
|
+
registerConfigurationKind<ProcessConfiguration>({
|
|
145
|
+
kind: 'process',
|
|
146
|
+
role: 'run',
|
|
147
|
+
describe: 'A process beside the host: an entry module, a port, readiness',
|
|
148
|
+
schema: z
|
|
149
|
+
.object({
|
|
150
|
+
id: z.string().min(1).describe('Configuration id'),
|
|
151
|
+
kind: z.literal('process'),
|
|
152
|
+
entry: z.string().min(1).describe('Project-relative entry module'),
|
|
153
|
+
command: z
|
|
154
|
+
.string()
|
|
155
|
+
.min(1)
|
|
156
|
+
.optional()
|
|
157
|
+
.describe('Shell command that starts it; `npx tsx <entry>` when omitted'),
|
|
158
|
+
port: z.number().int().min(1).max(65535).optional().describe('Port it listens on'),
|
|
159
|
+
cwd: z.string().min(1).optional().describe('Project-relative working directory'),
|
|
160
|
+
env: z.record(z.string(), z.string()).optional().describe('Environment for the process'),
|
|
161
|
+
ready: z
|
|
162
|
+
.object({ http: z.string().min(1).optional().describe('HTTP path answering 2xx when up') })
|
|
163
|
+
.strict()
|
|
164
|
+
.optional()
|
|
165
|
+
.describe('Readiness beyond the port accepting connections'),
|
|
166
|
+
})
|
|
167
|
+
.strict() as unknown as ZodType<ProcessConfiguration>,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
registerConfigurationKind<CompoundConfiguration>({
|
|
171
|
+
kind: 'compound',
|
|
172
|
+
role: 'run',
|
|
173
|
+
describe: 'Several configurations started together; `play` names the host mount',
|
|
174
|
+
schema: z
|
|
175
|
+
.object({
|
|
176
|
+
id: z.string().min(1).describe('Configuration id'),
|
|
177
|
+
kind: z.literal('compound'),
|
|
178
|
+
run: z.array(z.string().min(1)).min(1).describe('Configuration ids, `play` included'),
|
|
179
|
+
instances: z
|
|
180
|
+
.number()
|
|
181
|
+
.int()
|
|
182
|
+
.min(1)
|
|
183
|
+
.optional()
|
|
184
|
+
.describe('Instances of the host mount to run against the rest'),
|
|
185
|
+
})
|
|
186
|
+
.strict() as unknown as ZodType<CompoundConfiguration>,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
registerConfigurationKind<BundleConfiguration>({
|
|
190
|
+
kind: 'bundle',
|
|
191
|
+
role: 'build',
|
|
192
|
+
describe: "The project's web bundle: its build script, then dist/ packaged as the artifact",
|
|
193
|
+
schema: z
|
|
194
|
+
.object({
|
|
195
|
+
id: z.string().min(1).describe('Configuration id'),
|
|
196
|
+
kind: z.literal('bundle'),
|
|
197
|
+
script: z.string().min(1).optional().describe('package.json script that builds (`build`)'),
|
|
198
|
+
args: z.array(z.string()).optional().describe('Arguments passed after `--`'),
|
|
199
|
+
})
|
|
200
|
+
.strict() as unknown as ZodType<BundleConfiguration>,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
/** Validate `configurations[]` against the registry: every kind registered, every
|
|
204
|
+
* declaration its kind's shape, ids unique, compound members declared. */
|
|
205
|
+
export function configurationIssues(configurations: readonly ConfigurationEnvelope[]): string[] {
|
|
206
|
+
const issues: string[] = [];
|
|
207
|
+
const ids = new Set<string>();
|
|
208
|
+
for (const declaration of configurations) {
|
|
209
|
+
if (ids.has(declaration.id)) issues.push(`configurations: duplicate id "${declaration.id}"`);
|
|
210
|
+
ids.add(declaration.id);
|
|
211
|
+
const registration = registry.get(declaration.kind);
|
|
212
|
+
if (!registration) {
|
|
213
|
+
issues.push(
|
|
214
|
+
`configurations "${declaration.id}": unknown kind "${declaration.kind}" — registered: ${registeredConfigurationKinds()
|
|
215
|
+
.map((k) => `"${k}"`)
|
|
216
|
+
.join(', ')}`,
|
|
217
|
+
);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const parsed = registration.schema.safeParse(declaration);
|
|
221
|
+
if (!parsed.success) {
|
|
222
|
+
for (const issue of parsed.error.issues) {
|
|
223
|
+
issues.push(
|
|
224
|
+
`configurations "${declaration.id}" (${declaration.kind}): ${issue.path.join('.') || '(declaration)'}: ${issue.message}`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
for (const declaration of configurations) {
|
|
230
|
+
if (declaration.kind !== 'compound') continue;
|
|
231
|
+
const members = (declaration as { run?: readonly string[] }).run ?? [];
|
|
232
|
+
for (const member of members) {
|
|
233
|
+
if (member !== PLAY_CONFIGURATION_ID && !ids.has(member)) {
|
|
234
|
+
issues.push(
|
|
235
|
+
`configurations "${declaration.id}": member "${member}" is not a declared configuration (or "${PLAY_CONFIGURATION_ID}")`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return issues;
|
|
241
|
+
}
|
package/src/manifest/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Game manifest (vgai.project.json) — schema + loader (T3.1 slice
|
|
2
2
|
// 1)..
|
|
3
3
|
|
|
4
|
+
export * from './configuration-kinds';
|
|
4
5
|
export {
|
|
5
6
|
isManifestFilename,
|
|
6
7
|
MANIFEST_FILENAME,
|
|
@@ -8,11 +9,11 @@ export {
|
|
|
8
9
|
removedManifestFilenameMessage,
|
|
9
10
|
} from './filename';
|
|
10
11
|
export type { ResolvedAdapter, ResolvedAdapterRoot, ResolvedGameManifest } from './load';
|
|
11
|
-
export {
|
|
12
|
+
export { loadGameManifest } from './load';
|
|
12
13
|
export type { AdapterRoot, GameManifest, RootAdapter } from './schema';
|
|
13
14
|
export { AdapterRootSchema, GameManifestSchema, RootAdapterSchema } from './schema';
|
|
14
15
|
|
|
15
|
-
//
|
|
16
|
+
// File loading and kind-module discovery read `node:fs` and are
|
|
16
17
|
// deliberately NOT re-exported here — this barrel must stay import-safe for
|
|
17
18
|
// browser bundles. Import it directly from `@engine/manifest/load-file` in
|
|
18
|
-
// Node-only contexts
|
|
19
|
+
// Node-only contexts; kind discovery lives in `@engine/manifest/kind-modules`.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a project's CONFIGURATION-KIND CONTRIBUTIONS live and how a host
|
|
3
|
+
* takes them in (ARCHITECTURE-CORE §The project model). A module named
|
|
4
|
+
* `src/contributions/<name>.kind.ts` (or `.tsx`) exports `kind`, a plain
|
|
5
|
+
* `ConfigurationKindContribution`; the HOST that loads the manifest — the
|
|
6
|
+
* editor server through Vite's SSR loader, a project's own validate script
|
|
7
|
+
* through tsx — imports each and registers it in its own registry through
|
|
8
|
+
* {@link registerContributedConfigurationKind}. Node-only: it walks the
|
|
9
|
+
* project folder.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { registerContributedConfigurationKind } from './configuration-kinds';
|
|
15
|
+
|
|
16
|
+
const KIND_SUFFIXES = ['.kind.ts', '.kind.tsx'] as const;
|
|
17
|
+
|
|
18
|
+
/** Whether one file name is a kind contribution by the convention. */
|
|
19
|
+
export function isConfigurationKindModule(fileName: string): boolean {
|
|
20
|
+
return KIND_SUFFIXES.some((suffix) => fileName.endsWith(suffix));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Absolute paths of the project's kind contributions, sorted. */
|
|
24
|
+
export function contributedKindModulePaths(projectDir: string): string[] {
|
|
25
|
+
const found: string[] = [];
|
|
26
|
+
const walk = (directory: string): void => {
|
|
27
|
+
let names: string[];
|
|
28
|
+
try {
|
|
29
|
+
names = readdirSync(directory);
|
|
30
|
+
} catch {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
for (const name of names) {
|
|
34
|
+
if (name.startsWith('.') || name === 'node_modules') continue;
|
|
35
|
+
const absolute = join(directory, name);
|
|
36
|
+
let isDirectory = false;
|
|
37
|
+
try {
|
|
38
|
+
isDirectory = statSync(absolute).isDirectory();
|
|
39
|
+
} catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (isDirectory) walk(absolute);
|
|
43
|
+
else if (isConfigurationKindModule(name)) found.push(absolute);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
walk(join(projectDir, 'src', 'contributions'));
|
|
47
|
+
return found.sort();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Register the `kind` export of an already-imported module; the source
|
|
51
|
+
* path names the module in a refusal. */
|
|
52
|
+
export function registerKindModule(module: unknown, source: string): () => void {
|
|
53
|
+
const exported = (module as { kind?: unknown } | null)?.kind;
|
|
54
|
+
if (exported === undefined) {
|
|
55
|
+
throw new Error(`${source}: a kind contribution must export \`kind\``);
|
|
56
|
+
}
|
|
57
|
+
return registerContributedConfigurationKind(exported, source);
|
|
58
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// bundle.
|
|
8
8
|
|
|
9
9
|
import { readFileSync } from 'node:fs';
|
|
10
|
-
import { loadGameManifest, type ResolvedGameManifest } from './load';
|
|
10
|
+
import { type LoadGameManifestOptions, loadGameManifest, type ResolvedGameManifest } from './load';
|
|
11
11
|
import { resolveManifestPath } from './locate';
|
|
12
12
|
|
|
13
13
|
// The dual-name filesystem lookup lives in `./locate` (Zod-free on purpose);
|
|
@@ -16,12 +16,18 @@ import { resolveManifestPath } from './locate';
|
|
|
16
16
|
export { hasManifest, manifestWritePath, resolveManifestPath } from './locate';
|
|
17
17
|
|
|
18
18
|
/** Read + JSON.parse `path`, then resolve it via `loadGameManifest`. Node-only. */
|
|
19
|
-
export function loadGameManifestFile(
|
|
19
|
+
export function loadGameManifestFile(
|
|
20
|
+
path: string,
|
|
21
|
+
options: LoadGameManifestOptions = {},
|
|
22
|
+
): ResolvedGameManifest {
|
|
20
23
|
const raw: unknown = JSON.parse(readFileSync(path, 'utf-8'));
|
|
21
|
-
return loadGameManifest(raw);
|
|
24
|
+
return loadGameManifest(raw, options);
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
/** {@link loadGameManifestFile} against `dir`'s `vgai.project.json`. */
|
|
25
|
-
export function loadGameManifestDir(
|
|
26
|
-
|
|
28
|
+
export function loadGameManifestDir(
|
|
29
|
+
dir: string,
|
|
30
|
+
options: LoadGameManifestOptions = {},
|
|
31
|
+
): ResolvedGameManifest {
|
|
32
|
+
return loadGameManifestFile(resolveManifestPath(dir), options);
|
|
27
33
|
}
|
package/src/manifest/load.ts
CHANGED
|
@@ -10,9 +10,15 @@
|
|
|
10
10
|
// `packages/engine/test/schema-consumption-map.ts`'s `manifest.*` entries).
|
|
11
11
|
//
|
|
12
12
|
// No `fs` import here on purpose — `load-file.ts` is the thin Node-only
|
|
13
|
+
|
|
13
14
|
// wrapper that reads a path and calls this function, so browser bundles that
|
|
14
15
|
// only need `loadGameManifest` never pull in `node:fs`.
|
|
15
16
|
|
|
17
|
+
import {
|
|
18
|
+
configurationIssues,
|
|
19
|
+
configurationKind,
|
|
20
|
+
type DeclaredConfiguration,
|
|
21
|
+
} from './configuration-kinds';
|
|
16
22
|
import {
|
|
17
23
|
type AdapterRoot,
|
|
18
24
|
type GameManifest,
|
|
@@ -90,16 +96,15 @@ export interface ResolvedGameManifest {
|
|
|
90
96
|
readonly engine: { readonly version: string };
|
|
91
97
|
/** Sorted by zOrder; ties broken by original array order (§3). */
|
|
92
98
|
readonly roots: readonly ResolvedAdapterRoot[];
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
| undefined;
|
|
99
|
+
/** The declared run configurations, validated by kind (`run-kinds.ts`). */
|
|
100
|
+
readonly configurations: readonly DeclaredConfiguration[];
|
|
101
|
+
/** Ids whose kind no registry here knows — only ever non-empty when the
|
|
102
|
+
* caller loaded with `configurationKinds: 'defer'` (a bundled host that
|
|
103
|
+
* cannot evaluate a project's kind contributions); the editor session
|
|
104
|
+
* is the validator for those. */
|
|
105
|
+
readonly deferredConfigurationKinds: readonly string[];
|
|
106
|
+
/** Deploy-baked relay credentials, when the served manifest carries them. */
|
|
107
|
+
readonly relay: GameManifest['relay'];
|
|
103
108
|
readonly resolution: { readonly width: number; readonly height: number } | undefined;
|
|
104
109
|
/** Construction-time renderer properties no world can declare for itself — see the schema. */
|
|
105
110
|
readonly rendering: { readonly antialias: boolean } | undefined;
|
|
@@ -148,9 +153,6 @@ export interface ResolvedGameManifest {
|
|
|
148
153
|
readonly learn: LearnMetadata | undefined;
|
|
149
154
|
}
|
|
150
155
|
|
|
151
|
-
/** Default relative path for `server.module` when omitted (§3) — the scaffolded Colyseus bootstrap. */
|
|
152
|
-
export const DEFAULT_SERVER_MODULE = 'server/colyseus-setup.ts';
|
|
153
|
-
|
|
154
156
|
const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
|
|
155
157
|
|
|
156
158
|
// ---------------------------------------------------------------------------
|
|
@@ -288,7 +290,16 @@ function resolveRoot(root: AdapterRoot): ResolvedAdapterRoot {
|
|
|
288
290
|
* wrapper. Throws a descriptive `Error` (naming the offending root/field) on
|
|
289
291
|
* any Zod validation failure or cross-field rule violation.
|
|
290
292
|
*/
|
|
291
|
-
export
|
|
293
|
+
export interface LoadGameManifestOptions {
|
|
294
|
+
/** `require` (default): a declaration of an unregistered kind refuses.
|
|
295
|
+
* `defer`: it is reported in `deferredConfigurationKinds` instead. */
|
|
296
|
+
readonly configurationKinds?: 'require' | 'defer';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function loadGameManifest(
|
|
300
|
+
raw: unknown,
|
|
301
|
+
options: LoadGameManifestOptions = {},
|
|
302
|
+
): ResolvedGameManifest {
|
|
292
303
|
// Removed-format guard FIRST, on the raw value: a root declaring a removed
|
|
293
304
|
// content field would otherwise fail Zod's `roots` discriminated union with
|
|
294
305
|
// a confusing shape error instead of the named migration message.
|
|
@@ -325,13 +336,31 @@ export function loadGameManifest(raw: unknown): ResolvedGameManifest {
|
|
|
325
336
|
|
|
326
337
|
const roots = resolvedWithIndex.map(({ root }) => root);
|
|
327
338
|
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
339
|
+
const configurations = (manifest.configurations ?? []) as DeclaredConfiguration[];
|
|
340
|
+
// Kinds are OPEN (ARCHITECTURE-CORE §The project model): each declaration
|
|
341
|
+
// is checked against its registered kind's own schema HERE, where the
|
|
342
|
+
// host's registry is, not in the Zod schema. A host that cannot evaluate a
|
|
343
|
+
// project's kind contributions defers unknown kinds by name instead of
|
|
344
|
+
// refusing them.
|
|
345
|
+
const deferredConfigurationKinds: string[] = [];
|
|
346
|
+
const kindIssues = configurationIssues(configurations).filter((issue) => {
|
|
347
|
+
if (options.configurationKinds !== 'defer') return true;
|
|
348
|
+
const unknown = /^configurations "([^"]+)": unknown kind/.exec(issue);
|
|
349
|
+
if (!unknown) return true;
|
|
350
|
+
deferredConfigurationKinds.push(unknown[1] as string);
|
|
351
|
+
return false;
|
|
352
|
+
});
|
|
353
|
+
if (kindIssues.length > 0) {
|
|
354
|
+
throw new Error(`Game manifest: ${kindIssues.join('\n ')}`);
|
|
355
|
+
}
|
|
356
|
+
for (const configuration of configurations) {
|
|
357
|
+
if (
|
|
358
|
+
!configurationKind(configuration.kind) &&
|
|
359
|
+
!deferredConfigurationKinds.includes(configuration.id)
|
|
360
|
+
) {
|
|
361
|
+
deferredConfigurationKinds.push(configuration.id);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
335
364
|
|
|
336
365
|
return {
|
|
337
366
|
manifestVersion: manifest.manifestVersion,
|
|
@@ -340,7 +369,9 @@ export function loadGameManifest(raw: unknown): ResolvedGameManifest {
|
|
|
340
369
|
version: manifest.version,
|
|
341
370
|
engine: { version: manifest.engine.version },
|
|
342
371
|
roots,
|
|
343
|
-
|
|
372
|
+
configurations,
|
|
373
|
+
deferredConfigurationKinds,
|
|
374
|
+
relay: manifest.relay,
|
|
344
375
|
resolution: manifest.resolution,
|
|
345
376
|
rendering: manifest.rendering,
|
|
346
377
|
authoring: manifest.authoring,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE RUNTIME HALF OF A PROJECT'S ENVIRONMENT (ARCHITECTURE-CORE §The
|
|
3
|
+
* project model, "environment"): the Node version a project's processes need
|
|
4
|
+
* is declared ONCE, where the ecosystem already declares it —
|
|
5
|
+
* `package.json` `engines.node` — and read here by whatever starts a
|
|
6
|
+
* `process` configuration (the editor server's configurations route, the
|
|
7
|
+
* CLI's autoboot). The manifest carries no `environment` key: a second
|
|
8
|
+
* declaration of the same fact is the drift a single home prevents.
|
|
9
|
+
*
|
|
10
|
+
* Node-only (reads the project folder), like `locate.ts`.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import semver from 'semver';
|
|
15
|
+
|
|
16
|
+
/** `engines.node` from the project's `package.json`, or `null` when the
|
|
17
|
+
* project declares none (or has no package.json). */
|
|
18
|
+
export function declaredNodeRange(projectDir: string): string | null {
|
|
19
|
+
try {
|
|
20
|
+
const raw: unknown = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf-8'));
|
|
21
|
+
const engines = (raw as { engines?: { node?: unknown } } | null)?.engines;
|
|
22
|
+
return typeof engines?.node === 'string' && engines.node.trim() ? engines.node.trim() : null;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Why a process of this project cannot start on the running Node, spelled
|
|
30
|
+
* for the person who will fix it — or `null` when it can. A range that is
|
|
31
|
+
* not valid semver is itself the issue: a declaration nobody can satisfy is
|
|
32
|
+
* named, never skipped.
|
|
33
|
+
*/
|
|
34
|
+
export function nodeRuntimeIssue(
|
|
35
|
+
projectDir: string,
|
|
36
|
+
running: string = process.versions.node,
|
|
37
|
+
): string | null {
|
|
38
|
+
const range = declaredNodeRange(projectDir);
|
|
39
|
+
if (range === null) return null;
|
|
40
|
+
if (semver.validRange(range) === null) {
|
|
41
|
+
return `package.json declares engines.node "${range}", which is not a semver range`;
|
|
42
|
+
}
|
|
43
|
+
if (semver.satisfies(running, range, { includePrerelease: true })) return null;
|
|
44
|
+
return (
|
|
45
|
+
`package.json declares engines.node "${range}" and this host runs Node ${running} — ` +
|
|
46
|
+
`run the editor and \`vgai\` on a Node that satisfies it, or change engines.node`
|
|
47
|
+
);
|
|
48
|
+
}
|
package/src/manifest/schema.ts
CHANGED
|
@@ -398,6 +398,14 @@ export type LearnMetadata = z.infer<typeof LearnMetadataSchema>;
|
|
|
398
398
|
// Game manifest (vgai.project.json)
|
|
399
399
|
// ---------------------------------------------------------------------------
|
|
400
400
|
|
|
401
|
+
/** The envelope of one run configuration; the kind's schema takes the rest. */
|
|
402
|
+
const ConfigurationEnvelopeSchema = z
|
|
403
|
+
.object({
|
|
404
|
+
id: z.string().min(1).describe('Configuration id, unique in the manifest'),
|
|
405
|
+
kind: z.string().min(1).describe('A registered run kind'),
|
|
406
|
+
})
|
|
407
|
+
.passthrough();
|
|
408
|
+
|
|
401
409
|
export const GameManifestSchema = z
|
|
402
410
|
.object({
|
|
403
411
|
$schema: z
|
|
@@ -426,7 +434,6 @@ export const GameManifestSchema = z
|
|
|
426
434
|
.describe('Engine version pin'),
|
|
427
435
|
roots: z
|
|
428
436
|
.array(AdapterRootSchema)
|
|
429
|
-
.min(1)
|
|
430
437
|
.superRefine((roots, ctx) => {
|
|
431
438
|
// D-V6 (debt): root ids are described as unique above but that was
|
|
432
439
|
// describe-only at the SCHEMA level — only load.ts's loader-level
|
|
@@ -482,7 +489,7 @@ export const GameManifestSchema = z
|
|
|
482
489
|
}
|
|
483
490
|
})
|
|
484
491
|
.describe(
|
|
485
|
-
"The
|
|
492
|
+
"The project's explicit adapter-root composition — ZERO OR MORE roots, and no implicit root: a project with no roots mounts nothing (a library of models, a folder of documents) and the editor derives its chrome from what IS declared. " +
|
|
486
493
|
'Root ids are unique, and a project declares at most ONE world root per medium — ' +
|
|
487
494
|
'one `three`, one `dom`, one canvas-surface root (first-party canvas, canvas ingest ' +
|
|
488
495
|
'and canvas module are the same medium).',
|
|
@@ -518,52 +525,51 @@ export const GameManifestSchema = z
|
|
|
518
525
|
.strict()
|
|
519
526
|
.optional()
|
|
520
527
|
.describe('Editor-only authoring metadata; never runtime ownership'),
|
|
521
|
-
|
|
528
|
+
configurations: z
|
|
529
|
+
.array(ConfigurationEnvelopeSchema)
|
|
530
|
+
.optional()
|
|
531
|
+
.describe(
|
|
532
|
+
"The project's run configurations — its ENTRYPOINTS beyond the host mount (`play`): each an " +
|
|
533
|
+
'id and a registered kind (`process`, `compound`, and whatever a capability registers) with ' +
|
|
534
|
+
"that kind's own fields. The editor's transport, `vgai run <id>` and a harness start the same declaration.",
|
|
535
|
+
),
|
|
536
|
+
relay: z
|
|
522
537
|
.object({
|
|
523
|
-
|
|
524
|
-
endpoint: z
|
|
538
|
+
signalingUrl: z
|
|
525
539
|
.string()
|
|
526
540
|
.optional()
|
|
527
541
|
.describe(
|
|
528
|
-
'
|
|
529
|
-
'Omit to declare the scaffold default, `ws://<the page host>:2567` — the port ' +
|
|
530
|
-
"`server/colyseus-setup.ts` listens on. Stated here because the endpoint is the game's " +
|
|
531
|
-
'own deployment fact, not something the editor may guess',
|
|
542
|
+
'Managed relay signaling URL to prefer over the client default; omitted when the deploy did not pin one',
|
|
532
543
|
),
|
|
533
|
-
|
|
544
|
+
grant: z
|
|
534
545
|
.string()
|
|
535
546
|
.optional()
|
|
536
547
|
.describe(
|
|
537
|
-
'
|
|
548
|
+
'Room-scoped managed-relay GRANT minted at `vgai deploy`; players exchange it for a fresh token at `sessionUrl`',
|
|
538
549
|
),
|
|
539
|
-
|
|
540
|
-
.
|
|
541
|
-
signalingUrl: z
|
|
542
|
-
.string()
|
|
543
|
-
.optional()
|
|
544
|
-
.describe(
|
|
545
|
-
'Managed relay signaling URL to prefer over the client default; omitted when the deploy did not pin one',
|
|
546
|
-
),
|
|
547
|
-
accessToken: z
|
|
548
|
-
.string()
|
|
549
|
-
.describe(
|
|
550
|
-
'Room-scoped managed-relay access token minted at `vgai deploy` and read by the standalone client (main.ts)',
|
|
551
|
-
),
|
|
552
|
-
expiresAt: z
|
|
553
|
-
.number()
|
|
554
|
-
.describe(
|
|
555
|
-
'Token expiry as epoch milliseconds; the client falls back to a tokenless join once elapsed',
|
|
556
|
-
),
|
|
557
|
-
})
|
|
558
|
-
.strict()
|
|
550
|
+
sessionUrl: z
|
|
551
|
+
.string()
|
|
559
552
|
.optional()
|
|
560
553
|
.describe(
|
|
561
|
-
'
|
|
554
|
+
'Public session endpoint where a deployed client exchanges the grant for a short-lived relay token',
|
|
555
|
+
),
|
|
556
|
+
accessToken: z
|
|
557
|
+
.string()
|
|
558
|
+
.optional()
|
|
559
|
+
.describe('A baked relay token (older deploys); used directly while unexpired'),
|
|
560
|
+
expiresAt: z
|
|
561
|
+
.number()
|
|
562
|
+
.optional()
|
|
563
|
+
.describe(
|
|
564
|
+
'Token expiry as epoch milliseconds; the client falls back to a tokenless join once elapsed',
|
|
562
565
|
),
|
|
563
566
|
})
|
|
564
567
|
.strict()
|
|
565
568
|
.optional()
|
|
566
|
-
.describe(
|
|
569
|
+
.describe(
|
|
570
|
+
'Deploy-baked managed-relay credentials — a DEPLOYMENT fact. NEVER hand-authored in source: written only into ' +
|
|
571
|
+
'the staged manifest by `vgai deploy` and read by the deployed client at connect time.',
|
|
572
|
+
),
|
|
567
573
|
resolution: z
|
|
568
574
|
.object({
|
|
569
575
|
width: z.number().describe('Canvas width in pixels'),
|
package/src/react/use-data.ts
CHANGED
|
@@ -57,7 +57,7 @@ export function useData<T>(handle: DataHandle<T>): T {
|
|
|
57
57
|
* runtime state belongs in your game's own store, never written back into
|
|
58
58
|
* data files. Outside a running editor there is no `/__editor` server, so
|
|
59
59
|
* this rejects with a teaching error (a shipped game could never reach the
|
|
60
|
-
* route anyway — builds strip `src/tools/` entirely, §4).
|
|
60
|
+
* route anyway — builds strip `src/contributions/` and `src/tools/` entirely, §4).
|
|
61
61
|
*
|
|
62
62
|
* The body is PARSED VALUES, never serialized text: the dev server's fold
|
|
63
63
|
* (`packages/editor/server/data-file-serialize.ts`) owns key order and the
|