@vgai/engine 0.5.2 → 0.5.4
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/README.md +18 -11
- package/package.json +10 -6
- package/schemas/engine-api.json +1 -68
- package/schemas/engine-api.md +1 -32
- package/schemas/engine-capabilities.json +30 -42
- package/schemas/{vgai-game.schema.json → vgai-project.schema.json} +52 -34
- package/src/adapter/adapter-surface.ts +5 -5
- package/src/adapter/authoring.ts +168 -226
- package/src/adapter/colyseus-networking-adapter.ts +44 -5
- package/src/adapter/first-party-systems.ts +156 -42
- package/src/adapter/host-context.ts +177 -46
- package/src/adapter/index.ts +47 -51
- package/src/adapter/ingest/game-contract.ts +2 -2
- package/src/adapter/ingest/scene-capture.ts +18 -19
- package/src/adapter/ingest/structural-ids.ts +127 -0
- package/src/adapter/ingest/upstream-pin.ts +9 -12
- package/src/adapter/loop-gate-report.ts +11 -11
- package/src/adapter/rapier-physics-adapter.ts +27 -9
- package/src/adapter/root-adapter.ts +217 -0
- package/src/adapter/{vgai-scene-game-adapter.ts → setup-three-root-adapter.ts} +173 -351
- package/src/adapter/system-adapter.ts +80 -63
- package/src/ai/navigation.ts +1 -1
- package/src/animation/animation-clock.ts +1 -1
- package/src/animation/camera-ownership.ts +1 -2
- package/src/animation/cubic-spline-interpolant.ts +132 -0
- package/src/animation/theatre-clock-binding.ts +2 -2
- package/src/animation/theatre-object-binding.ts +4 -4
- package/src/animation/xstate-animation-binding.ts +75 -5
- package/src/{scene/schema → asset-formats}/camera.ts +2 -5
- package/src/{scene/schema → asset-formats}/collider.ts +2 -5
- package/src/asset-formats/index.ts +54 -0
- package/src/{scene/schema → asset-formats}/instances.ts +10 -6
- package/src/{scene/schema → asset-formats}/light.ts +3 -6
- package/src/{scene/schema → asset-formats}/material.ts +4 -7
- package/src/{scene/schema → asset-formats}/mesh.ts +4 -7
- package/src/asset-formats/parse.ts +39 -0
- package/src/{scene/schema → asset-formats}/particles.ts +3 -6
- package/src/{scene/schema/environment.ts → asset-formats/render-env.ts} +23 -86
- package/src/{scene/schema → asset-formats}/tuples.ts +1 -1
- package/src/{scene/asset-loaders.ts → asset-loaders.ts} +8 -9
- package/src/asset-parse-error.ts +33 -0
- package/src/{scene/asset-registry.ts → asset-registry.ts} +1 -1
- package/src/assets.ts +1 -1
- package/src/audio/wav-encode.ts +9 -9
- package/src/canvas-react/engine-bridge.ts +59 -0
- package/src/canvas-react/index.ts +50 -0
- package/src/canvas-react/pixi-primitive.tsx +202 -0
- package/src/canvas-react/pixi-react-adapter.tsx +290 -0
- package/src/canvas-react/pixi-react-root-factory.tsx +88 -0
- package/src/canvas-react/world-context.ts +328 -0
- package/src/core/frame-pacing.ts +100 -0
- package/src/core/game-loop.ts +50 -28
- package/src/core/seeded-random.ts +7 -7
- package/src/core/sim-clock.ts +388 -0
- package/src/core/system-runner.ts +17 -63
- package/src/core/types.ts +34 -15
- package/src/data/data-asset.ts +3 -3
- package/src/data/data-check-core.ts +6 -7
- package/src/data/data-ref.ts +11 -11
- package/src/data/vite-plugin-data.ts +10 -10
- package/src/{scene/defaults.ts → defaults.ts} +18 -40
- package/src/dev/render-debug-adapter.ts +1 -1
- package/src/dev/webgl-frame-capture.ts +1 -1
- package/src/ecs/scene-index.ts +439 -0
- package/src/ecs/scene-query.ts +43 -0
- package/src/{scene → ecs}/user-data.ts +17 -36
- package/src/index.ts +7 -9
- package/src/input/input-manager.ts +29 -32
- package/src/input/input-types.ts +2 -2
- package/src/input/schema.ts +5 -5
- package/src/loader.ts +57 -0
- package/src/manifest/editor-port.ts +69 -0
- package/src/manifest/filename.ts +49 -0
- package/src/manifest/index.ts +8 -2
- package/src/manifest/load-file.ts +11 -0
- package/src/manifest/load.ts +65 -77
- package/src/manifest/locate.ts +55 -0
- package/src/manifest/schema.ts +400 -233
- package/src/{scene → physics}/collider-dimensions.ts +3 -3
- package/src/physics/physics-registry.ts +1 -1
- package/src/{world2d/authoring-2d.ts → pixi/authoring.ts} +24 -11
- package/src/pixi/index.ts +43 -0
- package/src/{world2d/ingest-iframe-2d.ts → pixi/ingest-iframe.ts} +9 -9
- package/src/{world2d/ingest2d.ts → pixi/ingest.ts} +28 -28
- package/src/{world2d/physics2d-registry.ts → pixi/physics-registry.ts} +1 -1
- package/src/{world2d/scene-capture-2d.ts → pixi/scene-capture.ts} +3 -3
- package/src/{world2d/system-adapters-2d.ts → pixi/system-adapters.ts} +2 -2
- package/src/react/unmanaged-root-detector.ts +26 -1
- package/src/react/use-data.ts +5 -5
- package/src/react/use-selection.tsx +15 -42
- package/src/react/{game-state.tsx → world-state.tsx} +44 -46
- package/src/render/auto-batcher.ts +1 -2
- package/src/{scene → render}/instance-mesh.ts +1 -1
- package/src/{scene → render}/light-camera-factory.ts +14 -13
- package/src/render/lod.ts +17 -0
- package/src/{scene → render}/material-factory.ts +8 -7
- package/src/{scene → render}/particles-factory.ts +62 -12
- package/src/render/render-batch-system.ts +14 -41
- package/src/render/render-features.ts +1 -1
- package/src/render/render-settings.ts +1 -2
- package/src/render/spark-renderer-lifecycle.ts +1 -1
- package/src/runtime/create-runtime.ts +352 -463
- package/src/runtime/debug-bridge.ts +148 -93
- package/src/runtime/debug-registry.ts +79 -54
- package/src/runtime/dev-layers.ts +40 -0
- package/src/runtime/frame-selector-cache.ts +4 -4
- package/src/runtime/game.ts +618 -369
- package/src/runtime/gameplay-rng-trap.ts +6 -7
- package/src/runtime/input-router.ts +11 -11
- package/src/runtime/mount-game.ts +54 -55
- package/src/runtime/mount-manifest.ts +154 -150
- package/src/runtime/presentation.ts +141 -0
- package/src/runtime/render-audio-control.ts +64 -53
- package/src/runtime/render-control.ts +45 -51
- package/src/runtime/render-seed.ts +3 -4
- package/src/runtime/state-bridge.ts +17 -18
- package/src/runtime/types.ts +94 -61
- package/src/setup/setup-renderer.ts +11 -6
- package/src/world3d-react/engine-bridge.ts +46 -33
- package/src/world3d-react/index.ts +31 -26
- package/src/world3d-react/r3f-adapter.tsx +211 -96
- package/src/world3d-react/r3f-root-factory.tsx +91 -0
- package/src/world3d-react/renderer-config.ts +137 -0
- package/src/world3d-react/world-context.ts +141 -111
- package/schemas/entity2d.schema.json +0 -468
- package/schemas/prefab.schema.json +0 -9992
- package/schemas/scn2d.schema.json +0 -494
- package/schemas/vscn.schema.json +0 -10851
- package/src/adapter/game-adapter.ts +0 -164
- package/src/adapter/ingest/overlay-applier.ts +0 -207
- package/src/adapter/ingest/overlay-apply.ts +0 -168
- package/src/adapter/ingest/overlay-file.ts +0 -126
- package/src/adapter/ingest/overlay-report.ts +0 -176
- package/src/animation/gsap-registration.ts +0 -184
- package/src/audio/audio-introspection.ts +0 -290
- package/src/audio/index.ts +0 -39
- package/src/audio/tone-clock-binding.ts +0 -98
- package/src/audio/tone-context.ts +0 -175
- package/src/audio/tone-offline-render.ts +0 -167
- package/src/ecs/component-manager.ts +0 -814
- package/src/ecs/game-component.ts +0 -260
- package/src/ecs/hmr-swap-report.ts +0 -65
- package/src/physics/trigger-dispatch.ts +0 -97
- package/src/react/root-adapter.tsx +0 -49
- package/src/scene/asset-paths.ts +0 -121
- package/src/scene/asset-ref-check.ts +0 -248
- package/src/scene/component-registry.ts +0 -51
- package/src/scene/parse.ts +0 -204
- package/src/scene/scene-apply.ts +0 -407
- package/src/scene/scene-diff-schema.ts +0 -115
- package/src/scene/scene-diff-types.ts +0 -29
- package/src/scene/scene-loader.ts +0 -1526
- package/src/scene/scene-query.ts +0 -63
- package/src/scene/scene-types.ts +0 -33
- package/src/scene/scene-version.ts +0 -40
- package/src/scene/schema/animation.ts +0 -46
- package/src/scene/schema/audio.ts +0 -25
- package/src/scene/schema/entity-ref.ts +0 -78
- package/src/scene/schema/entity.ts +0 -189
- package/src/scene/schema/index.ts +0 -51
- package/src/scene/schema/joint.ts +0 -26
- package/src/scene/schema/physics.ts +0 -49
- package/src/scene/schema/scene-file.ts +0 -292
- package/src/scene/schema/shadow.ts +0 -24
- package/src/scene/schema/spline.ts +0 -21
- package/src/world2d/asset-paths2d.ts +0 -44
- package/src/world2d/capture-to-scene2d.ts +0 -52
- package/src/world2d/collision-2d.ts +0 -99
- package/src/world2d/entity2d-asset.ts +0 -22
- package/src/world2d/index.ts +0 -91
- package/src/world2d/physics2d-transform.ts +0 -173
- package/src/world2d/physics2d-units.ts +0 -10
- package/src/world2d/pixi-game-adapter.ts +0 -439
- package/src/world2d/pixi-surface.ts +0 -78
- package/src/world2d/scene2d-identity.ts +0 -49
- package/src/world2d/scene2d-loader.ts +0 -433
- package/src/world2d/schema/entity2d.ts +0 -163
- package/src/world2d/schema/physics2d.ts +0 -64
- package/src/world2d/schema/sprite.ts +0 -99
- package/src/world2d/schema/tilemap.ts +0 -39
- package/src/world2d/schema/tuples2d.ts +0 -25
- package/src/world2d/transform-writer-2d.ts +0 -42
- package/src/world2d/types.ts +0 -74
- package/src/world3d-react/behavior.tsx +0 -146
- /package/src/{scene → render}/mesh-shadow.ts +0 -0
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared asset-reference integrity check (B6 follow-up to B2 item #20,
|
|
3
|
-
* docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md §8 B6).
|
|
4
|
-
*
|
|
5
|
-
* RELOCATED HERE (from `packages/vgai-cli/src/apply-diff.ts`, where it was
|
|
6
|
-
* originally built as `dereferenceNewAssetRefs`) so it can be shared by BOTH
|
|
7
|
-
* `vgai apply-diff` (the file-mode CLI verb) and the SDK's
|
|
8
|
-
* `project.scene.apply` operation (`packages/vgai-sdk/src/project/scene-operations.ts`)
|
|
9
|
-
* WITHOUT the SDK importing from the CLI — that would invert the intended
|
|
10
|
-
* dependency direction (CLI -> SDK -> engine, §3.4). The engine's scene layer
|
|
11
|
-
* is the correct shared home: both the CLI and the SDK already depend
|
|
12
|
-
* directly on `@vgai/engine/scene/*` for scene parsing/patching, so this
|
|
13
|
-
* module sits alongside `parse.ts`/`scene-apply.ts` as one more piece of
|
|
14
|
-
* scene-layer machinery neither caller re-implements.
|
|
15
|
-
*
|
|
16
|
-
* WHAT THIS CATCHES that schema validation alone does not: the Zod scene
|
|
17
|
-
* schema only checks that e.g. `materialRef` is a STRING — never that the
|
|
18
|
-
* string points at anything real. A diff introducing
|
|
19
|
-
* `materialRef: "materials/typo.mat.json"` (nonexistent, or present but
|
|
20
|
-
* malformed) passes ordinary scene/patch validation and would be written;
|
|
21
|
-
* the failure would otherwise only surface later, at runtime asset fetch.
|
|
22
|
-
* `dereferenceNewAssetRefs` closes that gap by re-resolving every asset
|
|
23
|
-
* reference the diff INTRODUCED (present in `result`, absent from
|
|
24
|
-
* `inputScene` — deliberately not pre-existing refs, so a scene with
|
|
25
|
-
* unrelated prior breakage can still receive an unrelated repair;
|
|
26
|
-
* `validate-scenes` remains the whole-project content gate) against disk,
|
|
27
|
-
* refusing the write if anything is missing or fails its own schema.
|
|
28
|
-
*
|
|
29
|
-
* Reference kinds and their treatment (unchanged from the original CLI-only
|
|
30
|
-
* implementation):
|
|
31
|
-
* - Zod-validated (JSON formats with engine Zod schemas): `materialRef`
|
|
32
|
-
* (.mat.json -> parseMaterialFile), `prefab` in its file-path form
|
|
33
|
-
* (.prefab.json -> parsePrefabFile). Prefabs are validated ONE level
|
|
34
|
-
* deep — the referenced prefab file itself must exist and parse; assets
|
|
35
|
-
* IT references are that file's own concern. (`animation.animGraph` ->
|
|
36
|
-
* AnimGraphFileSchema was removed by E5 — AnimGraph no longer exists.)
|
|
37
|
-
* - Existence-checked only (binary formats with no Zod schema to parse
|
|
38
|
-
* them through): gltf `mesh.src`, every material texture-map path
|
|
39
|
-
* (including the nested clearcoat/transmission/sheen/iridescence groups),
|
|
40
|
-
* `particles.material.map`, `audio.src`, `environment.skybox`.
|
|
41
|
-
* - `.inputmap.json` is structurally out of scope: no `.vscn.json`/
|
|
42
|
-
* `.prefab.json` field references an input map.
|
|
43
|
-
*/
|
|
44
|
-
|
|
45
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
46
|
-
import { basename, dirname, join, resolve } from 'node:path';
|
|
47
|
-
import { parseMaterialFile, parsePrefabFile, SceneParseError } from './parse';
|
|
48
|
-
import type { SceneEntity, SceneFile } from './scene-types';
|
|
49
|
-
|
|
50
|
-
/** One asset reference found in a scene document: where it is + what it points at. */
|
|
51
|
-
export interface AssetRef {
|
|
52
|
-
/** The authored path string (web-root-relative, optionally /-prefixed). */
|
|
53
|
-
path: string;
|
|
54
|
-
/** Human-readable field location for error messages, e.g. `entity "crate" field materialRef`. */
|
|
55
|
-
field: string;
|
|
56
|
-
kind: 'material' | 'prefab' | 'binary';
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* One asset reference that failed to dereference. `reason` is a one-line-or-
|
|
61
|
-
* more human-readable explanation (schema-validation failures may be
|
|
62
|
-
* multi-line); `field`/`path` mirror `AssetRef` and `diskPath` is the
|
|
63
|
-
* resolved absolute path that was checked, so every caller (CLI text report,
|
|
64
|
-
* SDK structured error data) can build its own presentation from the same
|
|
65
|
-
* facts without re-deriving them.
|
|
66
|
-
*/
|
|
67
|
-
export interface AssetRefError {
|
|
68
|
-
field: string;
|
|
69
|
-
path: string;
|
|
70
|
-
diskPath: string;
|
|
71
|
-
reason: string;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Material texture-map fields that hold image paths — top-level and nested feature groups. */
|
|
75
|
-
const MATERIAL_MAP_KEYS = [
|
|
76
|
-
'map',
|
|
77
|
-
'normalMap',
|
|
78
|
-
'emissiveMap',
|
|
79
|
-
'aoMap',
|
|
80
|
-
'roughnessMap',
|
|
81
|
-
'metalnessMap',
|
|
82
|
-
'displacementMap',
|
|
83
|
-
] as const;
|
|
84
|
-
const MATERIAL_GROUP_MAP_KEYS: Record<string, readonly string[]> = {
|
|
85
|
-
clearcoat: ['clearcoatMap', 'clearcoatRoughnessMap'],
|
|
86
|
-
transmission: ['transmissionMap'],
|
|
87
|
-
sheen: ['sheenColorMap', 'sheenRoughnessMap'],
|
|
88
|
-
iridescence: ['iridescenceMap', 'iridescenceThicknessMap'],
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
/** Same file-path-vs-registry-id heuristic as `asset-paths.ts`. */
|
|
92
|
-
function isFilePrefab(value: string): boolean {
|
|
93
|
-
return value.includes('/');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function entityLabel(e: SceneEntity): string {
|
|
97
|
-
return e.id ? `entity "${e.name}" (id ${e.id})` : `entity "${e.name}"`;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Collect every asset reference in a scene document (entities walked recursively + environment), labeled for error messages. */
|
|
101
|
-
export function collectAssetRefs(doc: SceneFile): AssetRef[] {
|
|
102
|
-
const refs: AssetRef[] = [];
|
|
103
|
-
|
|
104
|
-
function walk(entities: SceneEntity[]): void {
|
|
105
|
-
for (const e of entities) {
|
|
106
|
-
const label = entityLabel(e);
|
|
107
|
-
if (e.mesh?.type === 'gltf' && e.mesh.src) {
|
|
108
|
-
refs.push({ path: e.mesh.src, field: `${label} field mesh.src`, kind: 'binary' });
|
|
109
|
-
}
|
|
110
|
-
if (e.material) {
|
|
111
|
-
const mat = e.material as Record<string, unknown>;
|
|
112
|
-
for (const key of MATERIAL_MAP_KEYS) {
|
|
113
|
-
const val = mat[key];
|
|
114
|
-
if (typeof val === 'string') {
|
|
115
|
-
refs.push({ path: val, field: `${label} field material.${key}`, kind: 'binary' });
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
for (const [group, keys] of Object.entries(MATERIAL_GROUP_MAP_KEYS)) {
|
|
119
|
-
const groupVal = mat[group];
|
|
120
|
-
if (groupVal && typeof groupVal === 'object') {
|
|
121
|
-
for (const key of keys) {
|
|
122
|
-
const val = (groupVal as Record<string, unknown>)[key];
|
|
123
|
-
if (typeof val === 'string') {
|
|
124
|
-
refs.push({
|
|
125
|
-
path: val,
|
|
126
|
-
field: `${label} field material.${group}.${key}`,
|
|
127
|
-
kind: 'binary',
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (e.materialRef) {
|
|
135
|
-
refs.push({ path: e.materialRef, field: `${label} field materialRef`, kind: 'material' });
|
|
136
|
-
}
|
|
137
|
-
if (e.particles?.material?.map) {
|
|
138
|
-
refs.push({
|
|
139
|
-
path: e.particles.material.map,
|
|
140
|
-
field: `${label} field particles.material.map`,
|
|
141
|
-
kind: 'binary',
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
if (e.audio?.src) {
|
|
145
|
-
refs.push({ path: e.audio.src, field: `${label} field audio.src`, kind: 'binary' });
|
|
146
|
-
}
|
|
147
|
-
if (e.prefab && isFilePrefab(e.prefab)) {
|
|
148
|
-
refs.push({ path: e.prefab, field: `${label} field prefab`, kind: 'prefab' });
|
|
149
|
-
}
|
|
150
|
-
if (e.children) walk(e.children);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
walk(doc.entities);
|
|
155
|
-
if (doc.environment?.skybox) {
|
|
156
|
-
refs.push({ path: doc.environment.skybox, field: 'environment.skybox', kind: 'binary' });
|
|
157
|
-
}
|
|
158
|
-
return refs;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Resolve the asset root the scene's references are relative to. Asset paths
|
|
163
|
-
* are web-root-relative (the runtime fetches them from the served `public/`
|
|
164
|
-
* directory), and scene files conventionally live INSIDE `public/`
|
|
165
|
-
* (`public/scenes/*.vscn.json`) — so the nearest ancestor directory literally
|
|
166
|
-
* named `public` is the root. Fallback for scenes not under a `public/`
|
|
167
|
-
* ancestor: the scene's own directory.
|
|
168
|
-
*/
|
|
169
|
-
export function findAssetRoot(scenePath: string): string {
|
|
170
|
-
const dir = resolve(dirname(scenePath));
|
|
171
|
-
let cursor = dir;
|
|
172
|
-
for (;;) {
|
|
173
|
-
if (basename(cursor) === 'public') return cursor;
|
|
174
|
-
const parent = dirname(cursor);
|
|
175
|
-
if (parent === cursor) return dir;
|
|
176
|
-
cursor = parent;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** Format Zod/scene-parse issues as `path: message`, one per line. */
|
|
181
|
-
function formatIssues(issues: { path: PropertyKey[]; message: string }[]): string {
|
|
182
|
-
return issues.map((i) => ` ${i.path.join('.')}: ${i.message}`).join('\n');
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Dereference every asset reference `result` introduces relative to
|
|
187
|
-
* `inputScene` (present in the result, absent from the input) against disk,
|
|
188
|
-
* relative to `scenePath`'s asset root (see `findAssetRoot`). Returns
|
|
189
|
-
* structured errors (empty = every new reference resolved and validated) —
|
|
190
|
-
* never throws; callers decide how to present/escalate a non-empty result
|
|
191
|
-
* (the CLI folds these into one `ApplyDiffCliError` message, the SDK raises
|
|
192
|
-
* a declared `ASSET_REF_INVALID` `ToolError`).
|
|
193
|
-
*/
|
|
194
|
-
export function dereferenceNewAssetRefs(
|
|
195
|
-
inputScene: SceneFile,
|
|
196
|
-
result: SceneFile,
|
|
197
|
-
scenePath: string,
|
|
198
|
-
): AssetRefError[] {
|
|
199
|
-
const preexisting = new Set(collectAssetRefs(inputScene).map((r) => r.path));
|
|
200
|
-
const newRefs = collectAssetRefs(result).filter((r) => !preexisting.has(r.path));
|
|
201
|
-
if (newRefs.length === 0) return [];
|
|
202
|
-
|
|
203
|
-
const assetRoot = findAssetRoot(scenePath);
|
|
204
|
-
const errors: AssetRefError[] = [];
|
|
205
|
-
|
|
206
|
-
for (const ref of newRefs) {
|
|
207
|
-
const diskPath = join(assetRoot, ref.path.replace(/^\//, ''));
|
|
208
|
-
if (!existsSync(diskPath)) {
|
|
209
|
-
errors.push({
|
|
210
|
-
field: ref.field,
|
|
211
|
-
path: ref.path,
|
|
212
|
-
diskPath,
|
|
213
|
-
reason: `file not found at ${diskPath} (resolved against ${assetRoot})`,
|
|
214
|
-
});
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
if (ref.kind === 'binary') continue; // existence-only (see the exclusion note above)
|
|
218
|
-
|
|
219
|
-
let json: unknown;
|
|
220
|
-
try {
|
|
221
|
-
json = JSON.parse(readFileSync(diskPath, 'utf-8'));
|
|
222
|
-
} catch (err) {
|
|
223
|
-
errors.push({
|
|
224
|
-
field: ref.field,
|
|
225
|
-
path: ref.path,
|
|
226
|
-
diskPath,
|
|
227
|
-
reason: `${diskPath} is not valid JSON: ${err instanceof Error ? err.message : err}`,
|
|
228
|
-
});
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
try {
|
|
232
|
-
if (ref.kind === 'material') {
|
|
233
|
-
parseMaterialFile(json, diskPath);
|
|
234
|
-
} else {
|
|
235
|
-
parsePrefabFile(json, diskPath);
|
|
236
|
-
}
|
|
237
|
-
} catch (err) {
|
|
238
|
-
const detail = err instanceof SceneParseError ? `\n${formatIssues(err.issues)}` : ` ${err}`;
|
|
239
|
-
errors.push({
|
|
240
|
-
field: ref.field,
|
|
241
|
-
path: ref.path,
|
|
242
|
-
diskPath,
|
|
243
|
-
reason: `${diskPath} fails schema validation:${detail}`,
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return errors;
|
|
248
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import type * as THREE from 'three';
|
|
2
|
-
import type { ComponentManager } from '../ecs/component-manager';
|
|
3
|
-
import {
|
|
4
|
-
GAME_COMPONENT_BRAND,
|
|
5
|
-
GameComponent,
|
|
6
|
-
type GameComponentClass,
|
|
7
|
-
} from '../ecs/game-component';
|
|
8
|
-
|
|
9
|
-
/** A registry mapping component names to GameComponent classes. */
|
|
10
|
-
export type ComponentRegistry = Record<string, GameComponentClass>;
|
|
11
|
-
|
|
12
|
-
/** Type guard: is this value a GameComponent class? `instanceof` alone is NOT
|
|
13
|
-
* enough: a scaffolded project's node-side scripts resolve TWO physical
|
|
14
|
-
* copies of the engine (published `@vgai/engine/*` + the `@engine/*` source
|
|
15
|
-
* alias), and a class extending the OTHER copy's `GameComponent` fails the
|
|
16
|
-
* prototype-chain check — so this also accepts the cross-copy
|
|
17
|
-
* {@link GAME_COMPONENT_BRAND}. */
|
|
18
|
-
export function isGameComponentClass(entry: unknown): entry is GameComponentClass {
|
|
19
|
-
if (typeof entry !== 'function') return false;
|
|
20
|
-
if (entry.prototype instanceof GameComponent) return true;
|
|
21
|
-
return (entry as unknown as Record<symbol, unknown>)[GAME_COMPONENT_BRAND] === true;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Apply components from scene JSON data onto an entity (an Object3D).
|
|
26
|
-
*
|
|
27
|
-
* For each named component: look up its class in the registry, validate +
|
|
28
|
-
* default the authored data through the optional `static schema` (Zod), assign
|
|
29
|
-
* onto a fresh instance, and attach it via the ComponentManager (which wires the
|
|
30
|
-
* Object3D + physics refs and queues init()).
|
|
31
|
-
*/
|
|
32
|
-
export function applyComponents(
|
|
33
|
-
registry: ComponentRegistry,
|
|
34
|
-
object3D: THREE.Object3D,
|
|
35
|
-
components: Record<string, Record<string, unknown>>,
|
|
36
|
-
manager: ComponentManager,
|
|
37
|
-
): void {
|
|
38
|
-
for (const [name, data] of Object.entries(components)) {
|
|
39
|
-
const Klass = registry[name];
|
|
40
|
-
if (!Klass) {
|
|
41
|
-
throw new Error(`Component "${name}" not found in registry`);
|
|
42
|
-
}
|
|
43
|
-
const parsed = Klass.schema ? Klass.schema.parse(data) : data;
|
|
44
|
-
const inst = new Klass();
|
|
45
|
-
Object.assign(inst, parsed);
|
|
46
|
-
// T5.4: record the registry name + raw authored data so a later `hotSwap`
|
|
47
|
-
// keys on this stable identity (not the live `constructor.name`) and can
|
|
48
|
-
// re-validate `data` against a NEW class version's schema.
|
|
49
|
-
manager.attach(object3D, inst, { key: name, props: data });
|
|
50
|
-
}
|
|
51
|
-
}
|
package/src/scene/parse.ts
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import type { ZodIssue } from 'zod';
|
|
2
|
-
import { checkSceneVersionInRange } from './scene-version';
|
|
3
|
-
import type { InstancesFile, MaterialFile, PrefabFile, SceneFile } from './schema';
|
|
4
|
-
import {
|
|
5
|
-
InstancesFileSchema,
|
|
6
|
-
MaterialFileSchema,
|
|
7
|
-
PrefabFileSchema,
|
|
8
|
-
SceneFileSchema,
|
|
9
|
-
} from './schema';
|
|
10
|
-
|
|
11
|
-
export class SceneParseError extends Error {
|
|
12
|
-
readonly issues: ZodIssue[];
|
|
13
|
-
/** The offending file's path/URL, when the caller knows it (T4.6). */
|
|
14
|
-
readonly filePath?: string | undefined;
|
|
15
|
-
|
|
16
|
-
constructor(issues: ZodIssue[], filePath?: string) {
|
|
17
|
-
const msg = issues.map((i) => ` ${i.path.join('.')}: ${i.message}`).join('\n');
|
|
18
|
-
const header = filePath ? `Scene validation failed (${filePath}):` : 'Scene validation failed:';
|
|
19
|
-
super(`${header}\n${msg}`);
|
|
20
|
-
this.name = 'SceneParseError';
|
|
21
|
-
this.issues = issues;
|
|
22
|
-
this.filePath = filePath;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
// D8 — removed-format migration guard: declarative property tracks
|
|
28
|
-
// (`animation.tracks`) were deleted (spec §3.2/§10 D8; Theatre — `@theatre/core`
|
|
29
|
-
// — is the sole authored continuous-animation source now). `SceneAnimationSchema`
|
|
30
|
-
// no longer declares a `tracks` field at all (so the generated JSON schema stays
|
|
31
|
-
// clean), which means Zod's default object mode would otherwise silently STRIP
|
|
32
|
-
// an authored `animation.tracks` rather than reporting it — violating Global AC
|
|
33
|
-
// §6 ("Removed formats fail with a concise migration/removal error rather than
|
|
34
|
-
// being partially read"). This walks the RAW pre-Zod JSON (Zod would already
|
|
35
|
-
// have stripped `tracks` by the time any schema-level check could see it) and
|
|
36
|
-
// throws a `SceneParseError` before Zod ever parses the data. Matches on the
|
|
37
|
-
// literal key `animation` (singular, an object) carrying a `tracks` property —
|
|
38
|
-
// this is deliberately narrower than Scene UI's unrelated, RETAINED
|
|
39
|
-
// `ui[].animations[].tracks` (plural `animations`), which this guard must not
|
|
40
|
-
// trip on.
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
|
|
43
|
-
/** True when `value` is a plain object (not an array) carrying a `tracks` property — the shape
|
|
44
|
-
* `SceneAnimationSchema` used to declare before D8 deleted it. */
|
|
45
|
-
function isRemovedTracksAnimation(value: unknown): boolean {
|
|
46
|
-
return !!value && typeof value === 'object' && !Array.isArray(value) && 'tracks' in value;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function findRemovedPropertyTrackPathInArray(node: unknown[], path: string): string | undefined {
|
|
50
|
-
for (let i = 0; i < node.length; i++) {
|
|
51
|
-
const hit = findRemovedPropertyTrackPath(node[i], `${path}[${i}]`);
|
|
52
|
-
if (hit) return hit;
|
|
53
|
-
}
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function findRemovedPropertyTrackPathInObject(
|
|
58
|
-
obj: Record<string, unknown>,
|
|
59
|
-
path: string,
|
|
60
|
-
): string | undefined {
|
|
61
|
-
if (isRemovedTracksAnimation(obj['animation'])) {
|
|
62
|
-
return `${path ? `${path}.` : ''}animation.tracks`;
|
|
63
|
-
}
|
|
64
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
65
|
-
const hit = findRemovedPropertyTrackPath(value, path ? `${path}.${key}` : key);
|
|
66
|
-
if (hit) return hit;
|
|
67
|
-
}
|
|
68
|
-
return undefined;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function findRemovedPropertyTrackPath(node: unknown, path: string): string | undefined {
|
|
72
|
-
if (Array.isArray(node)) return findRemovedPropertyTrackPathInArray(node, path);
|
|
73
|
-
if (node && typeof node === 'object') {
|
|
74
|
-
return findRemovedPropertyTrackPathInObject(node as Record<string, unknown>, path);
|
|
75
|
-
}
|
|
76
|
-
return undefined;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function assertNoRemovedPropertyTracks(json: unknown, filePath?: string): void {
|
|
80
|
-
const hit = findRemovedPropertyTrackPath(json, '');
|
|
81
|
-
if (!hit) return;
|
|
82
|
-
const issue: ZodIssue = {
|
|
83
|
-
code: 'custom',
|
|
84
|
-
message:
|
|
85
|
-
'property tracks were removed; author continuous animation with Theatre (@theatre/core) ' +
|
|
86
|
-
'instead — see docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md §3.2/§10 D8.',
|
|
87
|
-
path: hit.split('.'),
|
|
88
|
-
};
|
|
89
|
-
throw new SceneParseError([issue], filePath);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
|
-
// E5 — removed-format migration guard: the AnimGraph state-machine runtime,
|
|
94
|
-
// `.animgraph.json` format, and `SceneAnimationSchema.animGraph` field were
|
|
95
|
-
// deleted (spec §3.3/§11 E5; XState + native Three `AnimationMixer` — see
|
|
96
|
-
// `xstate-animation-binding.ts` — replace it). `SceneAnimationSchema` no
|
|
97
|
-
// longer declares an `animGraph` field at all (so it carries zero trace in
|
|
98
|
-
// the generated JSON schema), which means Zod's default object mode would
|
|
99
|
-
// otherwise silently STRIP an authored `animation.animGraph` rather than
|
|
100
|
-
// reporting it — violating Global AC §6. Mirrors
|
|
101
|
-
// `assertNoRemovedPropertyTracks` above exactly: walks the RAW pre-Zod JSON
|
|
102
|
-
// and throws a `SceneParseError` before Zod ever parses the data.
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
|
-
|
|
105
|
-
/** True when `value` is a plain object (not an array) carrying an `animGraph` property — the
|
|
106
|
-
* shape `SceneAnimationSchema` used to declare before E5 deleted it. */
|
|
107
|
-
function isRemovedAnimGraphAnimation(value: unknown): boolean {
|
|
108
|
-
return !!value && typeof value === 'object' && !Array.isArray(value) && 'animGraph' in value;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function findRemovedAnimGraphPathInArray(node: unknown[], path: string): string | undefined {
|
|
112
|
-
for (let i = 0; i < node.length; i++) {
|
|
113
|
-
const hit = findRemovedAnimGraphPath(node[i], `${path}[${i}]`);
|
|
114
|
-
if (hit) return hit;
|
|
115
|
-
}
|
|
116
|
-
return undefined;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function findRemovedAnimGraphPathInObject(
|
|
120
|
-
obj: Record<string, unknown>,
|
|
121
|
-
path: string,
|
|
122
|
-
): string | undefined {
|
|
123
|
-
if (isRemovedAnimGraphAnimation(obj['animation'])) {
|
|
124
|
-
return `${path ? `${path}.` : ''}animation.animGraph`;
|
|
125
|
-
}
|
|
126
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
127
|
-
const hit = findRemovedAnimGraphPath(value, path ? `${path}.${key}` : key);
|
|
128
|
-
if (hit) return hit;
|
|
129
|
-
}
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function findRemovedAnimGraphPath(node: unknown, path: string): string | undefined {
|
|
134
|
-
if (Array.isArray(node)) return findRemovedAnimGraphPathInArray(node, path);
|
|
135
|
-
if (node && typeof node === 'object') {
|
|
136
|
-
return findRemovedAnimGraphPathInObject(node as Record<string, unknown>, path);
|
|
137
|
-
}
|
|
138
|
-
return undefined;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function assertNoRemovedAnimGraph(json: unknown, filePath?: string): void {
|
|
142
|
-
const hit = findRemovedAnimGraphPath(json, '');
|
|
143
|
-
if (!hit) return;
|
|
144
|
-
const issue: ZodIssue = {
|
|
145
|
-
code: 'custom',
|
|
146
|
-
message:
|
|
147
|
-
'AnimGraph was removed; drive character animation with an XState animation machine instead ' +
|
|
148
|
-
'— see docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md §3.3/§5.6/§11 E5 and ' +
|
|
149
|
-
'packages/engine/src/animation/xstate-animation-binding.ts.',
|
|
150
|
-
path: hit.split('.'),
|
|
151
|
-
};
|
|
152
|
-
throw new SceneParseError([issue], filePath);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function parseSceneFile(json: unknown, filePath?: string): SceneFile {
|
|
156
|
-
assertNoRemovedPropertyTracks(json, filePath);
|
|
157
|
-
assertNoRemovedAnimGraph(json, filePath);
|
|
158
|
-
const result = SceneFileSchema.safeParse(json);
|
|
159
|
-
if (!result.success) {
|
|
160
|
-
throw new SceneParseError(result.error.issues, filePath);
|
|
161
|
-
}
|
|
162
|
-
// Post-Zod-parse range check (T2.3/D3 §1.E) — the Zod shape stays a bare
|
|
163
|
-
// `z.number()`; this is where the file-format `version` axis is actually
|
|
164
|
-
// read/enforced.
|
|
165
|
-
checkSceneVersionInRange(result.data.version, 'scene');
|
|
166
|
-
return result.data;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export function parsePrefabFile(json: unknown, filePath?: string): PrefabFile {
|
|
170
|
-
assertNoRemovedPropertyTracks(json, filePath);
|
|
171
|
-
assertNoRemovedAnimGraph(json, filePath);
|
|
172
|
-
const result = PrefabFileSchema.safeParse(json);
|
|
173
|
-
if (!result.success) {
|
|
174
|
-
throw new SceneParseError(result.error.issues, filePath);
|
|
175
|
-
}
|
|
176
|
-
checkSceneVersionInRange(result.data.version, 'prefab');
|
|
177
|
-
return result.data;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Parse a `.mat.json` shared material asset (T4.6). Unlike scene/prefab files,
|
|
182
|
-
* there is no version-range gate yet for this format — `MaterialFileSchema`'s
|
|
183
|
-
* `version` field is RESERVED (see `schema-consumption-map.ts`).
|
|
184
|
-
*/
|
|
185
|
-
export function parseMaterialFile(json: unknown, filePath?: string): MaterialFile {
|
|
186
|
-
const result = MaterialFileSchema.safeParse(json);
|
|
187
|
-
if (!result.success) {
|
|
188
|
-
throw new SceneParseError(result.error.issues, filePath);
|
|
189
|
-
}
|
|
190
|
-
return result.data;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Parse a `.instances.json` instance-transform asset (F3,
|
|
195
|
-
* docs/VSCN-STRUCTURAL-GAPS-DESIGN.md). Mirrors `parseMaterialFile` exactly —
|
|
196
|
-
* no version-range gate (the format is a bare tuple array, no `version` field).
|
|
197
|
-
*/
|
|
198
|
-
export function parseInstancesFile(json: unknown, filePath?: string): InstancesFile {
|
|
199
|
-
const result = InstancesFileSchema.safeParse(json);
|
|
200
|
-
if (!result.success) {
|
|
201
|
-
throw new SceneParseError(result.error.issues, filePath);
|
|
202
|
-
}
|
|
203
|
-
return result.data;
|
|
204
|
-
}
|