@game_engine/scene 0.1.0-alpha
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/index.d.ts +381 -0
- package/dist/index.js +3996 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3996 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { existsSync, statSync } from "fs";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import Ajv from "ajv";
|
|
6
|
+
import { parse, stringify } from "yaml";
|
|
7
|
+
import {
|
|
8
|
+
assetManifestSchema,
|
|
9
|
+
componentFieldOrder,
|
|
10
|
+
componentOrder,
|
|
11
|
+
componentSchemas,
|
|
12
|
+
generatedMapSchema,
|
|
13
|
+
mapGenerationConfigSchema,
|
|
14
|
+
patchSchema,
|
|
15
|
+
prefabSchema,
|
|
16
|
+
projectSchema,
|
|
17
|
+
sceneSchema,
|
|
18
|
+
supportedPatchOperations
|
|
19
|
+
} from "@game_engine/contracts";
|
|
20
|
+
import {
|
|
21
|
+
assetManifestSchema as assetManifestSchema2,
|
|
22
|
+
componentSchemas as componentSchemas2,
|
|
23
|
+
generatedMapSchema as generatedMapSchema2,
|
|
24
|
+
mapGenerationConfigSchema as mapGenerationConfigSchema2,
|
|
25
|
+
patchSchema as patchSchema2,
|
|
26
|
+
prefabSchema as prefabSchema2,
|
|
27
|
+
projectSchema as projectSchema2,
|
|
28
|
+
sceneSchema as sceneSchema2
|
|
29
|
+
} from "@game_engine/contracts";
|
|
30
|
+
var componentReferenceFields = {
|
|
31
|
+
CharacterController: ["groundEntityId", "targetEntityId"],
|
|
32
|
+
CameraFollow: ["target"],
|
|
33
|
+
Camera3D: ["targetEntityId"],
|
|
34
|
+
GroundedState: ["groundEntityId"],
|
|
35
|
+
Parent: ["entityId"]
|
|
36
|
+
};
|
|
37
|
+
var ajv = new Ajv({ allErrors: true, strict: false });
|
|
38
|
+
var validateAssetManifestSchema = ajv.compile(assetManifestSchema);
|
|
39
|
+
var validateMapGenerationConfigSchema = ajv.compile(mapGenerationConfigSchema);
|
|
40
|
+
var validateGeneratedMapSchema = ajv.compile(generatedMapSchema);
|
|
41
|
+
var validateProjectSchema = ajv.compile(projectSchema);
|
|
42
|
+
var validateSceneSchema = ajv.compile(sceneSchema);
|
|
43
|
+
var validatePrefabSchema = ajv.compile(prefabSchema);
|
|
44
|
+
var validatePatchSchema = ajv.compile(patchSchema);
|
|
45
|
+
var componentValidators = Object.fromEntries(
|
|
46
|
+
Object.entries(componentSchemas).map(([name, schema]) => [name, ajv.compile(schema)])
|
|
47
|
+
);
|
|
48
|
+
function parseProjectSource(source, file = "agent.project.yml") {
|
|
49
|
+
return parseYamlSource(source, file);
|
|
50
|
+
}
|
|
51
|
+
function parseSceneSource(source, file = "scene.yml") {
|
|
52
|
+
return parseYamlSource(source, file);
|
|
53
|
+
}
|
|
54
|
+
function parsePrefabSource(source, file = "prefab.yml") {
|
|
55
|
+
return parseYamlSource(source, file);
|
|
56
|
+
}
|
|
57
|
+
function parseAssetManifestSource(source, file = "agent.assets.yml") {
|
|
58
|
+
return parseYamlSource(source, file);
|
|
59
|
+
}
|
|
60
|
+
function parseMapGenerationConfigSource(source, file = "map.yml") {
|
|
61
|
+
return parseYamlSource(source, file);
|
|
62
|
+
}
|
|
63
|
+
function parseGeneratedMapSource(source, file = "generated-map.yml") {
|
|
64
|
+
return parseYamlSource(source, file);
|
|
65
|
+
}
|
|
66
|
+
function parsePatchSource(source, file = "patch.json") {
|
|
67
|
+
try {
|
|
68
|
+
const document = path.extname(file).toLowerCase() === ".json" ? JSON.parse(source) : parse(source);
|
|
69
|
+
if (document === null || document === void 0) {
|
|
70
|
+
return {
|
|
71
|
+
file,
|
|
72
|
+
diagnostics: [
|
|
73
|
+
{
|
|
74
|
+
severity: "error",
|
|
75
|
+
code: "PATCH_EMPTY_DOCUMENT",
|
|
76
|
+
file,
|
|
77
|
+
path: "$",
|
|
78
|
+
message: "Patch document is empty.",
|
|
79
|
+
suggestion: "Add version, target, and operations before applying the patch."
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
file,
|
|
86
|
+
document,
|
|
87
|
+
diagnostics: []
|
|
88
|
+
};
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return {
|
|
91
|
+
file,
|
|
92
|
+
diagnostics: [
|
|
93
|
+
{
|
|
94
|
+
severity: "error",
|
|
95
|
+
code: "PATCH_PARSE_ERROR",
|
|
96
|
+
file,
|
|
97
|
+
path: "$",
|
|
98
|
+
message: error instanceof Error ? error.message : String(error),
|
|
99
|
+
suggestion: "Fix the patch JSON or YAML syntax before applying it."
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function loadProjectFile(projectPath = ".", options = {}) {
|
|
106
|
+
const file = resolveProjectFile(projectPath, options.cwd);
|
|
107
|
+
return loadYamlFile(file);
|
|
108
|
+
}
|
|
109
|
+
async function loadSceneFile(scenePath, options = {}) {
|
|
110
|
+
const file = path.resolve(options.cwd ?? process.cwd(), scenePath);
|
|
111
|
+
return loadYamlFile(file);
|
|
112
|
+
}
|
|
113
|
+
async function loadPrefabFile(prefabPath, options = {}) {
|
|
114
|
+
const file = path.resolve(options.cwd ?? process.cwd(), prefabPath);
|
|
115
|
+
return loadYamlFile(file);
|
|
116
|
+
}
|
|
117
|
+
async function loadAssetManifestFile(assetManifestPath, options = {}) {
|
|
118
|
+
const file = path.resolve(options.cwd ?? process.cwd(), assetManifestPath);
|
|
119
|
+
return loadYamlFile(file);
|
|
120
|
+
}
|
|
121
|
+
async function loadMapGenerationConfigFile(configPath, options = {}) {
|
|
122
|
+
const file = path.resolve(options.cwd ?? process.cwd(), configPath);
|
|
123
|
+
return loadYamlFile(file);
|
|
124
|
+
}
|
|
125
|
+
async function loadGeneratedMapFile(generatedMapPath, options = {}) {
|
|
126
|
+
const file = path.resolve(options.cwd ?? process.cwd(), generatedMapPath);
|
|
127
|
+
return loadYamlFile(file);
|
|
128
|
+
}
|
|
129
|
+
async function loadPatchFile(patchPath, options = {}) {
|
|
130
|
+
const file = path.resolve(options.cwd ?? process.cwd(), patchPath);
|
|
131
|
+
try {
|
|
132
|
+
const source = await readFile(file, "utf8");
|
|
133
|
+
return parsePatchSource(source, file);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return {
|
|
136
|
+
file,
|
|
137
|
+
diagnostics: [
|
|
138
|
+
{
|
|
139
|
+
severity: "error",
|
|
140
|
+
code: "FILE_READ_FAILED",
|
|
141
|
+
file,
|
|
142
|
+
path: "$",
|
|
143
|
+
message: error instanceof Error ? error.message : String(error),
|
|
144
|
+
suggestion: "Check that the patch path exists and is readable."
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function validateProjectDocument(project, file = "agent.project.yml") {
|
|
151
|
+
const diagnostics = schemaDiagnostics(validateProjectSchema, project, file, "PROJECT_SCHEMA_INVALID");
|
|
152
|
+
if (isRecord(project) && Array.isArray(project.scenes) && typeof project.defaultScene === "string") {
|
|
153
|
+
if (!project.scenes.includes(project.defaultScene)) {
|
|
154
|
+
diagnostics.push({
|
|
155
|
+
severity: "error",
|
|
156
|
+
code: "PROJECT_DEFAULT_SCENE_MISSING",
|
|
157
|
+
file,
|
|
158
|
+
path: "$.defaultScene",
|
|
159
|
+
message: `Default scene '${project.defaultScene}' is not listed in scenes.`,
|
|
160
|
+
suggestion: "Add the default scene path to the scenes array."
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return diagnostics;
|
|
165
|
+
}
|
|
166
|
+
function validateAssetManifestDocument(assetManifest, file = "agent.assets.yml") {
|
|
167
|
+
const diagnostics = schemaDiagnostics(
|
|
168
|
+
validateAssetManifestSchema,
|
|
169
|
+
assetManifest,
|
|
170
|
+
file,
|
|
171
|
+
"ASSET_MANIFEST_SCHEMA_INVALID"
|
|
172
|
+
);
|
|
173
|
+
if (!isRecord(assetManifest) || !Array.isArray(assetManifest.assets)) {
|
|
174
|
+
return diagnostics;
|
|
175
|
+
}
|
|
176
|
+
const assetsById = /* @__PURE__ */ new Map();
|
|
177
|
+
assetManifest.assets.forEach((asset, index) => {
|
|
178
|
+
const assetPath = `$.assets[${index}]`;
|
|
179
|
+
if (!isRecord(asset)) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (typeof asset.type === "string" && !["texture", "sprite", "atlas", "audio", "font"].includes(asset.type)) {
|
|
183
|
+
diagnostics.push({
|
|
184
|
+
severity: "error",
|
|
185
|
+
code: "ASSET_UNKNOWN_TYPE",
|
|
186
|
+
file,
|
|
187
|
+
path: `${assetPath}.type`,
|
|
188
|
+
message: `Unknown asset type '${asset.type}'.`,
|
|
189
|
+
suggestion: "Use one of: texture, sprite, atlas, audio, font."
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (typeof asset.path === "string" && !isPortableAssetPath(asset.path)) {
|
|
193
|
+
diagnostics.push({
|
|
194
|
+
severity: "error",
|
|
195
|
+
code: "ASSET_PATH_INVALID",
|
|
196
|
+
file,
|
|
197
|
+
path: `${assetPath}.path`,
|
|
198
|
+
message: `Asset path '${asset.path}' is not a portable project-relative path.`,
|
|
199
|
+
suggestion: "Use a relative path with forward slashes, no drive letters, no leading slash, and no '..' segments."
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (asset.type === "sprite" && Object.prototype.hasOwnProperty.call(asset, "rect")) {
|
|
203
|
+
diagnostics.push(...validateAssetRectMetadata(asset.rect, file, `${assetPath}.rect`, String(asset.id)));
|
|
204
|
+
}
|
|
205
|
+
if (asset.type === "atlas" && Array.isArray(asset.frames)) {
|
|
206
|
+
diagnostics.push(...validateAtlasFramesMetadata(asset.frames, file, `${assetPath}.frames`, String(asset.id)));
|
|
207
|
+
}
|
|
208
|
+
if (typeof asset.id !== "string") {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (assetsById.has(asset.id)) {
|
|
212
|
+
diagnostics.push({
|
|
213
|
+
severity: "error",
|
|
214
|
+
code: "ASSET_DUPLICATE_ID",
|
|
215
|
+
file,
|
|
216
|
+
path: `${assetPath}.id`,
|
|
217
|
+
message: `Duplicate asset id '${asset.id}'.`,
|
|
218
|
+
suggestion: "Give every asset one unique stable id and update references to the chosen id."
|
|
219
|
+
});
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (typeof asset.type === "string" && typeof asset.path === "string") {
|
|
223
|
+
const record = {
|
|
224
|
+
type: asset.type,
|
|
225
|
+
path: asset.path,
|
|
226
|
+
dataPath: assetPath,
|
|
227
|
+
...typeof asset.texture === "string" ? { texture: asset.texture } : {},
|
|
228
|
+
...typeof asset.atlas === "string" ? { atlas: asset.atlas } : {},
|
|
229
|
+
...typeof asset.frame === "string" ? { frame: asset.frame } : {},
|
|
230
|
+
...asset.type === "atlas" && Array.isArray(asset.frames) ? { frames: readAtlasFrameRecords(asset.frames, `${assetPath}.frames`) } : {}
|
|
231
|
+
};
|
|
232
|
+
assetsById.set(asset.id, record);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
assetManifest.assets.forEach((asset, index) => {
|
|
236
|
+
if (!isRecord(asset) || typeof asset.id !== "string" || typeof asset.type !== "string") {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const assetPath = `$.assets[${index}]`;
|
|
240
|
+
if (asset.type === "sprite") {
|
|
241
|
+
if (typeof asset.texture === "string") {
|
|
242
|
+
diagnostics.push(
|
|
243
|
+
...validateAssetReferenceToType({
|
|
244
|
+
assetsById,
|
|
245
|
+
file,
|
|
246
|
+
path: `${assetPath}.texture`,
|
|
247
|
+
assetId: asset.id,
|
|
248
|
+
referencedId: asset.texture,
|
|
249
|
+
expectedType: "texture",
|
|
250
|
+
missingCode: "ASSET_BROKEN_REFERENCE",
|
|
251
|
+
mismatchCode: "ASSET_TYPE_MISMATCH",
|
|
252
|
+
ownerType: "Sprite asset",
|
|
253
|
+
suggestion: "Point SpriteAsset.texture to a texture asset id or remove the texture reference."
|
|
254
|
+
})
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
diagnostics.push(...validateSpriteAtlasFrameReference(asset, assetsById, file, assetPath));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (asset.type === "atlas" && typeof asset.texture === "string") {
|
|
261
|
+
diagnostics.push(
|
|
262
|
+
...validateAssetReferenceToType({
|
|
263
|
+
assetsById,
|
|
264
|
+
file,
|
|
265
|
+
path: `${assetPath}.texture`,
|
|
266
|
+
assetId: asset.id,
|
|
267
|
+
referencedId: asset.texture,
|
|
268
|
+
expectedType: "texture",
|
|
269
|
+
missingCode: "ASSET_ATLAS_TEXTURE_REFERENCE_UNRESOLVED",
|
|
270
|
+
mismatchCode: "ASSET_ATLAS_TEXTURE_TYPE_MISMATCH",
|
|
271
|
+
ownerType: "Atlas asset",
|
|
272
|
+
suggestion: "Point atlas.texture to a texture asset id or remove the source texture reference."
|
|
273
|
+
})
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
return diagnostics;
|
|
278
|
+
}
|
|
279
|
+
function validateAssetRectMetadata(rect, file, dataPath, assetId) {
|
|
280
|
+
if (!Array.isArray(rect) || rect.length !== 4) {
|
|
281
|
+
return [
|
|
282
|
+
{
|
|
283
|
+
severity: "error",
|
|
284
|
+
code: "ASSET_RECT_INVALID",
|
|
285
|
+
file,
|
|
286
|
+
path: dataPath,
|
|
287
|
+
message: `Sprite asset '${assetId}' rect must be [x, y, width, height].`,
|
|
288
|
+
suggestion: "Use four finite numbers and keep width and height greater than zero."
|
|
289
|
+
}
|
|
290
|
+
];
|
|
291
|
+
}
|
|
292
|
+
const invalidIndex = rect.findIndex((value) => typeof value !== "number" || !Number.isFinite(value));
|
|
293
|
+
if (invalidIndex !== -1) {
|
|
294
|
+
return [
|
|
295
|
+
{
|
|
296
|
+
severity: "error",
|
|
297
|
+
code: "ASSET_RECT_INVALID",
|
|
298
|
+
file,
|
|
299
|
+
path: `${dataPath}[${invalidIndex}]`,
|
|
300
|
+
message: `Sprite asset '${assetId}' rect value at index ${invalidIndex} must be a finite number.`,
|
|
301
|
+
suggestion: "Use numeric [x, y, width, height] atlas planning metadata."
|
|
302
|
+
}
|
|
303
|
+
];
|
|
304
|
+
}
|
|
305
|
+
const [, , width, height] = rect;
|
|
306
|
+
if (width <= 0 || height <= 0) {
|
|
307
|
+
return [
|
|
308
|
+
{
|
|
309
|
+
severity: "error",
|
|
310
|
+
code: "ASSET_RECT_INVALID",
|
|
311
|
+
file,
|
|
312
|
+
path: width <= 0 ? `${dataPath}[2]` : `${dataPath}[3]`,
|
|
313
|
+
message: `Sprite asset '${assetId}' rect width and height must be greater than zero.`,
|
|
314
|
+
suggestion: "Use positive width and height values. No atlas slicing is performed from this metadata."
|
|
315
|
+
}
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
function validateAtlasFramesMetadata(frames, file, dataPath, atlasId) {
|
|
321
|
+
const diagnostics = [];
|
|
322
|
+
const frameIds = /* @__PURE__ */ new Map();
|
|
323
|
+
frames.forEach((frame, index) => {
|
|
324
|
+
const framePath = `${dataPath}[${index}]`;
|
|
325
|
+
if (!isRecord(frame)) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (typeof frame.id === "string") {
|
|
329
|
+
const existingPath = frameIds.get(frame.id);
|
|
330
|
+
if (existingPath) {
|
|
331
|
+
diagnostics.push({
|
|
332
|
+
severity: "error",
|
|
333
|
+
code: "ASSET_ATLAS_DUPLICATE_FRAME_ID",
|
|
334
|
+
file,
|
|
335
|
+
path: `${framePath}.id`,
|
|
336
|
+
message: `Atlas asset '${atlasId}' defines duplicate frame id '${frame.id}'.`,
|
|
337
|
+
suggestion: `Keep one '${frame.id}' frame in this atlas. First occurrence is at ${existingPath}.`
|
|
338
|
+
});
|
|
339
|
+
} else {
|
|
340
|
+
frameIds.set(frame.id, `${framePath}.id`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (Object.prototype.hasOwnProperty.call(frame, "rect")) {
|
|
344
|
+
diagnostics.push(...validateAtlasFrameRectMetadata(frame.rect, file, `${framePath}.rect`, atlasId, String(frame.id)));
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
return diagnostics;
|
|
348
|
+
}
|
|
349
|
+
function validateAtlasFrameRectMetadata(rect, file, dataPath, atlasId, frameId) {
|
|
350
|
+
if (!Array.isArray(rect) || rect.length !== 4) {
|
|
351
|
+
return [
|
|
352
|
+
{
|
|
353
|
+
severity: "error",
|
|
354
|
+
code: "ASSET_ATLAS_FRAME_RECT_INVALID",
|
|
355
|
+
file,
|
|
356
|
+
path: dataPath,
|
|
357
|
+
message: `Atlas frame '${frameId}' in atlas '${atlasId}' rect must be [x, y, width, height].`,
|
|
358
|
+
suggestion: "Use four finite numbers and keep width and height greater than zero."
|
|
359
|
+
}
|
|
360
|
+
];
|
|
361
|
+
}
|
|
362
|
+
const invalidIndex = rect.findIndex((value) => typeof value !== "number" || !Number.isFinite(value));
|
|
363
|
+
if (invalidIndex !== -1) {
|
|
364
|
+
return [
|
|
365
|
+
{
|
|
366
|
+
severity: "error",
|
|
367
|
+
code: "ASSET_ATLAS_FRAME_RECT_INVALID",
|
|
368
|
+
file,
|
|
369
|
+
path: `${dataPath}[${invalidIndex}]`,
|
|
370
|
+
message: `Atlas frame '${frameId}' in atlas '${atlasId}' rect value at index ${invalidIndex} must be a finite number.`,
|
|
371
|
+
suggestion: "Use numeric [x, y, width, height] atlas metadata."
|
|
372
|
+
}
|
|
373
|
+
];
|
|
374
|
+
}
|
|
375
|
+
const [, , width, height] = rect;
|
|
376
|
+
if (width <= 0 || height <= 0) {
|
|
377
|
+
return [
|
|
378
|
+
{
|
|
379
|
+
severity: "error",
|
|
380
|
+
code: "ASSET_ATLAS_FRAME_RECT_INVALID",
|
|
381
|
+
file,
|
|
382
|
+
path: width <= 0 ? `${dataPath}[2]` : `${dataPath}[3]`,
|
|
383
|
+
message: `Atlas frame '${frameId}' in atlas '${atlasId}' rect width and height must be greater than zero.`,
|
|
384
|
+
suggestion: "Use positive width and height values. No atlas slicing is performed from this metadata."
|
|
385
|
+
}
|
|
386
|
+
];
|
|
387
|
+
}
|
|
388
|
+
return [];
|
|
389
|
+
}
|
|
390
|
+
function readAtlasFrameRecords(frames, dataPath) {
|
|
391
|
+
return frames.flatMap((frame, index) => {
|
|
392
|
+
if (!isRecord(frame) || typeof frame.id !== "string") {
|
|
393
|
+
return [];
|
|
394
|
+
}
|
|
395
|
+
const rect = readRectTuple(frame.rect);
|
|
396
|
+
if (!rect) {
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
return [
|
|
400
|
+
{
|
|
401
|
+
id: frame.id,
|
|
402
|
+
rect,
|
|
403
|
+
dataPath: `${dataPath}[${index}]`
|
|
404
|
+
}
|
|
405
|
+
];
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
function validateAssetReferenceToType(input) {
|
|
409
|
+
const referenced = input.assetsById.get(input.referencedId);
|
|
410
|
+
if (!referenced) {
|
|
411
|
+
return [
|
|
412
|
+
{
|
|
413
|
+
severity: "error",
|
|
414
|
+
code: input.missingCode,
|
|
415
|
+
file: input.file,
|
|
416
|
+
path: input.path,
|
|
417
|
+
message: `${input.ownerType} '${input.assetId}' references missing ${input.expectedType} asset '${input.referencedId}'.`,
|
|
418
|
+
suggestion: input.suggestion
|
|
419
|
+
}
|
|
420
|
+
];
|
|
421
|
+
}
|
|
422
|
+
if (referenced.type !== input.expectedType) {
|
|
423
|
+
return [
|
|
424
|
+
{
|
|
425
|
+
severity: "error",
|
|
426
|
+
code: input.mismatchCode,
|
|
427
|
+
file: input.file,
|
|
428
|
+
path: input.path,
|
|
429
|
+
message: `${input.ownerType} '${input.assetId}' references '${input.referencedId}', but it is '${referenced.type}', not '${input.expectedType}'.`,
|
|
430
|
+
suggestion: input.suggestion
|
|
431
|
+
}
|
|
432
|
+
];
|
|
433
|
+
}
|
|
434
|
+
return [];
|
|
435
|
+
}
|
|
436
|
+
function validateSpriteAtlasFrameReference(asset, assetsById, file, assetPath) {
|
|
437
|
+
const hasAtlas = typeof asset.atlas === "string";
|
|
438
|
+
const hasFrame = typeof asset.frame === "string";
|
|
439
|
+
if (!hasAtlas && !hasFrame) {
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
if (!hasAtlas || !hasFrame) {
|
|
443
|
+
return [
|
|
444
|
+
{
|
|
445
|
+
severity: "error",
|
|
446
|
+
code: "ASSET_ATLAS_FRAME_REFERENCE_INCOMPLETE",
|
|
447
|
+
file,
|
|
448
|
+
path: hasAtlas ? `${assetPath}.frame` : `${assetPath}.atlas`,
|
|
449
|
+
message: `Sprite asset '${String(asset.id)}' must set both atlas and frame when referencing atlas metadata.`,
|
|
450
|
+
suggestion: "Set atlas to an atlas asset id and frame to a frame id inside that atlas."
|
|
451
|
+
}
|
|
452
|
+
];
|
|
453
|
+
}
|
|
454
|
+
const atlas = assetsById.get(String(asset.atlas));
|
|
455
|
+
if (!atlas) {
|
|
456
|
+
return [
|
|
457
|
+
{
|
|
458
|
+
severity: "error",
|
|
459
|
+
code: "ASSET_ATLAS_REFERENCE_UNRESOLVED",
|
|
460
|
+
file,
|
|
461
|
+
path: `${assetPath}.atlas`,
|
|
462
|
+
message: `Sprite asset '${String(asset.id)}' references missing atlas asset '${String(asset.atlas)}'.`,
|
|
463
|
+
suggestion: "Add the atlas asset or update SpriteAsset.atlas to an existing atlas id."
|
|
464
|
+
}
|
|
465
|
+
];
|
|
466
|
+
}
|
|
467
|
+
if (atlas.type !== "atlas") {
|
|
468
|
+
return [
|
|
469
|
+
{
|
|
470
|
+
severity: "error",
|
|
471
|
+
code: "ASSET_ATLAS_REFERENCE_TYPE_MISMATCH",
|
|
472
|
+
file,
|
|
473
|
+
path: `${assetPath}.atlas`,
|
|
474
|
+
message: `Sprite asset '${String(asset.id)}' references '${String(asset.atlas)}', but it is '${atlas.type}', not 'atlas'.`,
|
|
475
|
+
suggestion: "Point SpriteAsset.atlas to an atlas asset id."
|
|
476
|
+
}
|
|
477
|
+
];
|
|
478
|
+
}
|
|
479
|
+
const frame = atlas.frames?.find((candidate) => candidate.id === asset.frame);
|
|
480
|
+
if (!frame) {
|
|
481
|
+
return [
|
|
482
|
+
{
|
|
483
|
+
severity: "error",
|
|
484
|
+
code: "ASSET_ATLAS_FRAME_REFERENCE_UNRESOLVED",
|
|
485
|
+
file,
|
|
486
|
+
path: `${assetPath}.frame`,
|
|
487
|
+
message: `Sprite asset '${String(asset.id)}' references missing frame '${String(asset.frame)}' in atlas '${String(asset.atlas)}'.`,
|
|
488
|
+
suggestion: "Use a frame id defined in the referenced atlas asset."
|
|
489
|
+
}
|
|
490
|
+
];
|
|
491
|
+
}
|
|
492
|
+
return [];
|
|
493
|
+
}
|
|
494
|
+
function validateMapGenerationConfigDocument(config, file = "map.yml") {
|
|
495
|
+
const diagnostics = schemaDiagnostics(
|
|
496
|
+
validateMapGenerationConfigSchema,
|
|
497
|
+
config,
|
|
498
|
+
file,
|
|
499
|
+
"MAP_CONFIG_SCHEMA_INVALID"
|
|
500
|
+
);
|
|
501
|
+
if (!isRecord(config) || !Array.isArray(config.prefabRefs)) {
|
|
502
|
+
return diagnostics;
|
|
503
|
+
}
|
|
504
|
+
const prefabRefIds = /* @__PURE__ */ new Set();
|
|
505
|
+
for (const [index, prefabRef] of config.prefabRefs.entries()) {
|
|
506
|
+
if (!isRecord(prefabRef)) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (typeof prefabRef.id === "string") {
|
|
510
|
+
if (prefabRefIds.has(prefabRef.id)) {
|
|
511
|
+
diagnostics.push({
|
|
512
|
+
severity: "error",
|
|
513
|
+
code: "MAP_PREFAB_REF_DUPLICATE",
|
|
514
|
+
file,
|
|
515
|
+
path: `$.prefabRefs[${index}].id`,
|
|
516
|
+
message: `Duplicate prefab ref id '${prefabRef.id}'.`,
|
|
517
|
+
suggestion: "Use one stable prefab ref id once per map generation config."
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
prefabRefIds.add(prefabRef.id);
|
|
521
|
+
}
|
|
522
|
+
if (typeof prefabRef.prefab === "string" && !isPortableSourcePath(prefabRef.prefab)) {
|
|
523
|
+
diagnostics.push({
|
|
524
|
+
severity: "error",
|
|
525
|
+
code: "MAP_PREFAB_PATH_INVALID",
|
|
526
|
+
file,
|
|
527
|
+
path: `$.prefabRefs[${index}].prefab`,
|
|
528
|
+
message: `Prefab path '${prefabRef.prefab}' is not a portable relative source path.`,
|
|
529
|
+
suggestion: "Use a relative path with forward slashes, no drive letters, no leading slash, and no '..' escape."
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const platformCount = typeof config.platformCount === "number" ? config.platformCount : 0;
|
|
534
|
+
const enemyCount = typeof config.enemyCount === "number" ? config.enemyCount : 0;
|
|
535
|
+
const width = typeof config.width === "number" ? config.width : 0;
|
|
536
|
+
const minPlatformSpacing = isRecord(config.constraints) && typeof config.constraints.minPlatformSpacing === "number" ? config.constraints.minPlatformSpacing : 0;
|
|
537
|
+
const minEnemySpacing = isRecord(config.constraints) && typeof config.constraints.minEnemySpacing === "number" ? config.constraints.minEnemySpacing : 0;
|
|
538
|
+
if (platformCount > 0 && !hasPrefabRole(config.prefabRefs, "platform")) {
|
|
539
|
+
diagnostics.push({
|
|
540
|
+
severity: "error",
|
|
541
|
+
code: "MAP_PREFAB_ROLE_REQUIRED",
|
|
542
|
+
file,
|
|
543
|
+
path: "$.prefabRefs",
|
|
544
|
+
message: "platformCount is greater than zero but no platform prefab ref exists.",
|
|
545
|
+
suggestion: "Add a prefabRefs entry with role: platform."
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
if (enemyCount > 0 && !hasPrefabRole(config.prefabRefs, "enemy")) {
|
|
549
|
+
diagnostics.push({
|
|
550
|
+
severity: "error",
|
|
551
|
+
code: "MAP_PREFAB_ROLE_REQUIRED",
|
|
552
|
+
file,
|
|
553
|
+
path: "$.prefabRefs",
|
|
554
|
+
message: "enemyCount is greater than zero but no enemy prefab ref exists.",
|
|
555
|
+
suggestion: "Add a prefabRefs entry with role: enemy."
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
if (enemyCount > 0 && platformCount === 0) {
|
|
559
|
+
diagnostics.push({
|
|
560
|
+
severity: "error",
|
|
561
|
+
code: "MAP_CONSTRAINT_UNSATISFIABLE",
|
|
562
|
+
file,
|
|
563
|
+
path: "$.enemyCount",
|
|
564
|
+
message: "Enemies require at least one generated platform in procedural-map-v0.",
|
|
565
|
+
suggestion: "Increase platformCount or set enemyCount to 0."
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
if (platformCount > 0 && width > 0 && platformCount + minPlatformSpacing * Math.max(platformCount - 1, 0) > width) {
|
|
569
|
+
diagnostics.push({
|
|
570
|
+
severity: "error",
|
|
571
|
+
code: "MAP_CONSTRAINT_UNSATISFIABLE",
|
|
572
|
+
file,
|
|
573
|
+
path: "$.constraints.minPlatformSpacing",
|
|
574
|
+
message: "Platform spacing constraints cannot fit within the requested width.",
|
|
575
|
+
suggestion: "Reduce platformCount, reduce minPlatformSpacing, or increase width."
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
if (enemyCount > 1 && width > 0 && enemyCount + minEnemySpacing * (enemyCount - 1) > width) {
|
|
579
|
+
diagnostics.push({
|
|
580
|
+
severity: "error",
|
|
581
|
+
code: "MAP_CONSTRAINT_UNSATISFIABLE",
|
|
582
|
+
file,
|
|
583
|
+
path: "$.constraints.minEnemySpacing",
|
|
584
|
+
message: "Enemy spacing constraints cannot fit within the requested width.",
|
|
585
|
+
suggestion: "Reduce enemyCount, reduce minEnemySpacing, or increase width."
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
return diagnostics;
|
|
589
|
+
}
|
|
590
|
+
function validateGeneratedMapDocument(map, file = "generated-map.yml") {
|
|
591
|
+
const diagnostics = schemaDiagnostics(validateGeneratedMapSchema, map, file, "GENERATED_MAP_SCHEMA_INVALID");
|
|
592
|
+
if (!isRecord(map) || !isRecord(map.config) || !isRecord(map.stats) || !Array.isArray(map.entities)) {
|
|
593
|
+
return diagnostics;
|
|
594
|
+
}
|
|
595
|
+
const configDiagnostics = validateMapGenerationConfigDocument(map.config, file).map((diagnostic) => ({
|
|
596
|
+
...diagnostic,
|
|
597
|
+
path: diagnostic.path === "$" ? "$.config" : `$.config${diagnostic.path.slice(1)}`
|
|
598
|
+
}));
|
|
599
|
+
diagnostics.push(...configDiagnostics);
|
|
600
|
+
const prefabRefs = Array.isArray(map.config.prefabRefs) ? map.config.prefabRefs : [];
|
|
601
|
+
const prefabRefIds = new Set(
|
|
602
|
+
prefabRefs.filter(isRecord).map((prefabRef) => prefabRef.id).filter((id) => typeof id === "string")
|
|
603
|
+
);
|
|
604
|
+
let platformCount = 0;
|
|
605
|
+
let enemyCount = 0;
|
|
606
|
+
for (const [index, entity] of map.entities.entries()) {
|
|
607
|
+
const entityPath = `$.entities[${index}]`;
|
|
608
|
+
if (!isRecord(entity)) {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (typeof entity.prefabRef === "string" && !prefabRefIds.has(entity.prefabRef)) {
|
|
612
|
+
diagnostics.push({
|
|
613
|
+
severity: "error",
|
|
614
|
+
code: "GENERATED_MAP_PREFAB_REF_MISSING",
|
|
615
|
+
file,
|
|
616
|
+
path: `${entityPath}.prefabRef`,
|
|
617
|
+
message: `Generated entity references unknown prefab ref '${entity.prefabRef}'.`,
|
|
618
|
+
suggestion: "Regenerate the map from a config containing that prefab ref id."
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
if (entity.role === "platform") {
|
|
622
|
+
platformCount += 1;
|
|
623
|
+
}
|
|
624
|
+
if (entity.role === "enemy") {
|
|
625
|
+
enemyCount += 1;
|
|
626
|
+
}
|
|
627
|
+
if (Array.isArray(entity.position) && typeof map.stats.width === "number" && typeof map.stats.height === "number") {
|
|
628
|
+
const [x, y] = entity.position;
|
|
629
|
+
if (typeof x === "number" && typeof y === "number" && (x < 0 || y < 0 || x >= map.stats.width || y >= map.stats.height)) {
|
|
630
|
+
diagnostics.push({
|
|
631
|
+
severity: "error",
|
|
632
|
+
code: "GENERATED_MAP_ENTITY_OUT_OF_BOUNDS",
|
|
633
|
+
file,
|
|
634
|
+
path: `${entityPath}.position`,
|
|
635
|
+
message: `Generated entity '${String(entity.id)}' is outside the map bounds.`,
|
|
636
|
+
suggestion: "Regenerate the map with valid constraints or adjust the dimensions."
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (typeof map.stats.platformCount === "number" && map.stats.platformCount !== platformCount) {
|
|
642
|
+
diagnostics.push({
|
|
643
|
+
severity: "error",
|
|
644
|
+
code: "GENERATED_MAP_STATS_MISMATCH",
|
|
645
|
+
file,
|
|
646
|
+
path: "$.stats.platformCount",
|
|
647
|
+
message: `Generated map stats report ${map.stats.platformCount} platforms, but entities contain ${platformCount}.`,
|
|
648
|
+
suggestion: "Regenerate the map so stats match generated entities."
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
if (typeof map.stats.enemyCount === "number" && map.stats.enemyCount !== enemyCount) {
|
|
652
|
+
diagnostics.push({
|
|
653
|
+
severity: "error",
|
|
654
|
+
code: "GENERATED_MAP_STATS_MISMATCH",
|
|
655
|
+
file,
|
|
656
|
+
path: "$.stats.enemyCount",
|
|
657
|
+
message: `Generated map stats report ${map.stats.enemyCount} enemies, but entities contain ${enemyCount}.`,
|
|
658
|
+
suggestion: "Regenerate the map so stats match generated entities."
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
return diagnostics;
|
|
662
|
+
}
|
|
663
|
+
function validatePatchDocument(patch, file = "patch.json") {
|
|
664
|
+
const diagnostics = schemaDiagnostics(validatePatchSchema, patch, file, "PATCH_SCHEMA_INVALID");
|
|
665
|
+
if (!isRecord(patch) || !Array.isArray(patch.operations)) {
|
|
666
|
+
return diagnostics;
|
|
667
|
+
}
|
|
668
|
+
patch.operations.forEach((operation, index) => {
|
|
669
|
+
const operationPath = `$.operations[${index}]`;
|
|
670
|
+
if (!isRecord(operation)) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (typeof operation.op === "string" && !supportedPatchOperations.includes(operation.op)) {
|
|
674
|
+
diagnostics.push({
|
|
675
|
+
severity: "error",
|
|
676
|
+
code: "PATCH_UNKNOWN_OPERATION",
|
|
677
|
+
file,
|
|
678
|
+
path: `${operationPath}.op`,
|
|
679
|
+
message: `Unknown patch operation '${operation.op}'.`,
|
|
680
|
+
suggestion: `Use one of: ${supportedPatchOperations.join(", ")}.`
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
if ((operation.op === "add_component" || operation.op === "set_component_field" || operation.op === "update_component") && typeof operation.component === "string" && !componentValidators[operation.component]) {
|
|
684
|
+
diagnostics.push({
|
|
685
|
+
severity: "error",
|
|
686
|
+
code: "PATCH_UNKNOWN_COMPONENT",
|
|
687
|
+
file,
|
|
688
|
+
path: `${operationPath}.component`,
|
|
689
|
+
message: `Unknown component '${operation.component}'.`,
|
|
690
|
+
suggestion: `Use one of: ${componentOrder.join(", ")}.`
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
if (operation.op === "duplicate_entity" && isRecord(operation.overrides)) {
|
|
694
|
+
for (const componentName of Object.keys(operation.overrides)) {
|
|
695
|
+
if (!componentValidators[componentName]) {
|
|
696
|
+
diagnostics.push({
|
|
697
|
+
severity: "error",
|
|
698
|
+
code: "PATCH_UNKNOWN_COMPONENT",
|
|
699
|
+
file,
|
|
700
|
+
path: `${operationPath}.overrides.${componentName}`,
|
|
701
|
+
message: `Unknown component '${componentName}'.`,
|
|
702
|
+
suggestion: `Use one of: ${componentOrder.join(", ")}.`
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
return diagnostics;
|
|
709
|
+
}
|
|
710
|
+
function validatePrefabDocument(prefab, file = "prefab.yml") {
|
|
711
|
+
const diagnostics = schemaDiagnostics(validatePrefabSchema, prefab, file, "PREFAB_SCHEMA_INVALID");
|
|
712
|
+
if (!isRecord(prefab) || !isRecord(prefab.components)) {
|
|
713
|
+
return diagnostics;
|
|
714
|
+
}
|
|
715
|
+
for (const [componentName, componentValue] of Object.entries(prefab.components)) {
|
|
716
|
+
const componentPath = `$.components.${componentName}`;
|
|
717
|
+
const componentValidator = componentValidators[componentName];
|
|
718
|
+
if (!componentValidator) {
|
|
719
|
+
diagnostics.push({
|
|
720
|
+
severity: "error",
|
|
721
|
+
code: "PREFAB_UNKNOWN_COMPONENT",
|
|
722
|
+
file,
|
|
723
|
+
path: componentPath,
|
|
724
|
+
message: `Unknown component '${componentName}'.`,
|
|
725
|
+
suggestion: `Use one of: ${componentOrder.join(", ")}.`
|
|
726
|
+
});
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
diagnostics.push(
|
|
730
|
+
...schemaDiagnostics(componentValidator, componentValue, file, "PREFAB_COMPONENT_INVALID", componentPath)
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
return diagnostics;
|
|
734
|
+
}
|
|
735
|
+
async function validatePrefabFile(prefabPath, options = {}) {
|
|
736
|
+
const result = await loadPrefabFile(prefabPath, options);
|
|
737
|
+
if (result.document !== void 0) {
|
|
738
|
+
result.diagnostics.push(...validatePrefabDocument(result.document, result.file));
|
|
739
|
+
}
|
|
740
|
+
return result;
|
|
741
|
+
}
|
|
742
|
+
async function validateAssetManifestFile(assetManifestPath, options = {}) {
|
|
743
|
+
const result = await loadAssetManifestFile(assetManifestPath, options);
|
|
744
|
+
if (result.document !== void 0) {
|
|
745
|
+
result.diagnostics.push(...validateAssetManifestDocument(result.document, result.file));
|
|
746
|
+
}
|
|
747
|
+
return result;
|
|
748
|
+
}
|
|
749
|
+
async function validateMapGenerationConfigFile(configPath, options = {}) {
|
|
750
|
+
const result = await loadMapGenerationConfigFile(configPath, options);
|
|
751
|
+
if (result.document === void 0) {
|
|
752
|
+
return result;
|
|
753
|
+
}
|
|
754
|
+
result.diagnostics.push(...validateMapGenerationConfigDocument(result.document, result.file));
|
|
755
|
+
if (hasErrors(result.diagnostics)) {
|
|
756
|
+
return result;
|
|
757
|
+
}
|
|
758
|
+
result.diagnostics.push(...await validateMapPrefabRefs(result.document, result.file));
|
|
759
|
+
return result;
|
|
760
|
+
}
|
|
761
|
+
async function validateGeneratedMapFile(generatedMapPath, options = {}) {
|
|
762
|
+
const result = await loadGeneratedMapFile(generatedMapPath, options);
|
|
763
|
+
if (result.document !== void 0) {
|
|
764
|
+
result.diagnostics.push(...validateGeneratedMapDocument(result.document, result.file));
|
|
765
|
+
}
|
|
766
|
+
return result;
|
|
767
|
+
}
|
|
768
|
+
async function loadProjectAssetManifests(projectPath = ".", options = {}) {
|
|
769
|
+
const projectFile = resolveProjectFile(projectPath, options.cwd);
|
|
770
|
+
const rootPath = path.dirname(projectFile);
|
|
771
|
+
const projectResult = await loadProjectFile(projectFile);
|
|
772
|
+
const diagnostics = [...projectResult.diagnostics];
|
|
773
|
+
if (projectResult.document !== void 0) {
|
|
774
|
+
diagnostics.push(...validateProjectDocument(projectResult.document, projectFile));
|
|
775
|
+
}
|
|
776
|
+
if (projectResult.document === void 0 || hasErrors(diagnostics)) {
|
|
777
|
+
return {
|
|
778
|
+
projectFile,
|
|
779
|
+
rootPath,
|
|
780
|
+
manifestFiles: [],
|
|
781
|
+
assets: [],
|
|
782
|
+
assetsById: /* @__PURE__ */ new Map(),
|
|
783
|
+
diagnostics
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
const context = await loadProjectAssetManifestContext(projectFile, projectResult.document);
|
|
787
|
+
return {
|
|
788
|
+
projectFile,
|
|
789
|
+
rootPath,
|
|
790
|
+
manifestFiles: context.manifestFiles,
|
|
791
|
+
assets: context.assets,
|
|
792
|
+
assetsById: context.assetsById,
|
|
793
|
+
diagnostics: [...diagnostics, ...context.diagnostics]
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
async function loadProjectAssetManifestContext(projectFile, project) {
|
|
797
|
+
const manifestRefs = project.assetManifests ?? [];
|
|
798
|
+
const context = createAssetManifestContext();
|
|
799
|
+
const rootPath = path.dirname(projectFile);
|
|
800
|
+
for (const [manifestIndex, manifestRef] of manifestRefs.entries()) {
|
|
801
|
+
const manifestFile = path.resolve(rootPath, manifestRef);
|
|
802
|
+
context.manifestFiles.push(manifestFile);
|
|
803
|
+
if (!existsSync(manifestFile)) {
|
|
804
|
+
context.diagnostics.push({
|
|
805
|
+
severity: "error",
|
|
806
|
+
code: "PROJECT_ASSET_MANIFEST_NOT_FOUND",
|
|
807
|
+
file: projectFile,
|
|
808
|
+
path: `$.assetManifests[${manifestIndex}]`,
|
|
809
|
+
message: `Asset manifest '${manifestRef}' was not found.`,
|
|
810
|
+
suggestion: "Create the asset manifest file or remove the path from assetManifests."
|
|
811
|
+
});
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
const manifestResult = await validateAssetManifestFile(manifestFile);
|
|
815
|
+
context.diagnostics.push(...manifestResult.diagnostics);
|
|
816
|
+
if (!manifestResult.document || hasErrors(manifestResult.diagnostics)) {
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
for (const [assetIndex, asset] of manifestResult.document.assets.entries()) {
|
|
820
|
+
const existing = context.assetsById.get(asset.id);
|
|
821
|
+
const assetPath = `$.assets[${assetIndex}].id`;
|
|
822
|
+
if (existing) {
|
|
823
|
+
context.diagnostics.push({
|
|
824
|
+
severity: "error",
|
|
825
|
+
code: "ASSET_DUPLICATE_ID",
|
|
826
|
+
file: manifestFile,
|
|
827
|
+
path: assetPath,
|
|
828
|
+
message: `Asset id '${asset.id}' already exists in '${existing.manifestFile}'.`,
|
|
829
|
+
suggestion: "Use one stable asset id once across all project asset manifests."
|
|
830
|
+
});
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
const record = {
|
|
834
|
+
id: asset.id,
|
|
835
|
+
type: asset.type,
|
|
836
|
+
path: asset.path,
|
|
837
|
+
...(asset.type === "sprite" || asset.type === "atlas") && typeof asset.texture === "string" ? { texture: asset.texture } : {},
|
|
838
|
+
...asset.type === "sprite" && Array.isArray(asset.rect) ? { rect: asset.rect } : {},
|
|
839
|
+
...asset.type === "sprite" && typeof asset.atlas === "string" ? { atlas: asset.atlas } : {},
|
|
840
|
+
...asset.type === "sprite" && typeof asset.frame === "string" ? { frame: asset.frame } : {},
|
|
841
|
+
...asset.type === "atlas" ? { frames: readAtlasFrameRecords(asset.frames, `$.assets[${assetIndex}].frames`) } : {},
|
|
842
|
+
manifestFile,
|
|
843
|
+
dataPath: `$.assets[${assetIndex}]`
|
|
844
|
+
};
|
|
845
|
+
context.assetsById.set(asset.id, record);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
context.assets = sortedAssetDefinitions(context.assetsById);
|
|
849
|
+
return context;
|
|
850
|
+
}
|
|
851
|
+
function validateSceneDocument(scene, file = "scene.yml", options = {}) {
|
|
852
|
+
const diagnostics = schemaDiagnostics(validateSceneSchema, scene, file, "SCENE_SCHEMA_INVALID");
|
|
853
|
+
if (!isRecord(scene) || !Array.isArray(scene.entities)) {
|
|
854
|
+
return diagnostics;
|
|
855
|
+
}
|
|
856
|
+
const ids = /* @__PURE__ */ new Set();
|
|
857
|
+
scene.entities.forEach((entity, index) => {
|
|
858
|
+
if (!isRecord(entity)) {
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (typeof entity.id === "string") {
|
|
862
|
+
if (ids.has(entity.id)) {
|
|
863
|
+
diagnostics.push({
|
|
864
|
+
severity: "error",
|
|
865
|
+
code: "SCENE_DUPLICATE_ENTITY_ID",
|
|
866
|
+
file,
|
|
867
|
+
path: `$.entities[${index}].id`,
|
|
868
|
+
message: `Duplicate entity id '${entity.id}'.`,
|
|
869
|
+
suggestion: "Give every entity a unique stable id."
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
ids.add(entity.id);
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
scene.entities.forEach((entity, entityIndex) => {
|
|
876
|
+
if (!isRecord(entity) || !isRecord(entity.components)) {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
for (const [componentName, componentValue] of Object.entries(entity.components)) {
|
|
880
|
+
const componentPath = `$.entities[${entityIndex}].components.${componentName}`;
|
|
881
|
+
const componentValidator = componentValidators[componentName];
|
|
882
|
+
if (!componentValidator) {
|
|
883
|
+
diagnostics.push({
|
|
884
|
+
severity: "error",
|
|
885
|
+
code: "SCENE_UNKNOWN_COMPONENT",
|
|
886
|
+
file,
|
|
887
|
+
path: componentPath,
|
|
888
|
+
message: `Unknown component '${componentName}'.`,
|
|
889
|
+
suggestion: `Use one of: ${componentOrder.join(", ")}.`
|
|
890
|
+
});
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
diagnostics.push(
|
|
894
|
+
...schemaDiagnostics(componentValidator, componentValue, file, "COMPONENT_SCHEMA_INVALID", componentPath)
|
|
895
|
+
);
|
|
896
|
+
for (const reference of findEntityReferences(scene).filter(
|
|
897
|
+
(candidate) => candidate.path.startsWith(componentPath)
|
|
898
|
+
)) {
|
|
899
|
+
if (!ids.has(reference.value)) {
|
|
900
|
+
diagnostics.push({
|
|
901
|
+
severity: "error",
|
|
902
|
+
code: "SCENE_BROKEN_ENTITY_REFERENCE",
|
|
903
|
+
file,
|
|
904
|
+
path: reference.path,
|
|
905
|
+
message: `Entity reference '${reference.value}' does not resolve.`,
|
|
906
|
+
suggestion: "Use the stable id of an entity in this scene."
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
diagnostics.push(...validateHierarchyRules(scene, file, ids));
|
|
913
|
+
diagnostics.push(...validateAnimationRules(scene, file));
|
|
914
|
+
diagnostics.push(...validate3DContractRules(scene, file));
|
|
915
|
+
if (options.assets) {
|
|
916
|
+
diagnostics.push(...validateSceneAssetReferences(scene, file, options.assets));
|
|
917
|
+
}
|
|
918
|
+
return diagnostics;
|
|
919
|
+
}
|
|
920
|
+
async function validateProjectFile(projectPath = ".", options = {}) {
|
|
921
|
+
const projectFile = resolveProjectFile(projectPath, options.cwd);
|
|
922
|
+
const result = await loadYamlFile(projectFile);
|
|
923
|
+
if (result.document === void 0) {
|
|
924
|
+
return result;
|
|
925
|
+
}
|
|
926
|
+
result.diagnostics.push(...validateProjectDocument(result.document, projectFile));
|
|
927
|
+
if (hasErrors(result.diagnostics)) {
|
|
928
|
+
return result;
|
|
929
|
+
}
|
|
930
|
+
const rootPath = path.dirname(projectFile);
|
|
931
|
+
const assetContext = await loadProjectAssetManifestContext(projectFile, result.document);
|
|
932
|
+
result.diagnostics.push(...assetContext.diagnostics);
|
|
933
|
+
if (hasErrors(result.diagnostics)) {
|
|
934
|
+
return result;
|
|
935
|
+
}
|
|
936
|
+
for (const sceneRef of result.document.scenes) {
|
|
937
|
+
const sceneFile = path.resolve(rootPath, sceneRef);
|
|
938
|
+
if (!existsSync(sceneFile)) {
|
|
939
|
+
result.diagnostics.push({
|
|
940
|
+
severity: "error",
|
|
941
|
+
code: "PROJECT_SCENE_NOT_FOUND",
|
|
942
|
+
file: projectFile,
|
|
943
|
+
path: "$.scenes",
|
|
944
|
+
message: `Scene file '${sceneRef}' was not found.`,
|
|
945
|
+
suggestion: "Create the scene file or remove the scene path from the project."
|
|
946
|
+
});
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
const sceneResult = await validateSceneFile(sceneFile, { assets: assetContext });
|
|
950
|
+
result.diagnostics.push(...sceneResult.diagnostics);
|
|
951
|
+
}
|
|
952
|
+
return result;
|
|
953
|
+
}
|
|
954
|
+
async function validateSceneFile(scenePath, options = {}) {
|
|
955
|
+
const file = path.resolve(options.cwd ?? process.cwd(), scenePath);
|
|
956
|
+
const result = await loadYamlFile(file);
|
|
957
|
+
if (result.document !== void 0) {
|
|
958
|
+
const expanded = await expandSceneDocument(result.document, { sceneFile: file, assets: options.assets });
|
|
959
|
+
result.diagnostics.push(...expanded.diagnostics);
|
|
960
|
+
}
|
|
961
|
+
return result;
|
|
962
|
+
}
|
|
963
|
+
async function loadExpandedSceneFile(scenePath, options = {}) {
|
|
964
|
+
const file = path.resolve(options.cwd ?? process.cwd(), scenePath);
|
|
965
|
+
const result = await loadYamlFile(file);
|
|
966
|
+
if (result.document === void 0) {
|
|
967
|
+
return result;
|
|
968
|
+
}
|
|
969
|
+
const expanded = await expandSceneDocument(result.document, { sceneFile: file, assets: options.assets });
|
|
970
|
+
return {
|
|
971
|
+
file,
|
|
972
|
+
document: expanded.document,
|
|
973
|
+
diagnostics: [...result.diagnostics, ...expanded.diagnostics]
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
async function expandSceneDocument(scene, options = {}) {
|
|
977
|
+
const sceneFile = options.sceneFile ?? "scene.yml";
|
|
978
|
+
const diagnostics = validateSceneDocument(scene, sceneFile, { assets: options.assets });
|
|
979
|
+
if (hasErrors(diagnostics)) {
|
|
980
|
+
return {
|
|
981
|
+
file: sceneFile,
|
|
982
|
+
diagnostics
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
const expandedEntities = [];
|
|
986
|
+
for (const [entityIndex, entity] of scene.entities.entries()) {
|
|
987
|
+
if (typeof entity.prefab !== "string") {
|
|
988
|
+
expandedEntities.push({
|
|
989
|
+
id: entity.id,
|
|
990
|
+
components: cloneJson(entity.components ?? {})
|
|
991
|
+
});
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
const prefabFile = resolveScenePrefabPath(sceneFile, entity.prefab);
|
|
995
|
+
const prefabResult = await loadPrefabFile(prefabFile);
|
|
996
|
+
const prefabDiagnostics = [...prefabResult.diagnostics];
|
|
997
|
+
if (prefabResult.document !== void 0) {
|
|
998
|
+
prefabDiagnostics.push(...validatePrefabDocument(prefabResult.document, prefabResult.file));
|
|
999
|
+
}
|
|
1000
|
+
if (prefabResult.document === void 0 || hasErrors(prefabDiagnostics)) {
|
|
1001
|
+
if (prefabResult.diagnostics.some((diagnostic) => diagnostic.code === "FILE_READ_FAILED")) {
|
|
1002
|
+
diagnostics.push({
|
|
1003
|
+
severity: "error",
|
|
1004
|
+
code: "SCENE_PREFAB_NOT_FOUND",
|
|
1005
|
+
file: sceneFile,
|
|
1006
|
+
path: `$.entities[${entityIndex}].prefab`,
|
|
1007
|
+
message: `Prefab file '${entity.prefab}' was not found.`,
|
|
1008
|
+
suggestion: "Create the prefab file or update the prefab path relative to the scene file."
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
diagnostics.push(...prefabDiagnostics);
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
const overrideDiagnostics = validatePrefabOverrides(entity, prefabResult.document, sceneFile, entityIndex);
|
|
1015
|
+
diagnostics.push(...overrideDiagnostics);
|
|
1016
|
+
if (hasErrors(overrideDiagnostics)) {
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
expandedEntities.push({
|
|
1020
|
+
id: entity.id,
|
|
1021
|
+
components: mergePrefabComponents(prefabResult.document.components, entity.overrides)
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
if (hasErrors(diagnostics)) {
|
|
1025
|
+
return {
|
|
1026
|
+
file: sceneFile,
|
|
1027
|
+
diagnostics
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
const expandedScene = {
|
|
1031
|
+
version: scene.version,
|
|
1032
|
+
name: scene.name,
|
|
1033
|
+
entities: expandedEntities
|
|
1034
|
+
};
|
|
1035
|
+
diagnostics.push(...validateSceneDocument(expandedScene, sceneFile, { assets: options.assets }));
|
|
1036
|
+
return {
|
|
1037
|
+
file: sceneFile,
|
|
1038
|
+
document: hasErrors(diagnostics) ? void 0 : expandedScene,
|
|
1039
|
+
diagnostics
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
function formatProject(project) {
|
|
1043
|
+
return toYaml({
|
|
1044
|
+
version: project.version,
|
|
1045
|
+
name: project.name,
|
|
1046
|
+
defaultScene: project.defaultScene,
|
|
1047
|
+
...project.assetManifests ? { assetManifests: [...project.assetManifests] } : {},
|
|
1048
|
+
scenes: [...project.scenes]
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
function formatScene(scene) {
|
|
1052
|
+
return toYaml({
|
|
1053
|
+
version: scene.version,
|
|
1054
|
+
name: scene.name,
|
|
1055
|
+
entities: scene.entities.map((entity) => {
|
|
1056
|
+
if (typeof entity.prefab === "string") {
|
|
1057
|
+
return {
|
|
1058
|
+
id: entity.id,
|
|
1059
|
+
prefab: entity.prefab,
|
|
1060
|
+
...entity.overrides ? { overrides: orderComponents(entity.overrides) } : {}
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
return {
|
|
1064
|
+
id: entity.id,
|
|
1065
|
+
components: orderComponents(entity.components ?? {})
|
|
1066
|
+
};
|
|
1067
|
+
})
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
function formatPrefab(prefab) {
|
|
1071
|
+
return toYaml({
|
|
1072
|
+
prefab: prefab.prefab,
|
|
1073
|
+
version: prefab.version,
|
|
1074
|
+
components: orderComponents(prefab.components)
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
function formatMapGenerationConfig(config) {
|
|
1078
|
+
return toYaml({
|
|
1079
|
+
version: config.version,
|
|
1080
|
+
name: config.name,
|
|
1081
|
+
seed: config.seed,
|
|
1082
|
+
width: config.width,
|
|
1083
|
+
height: config.height,
|
|
1084
|
+
platformCount: config.platformCount,
|
|
1085
|
+
enemyCount: config.enemyCount,
|
|
1086
|
+
prefabRefs: config.prefabRefs.map((prefabRef) => ({
|
|
1087
|
+
id: prefabRef.id,
|
|
1088
|
+
prefab: prefabRef.prefab,
|
|
1089
|
+
role: prefabRef.role
|
|
1090
|
+
})),
|
|
1091
|
+
...config.tileSetRef ? { tileSetRef: config.tileSetRef } : {},
|
|
1092
|
+
...config.goal ? { goal: config.goal } : {},
|
|
1093
|
+
...config.constraints ? { constraints: orderObject(config.constraints) } : {}
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
function formatGeneratedMap(map) {
|
|
1097
|
+
return toYaml({
|
|
1098
|
+
generatedMap: map.generatedMap,
|
|
1099
|
+
version: map.version,
|
|
1100
|
+
generator: map.generator,
|
|
1101
|
+
config: parse(formatMapGenerationConfig(map.config)),
|
|
1102
|
+
stats: orderObject(map.stats),
|
|
1103
|
+
regions: map.regions.map((region) => ({
|
|
1104
|
+
id: region.id,
|
|
1105
|
+
x: region.x,
|
|
1106
|
+
y: region.y,
|
|
1107
|
+
width: region.width,
|
|
1108
|
+
height: region.height
|
|
1109
|
+
})),
|
|
1110
|
+
entities: map.entities.map((entity) => ({
|
|
1111
|
+
id: entity.id,
|
|
1112
|
+
prefabRef: entity.prefabRef,
|
|
1113
|
+
role: entity.role,
|
|
1114
|
+
position: entity.position,
|
|
1115
|
+
...entity.size ? { size: entity.size } : {}
|
|
1116
|
+
}))
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
async function generateMapFromConfigFile(configPath, options = {}) {
|
|
1120
|
+
const configResult = await validateMapGenerationConfigFile(configPath, options);
|
|
1121
|
+
const diagnostics = [...configResult.diagnostics];
|
|
1122
|
+
if (!configResult.document || hasErrors(diagnostics)) {
|
|
1123
|
+
return {
|
|
1124
|
+
configPath: configResult.file,
|
|
1125
|
+
diagnostics,
|
|
1126
|
+
config: configResult.document
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
const generated = generateMapDocument(configResult.document, { configFile: configResult.file });
|
|
1130
|
+
diagnostics.push(...generated.diagnostics);
|
|
1131
|
+
if (!generated.document || hasErrors(diagnostics)) {
|
|
1132
|
+
return {
|
|
1133
|
+
configPath: configResult.file,
|
|
1134
|
+
diagnostics,
|
|
1135
|
+
config: configResult.document
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
const scene = generatedMapToScene(generated.document);
|
|
1139
|
+
return {
|
|
1140
|
+
configPath: configResult.file,
|
|
1141
|
+
diagnostics,
|
|
1142
|
+
config: configResult.document,
|
|
1143
|
+
map: generated.document,
|
|
1144
|
+
scene,
|
|
1145
|
+
summary: createMapGenerationSummary(generated.document)
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
function generateMapDocument(config, options = {}) {
|
|
1149
|
+
const file = options.configFile ?? "map.yml";
|
|
1150
|
+
const diagnostics = validateMapGenerationConfigDocument(config, file);
|
|
1151
|
+
if (hasErrors(diagnostics)) {
|
|
1152
|
+
return {
|
|
1153
|
+
file,
|
|
1154
|
+
diagnostics
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
const configHash = hashStableObject(config);
|
|
1158
|
+
const rng = createSeededRandom(config.seed);
|
|
1159
|
+
const platformRef = findMapPrefabRefByRole(config.prefabRefs, "platform");
|
|
1160
|
+
const enemyRef = findMapPrefabRefByRole(config.prefabRefs, "enemy");
|
|
1161
|
+
const goalRef = findMapPrefabRefByRole(config.prefabRefs, "goal");
|
|
1162
|
+
const entities = [];
|
|
1163
|
+
const platforms = [];
|
|
1164
|
+
if (config.platformCount > 0 && !platformRef) {
|
|
1165
|
+
return {
|
|
1166
|
+
file,
|
|
1167
|
+
diagnostics: [
|
|
1168
|
+
...diagnostics,
|
|
1169
|
+
{
|
|
1170
|
+
severity: "error",
|
|
1171
|
+
code: "MAP_PREFAB_ROLE_REQUIRED",
|
|
1172
|
+
file,
|
|
1173
|
+
path: "$.prefabRefs",
|
|
1174
|
+
message: "platformCount is greater than zero but no platform prefab ref exists.",
|
|
1175
|
+
suggestion: "Add a prefabRefs entry with role: platform."
|
|
1176
|
+
}
|
|
1177
|
+
]
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
if (config.enemyCount > 0 && !enemyRef) {
|
|
1181
|
+
return {
|
|
1182
|
+
file,
|
|
1183
|
+
diagnostics: [
|
|
1184
|
+
...diagnostics,
|
|
1185
|
+
{
|
|
1186
|
+
severity: "error",
|
|
1187
|
+
code: "MAP_PREFAB_ROLE_REQUIRED",
|
|
1188
|
+
file,
|
|
1189
|
+
path: "$.prefabRefs",
|
|
1190
|
+
message: "enemyCount is greater than zero but no enemy prefab ref exists.",
|
|
1191
|
+
suggestion: "Add a prefabRefs entry with role: enemy."
|
|
1192
|
+
}
|
|
1193
|
+
]
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
if (platformRef) {
|
|
1197
|
+
const platformWidth = Math.max(2, Math.floor(config.width / Math.max(config.platformCount * 2, 1)));
|
|
1198
|
+
const slotWidth = Math.max(1, Math.floor(config.width / Math.max(config.platformCount, 1)));
|
|
1199
|
+
for (let index = 0; index < config.platformCount; index += 1) {
|
|
1200
|
+
const slotStart = index * slotWidth;
|
|
1201
|
+
const slotEnd = index === config.platformCount - 1 ? config.width - platformWidth : Math.min(config.width - platformWidth, (index + 1) * slotWidth - platformWidth);
|
|
1202
|
+
const x = clampInteger(randomInteger(rng, slotStart, Math.max(slotStart, slotEnd)), 0, Math.max(config.width - platformWidth, 0));
|
|
1203
|
+
const y = index === 0 ? 0 : randomInteger(rng, 0, Math.max(config.height - 2, 0));
|
|
1204
|
+
const platform = {
|
|
1205
|
+
id: `platform_${padNumber(index + 1)}`,
|
|
1206
|
+
prefabRef: platformRef.id,
|
|
1207
|
+
role: "platform",
|
|
1208
|
+
position: [x, y],
|
|
1209
|
+
size: [platformWidth, 1]
|
|
1210
|
+
};
|
|
1211
|
+
platforms.push(platform);
|
|
1212
|
+
entities.push(platform);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
if (enemyRef) {
|
|
1216
|
+
for (let index = 0; index < config.enemyCount; index += 1) {
|
|
1217
|
+
const platform = platforms[index % Math.max(platforms.length, 1)];
|
|
1218
|
+
if (!platform) {
|
|
1219
|
+
diagnostics.push({
|
|
1220
|
+
severity: "error",
|
|
1221
|
+
code: "MAP_CONSTRAINT_UNSATISFIABLE",
|
|
1222
|
+
file,
|
|
1223
|
+
path: "$.enemyCount",
|
|
1224
|
+
message: "Enemies require generated platforms in procedural-map-v0.",
|
|
1225
|
+
suggestion: "Increase platformCount or set enemyCount to 0."
|
|
1226
|
+
});
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
const width = Array.isArray(platform.size) ? Math.max(Math.floor(platform.size[0]), 1) : 1;
|
|
1230
|
+
entities.push({
|
|
1231
|
+
id: `enemy_${padNumber(index + 1)}`,
|
|
1232
|
+
prefabRef: enemyRef.id,
|
|
1233
|
+
role: "enemy",
|
|
1234
|
+
position: [
|
|
1235
|
+
clampInteger(platform.position[0] + randomInteger(rng, 0, width - 1), 0, config.width - 1),
|
|
1236
|
+
clampInteger(platform.position[1] + 1, 0, config.height - 1)
|
|
1237
|
+
]
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
if (goalRef) {
|
|
1242
|
+
entities.push({
|
|
1243
|
+
id: config.goal?.targetEntityId ?? "goal",
|
|
1244
|
+
prefabRef: goalRef.id,
|
|
1245
|
+
role: "goal",
|
|
1246
|
+
position: [config.width - 1, Math.max(config.height - 1, 0)]
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
if (hasErrors(diagnostics)) {
|
|
1250
|
+
return {
|
|
1251
|
+
file,
|
|
1252
|
+
diagnostics
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
const map = {
|
|
1256
|
+
generatedMap: config.name,
|
|
1257
|
+
version: 1,
|
|
1258
|
+
generator: "procedural-map-v0",
|
|
1259
|
+
config: cloneJson(config),
|
|
1260
|
+
stats: {
|
|
1261
|
+
seed: config.seed,
|
|
1262
|
+
width: config.width,
|
|
1263
|
+
height: config.height,
|
|
1264
|
+
platformCount: config.platformCount,
|
|
1265
|
+
enemyCount: config.enemyCount,
|
|
1266
|
+
configHash
|
|
1267
|
+
},
|
|
1268
|
+
regions: [
|
|
1269
|
+
{
|
|
1270
|
+
id: "world",
|
|
1271
|
+
x: 0,
|
|
1272
|
+
y: 0,
|
|
1273
|
+
width: config.width,
|
|
1274
|
+
height: config.height
|
|
1275
|
+
}
|
|
1276
|
+
],
|
|
1277
|
+
entities
|
|
1278
|
+
};
|
|
1279
|
+
diagnostics.push(...validateGeneratedMapDocument(map, file));
|
|
1280
|
+
return {
|
|
1281
|
+
file,
|
|
1282
|
+
document: hasErrors(diagnostics) ? void 0 : map,
|
|
1283
|
+
diagnostics
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
function generatedMapToScene(map, options = {}) {
|
|
1287
|
+
const prefabRefsById = new Map(map.config.prefabRefs.map((prefabRef) => [prefabRef.id, prefabRef]));
|
|
1288
|
+
const entities = [
|
|
1289
|
+
{
|
|
1290
|
+
id: "generated_map_meta",
|
|
1291
|
+
components: {
|
|
1292
|
+
GeneratedMap: {
|
|
1293
|
+
generator: map.generator,
|
|
1294
|
+
seed: map.stats.seed,
|
|
1295
|
+
width: map.stats.width,
|
|
1296
|
+
height: map.stats.height,
|
|
1297
|
+
platformCount: map.stats.platformCount,
|
|
1298
|
+
enemyCount: map.stats.enemyCount,
|
|
1299
|
+
configHash: map.stats.configHash,
|
|
1300
|
+
...map.config.constraints ? { constraints: cloneJson(map.config.constraints) } : {}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
];
|
|
1305
|
+
for (const generatedEntity of map.entities) {
|
|
1306
|
+
const prefabRef = prefabRefsById.get(generatedEntity.prefabRef);
|
|
1307
|
+
if (!prefabRef) {
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
const overrides = {
|
|
1311
|
+
Transform: {
|
|
1312
|
+
position: generatedEntity.position,
|
|
1313
|
+
rotation: 0,
|
|
1314
|
+
scale: [1, 1]
|
|
1315
|
+
}
|
|
1316
|
+
};
|
|
1317
|
+
if (generatedEntity.role === "platform" && generatedEntity.size) {
|
|
1318
|
+
overrides.Collider = {
|
|
1319
|
+
shape: "box",
|
|
1320
|
+
size: generatedEntity.size,
|
|
1321
|
+
static: true
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
entities.push({
|
|
1325
|
+
id: generatedEntity.id,
|
|
1326
|
+
prefab: resolveGeneratedScenePrefabRef(prefabRef, options),
|
|
1327
|
+
overrides
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
return {
|
|
1331
|
+
version: 1,
|
|
1332
|
+
name: map.generatedMap,
|
|
1333
|
+
entities
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
async function generateMapSceneFile(configPath, outPath, options = {}) {
|
|
1337
|
+
const result = await generateMapFromConfigFile(configPath, options);
|
|
1338
|
+
const outFile = path.resolve(options.cwd ?? process.cwd(), outPath);
|
|
1339
|
+
const mapOutFile = options.mapOutPath ? path.resolve(options.cwd ?? process.cwd(), options.mapOutPath) : void 0;
|
|
1340
|
+
const diagnostics = [...result.diagnostics];
|
|
1341
|
+
if (!result.map || !result.scene || hasErrors(diagnostics)) {
|
|
1342
|
+
return {
|
|
1343
|
+
...result,
|
|
1344
|
+
outPath: outFile,
|
|
1345
|
+
...mapOutFile ? { mapOutPath: mapOutFile } : {},
|
|
1346
|
+
diagnostics
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
const scene = generatedMapToScene(result.map, {
|
|
1350
|
+
configFile: result.configPath,
|
|
1351
|
+
sceneFile: outFile
|
|
1352
|
+
});
|
|
1353
|
+
await mkdir(path.dirname(outFile), { recursive: true });
|
|
1354
|
+
await writeFile(outFile, formatScene(scene), "utf8");
|
|
1355
|
+
if (mapOutFile) {
|
|
1356
|
+
await mkdir(path.dirname(mapOutFile), { recursive: true });
|
|
1357
|
+
await writeFile(mapOutFile, formatGeneratedMap(result.map), "utf8");
|
|
1358
|
+
}
|
|
1359
|
+
const sceneValidation = await validateSceneFile(outFile);
|
|
1360
|
+
diagnostics.push(...sceneValidation.diagnostics);
|
|
1361
|
+
return {
|
|
1362
|
+
...result,
|
|
1363
|
+
outPath: outFile,
|
|
1364
|
+
...mapOutFile ? { mapOutPath: mapOutFile } : {},
|
|
1365
|
+
diagnostics,
|
|
1366
|
+
scene: hasErrors(diagnostics) ? void 0 : scene
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
function createMapGenerationSummary(map) {
|
|
1370
|
+
return {
|
|
1371
|
+
generator: map.generator,
|
|
1372
|
+
seed: map.stats.seed,
|
|
1373
|
+
width: map.stats.width,
|
|
1374
|
+
height: map.stats.height,
|
|
1375
|
+
platformCount: map.stats.platformCount,
|
|
1376
|
+
enemyCount: map.stats.enemyCount,
|
|
1377
|
+
entityCount: map.entities.length,
|
|
1378
|
+
configHash: map.stats.configHash
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
async function formatProjectFiles(projectPath = ".", options = {}) {
|
|
1382
|
+
const projectFile = resolveProjectFile(projectPath, options.cwd);
|
|
1383
|
+
const projectResult = await loadYamlFile(projectFile);
|
|
1384
|
+
const diagnostics = [...projectResult.diagnostics];
|
|
1385
|
+
const files = [];
|
|
1386
|
+
if (projectResult.document === void 0) {
|
|
1387
|
+
return { files, diagnostics };
|
|
1388
|
+
}
|
|
1389
|
+
diagnostics.push(...validateProjectDocument(projectResult.document, projectFile));
|
|
1390
|
+
if (hasErrors(diagnostics)) {
|
|
1391
|
+
return { files, diagnostics };
|
|
1392
|
+
}
|
|
1393
|
+
const assetContext = await loadProjectAssetManifestContext(projectFile, projectResult.document);
|
|
1394
|
+
diagnostics.push(...assetContext.diagnostics);
|
|
1395
|
+
if (hasErrors(diagnostics)) {
|
|
1396
|
+
return { files, diagnostics };
|
|
1397
|
+
}
|
|
1398
|
+
await writeFile(projectFile, formatProject(projectResult.document), "utf8");
|
|
1399
|
+
files.push(projectFile);
|
|
1400
|
+
const rootPath = path.dirname(projectFile);
|
|
1401
|
+
for (const sceneRef of projectResult.document.scenes) {
|
|
1402
|
+
const sceneFile = path.resolve(rootPath, sceneRef);
|
|
1403
|
+
const sceneResult = await loadYamlFile(sceneFile);
|
|
1404
|
+
diagnostics.push(...sceneResult.diagnostics);
|
|
1405
|
+
if (sceneResult.document === void 0) {
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
const sceneValidation = await validateSceneFile(sceneFile, { assets: assetContext });
|
|
1409
|
+
const sceneDiagnostics = sceneValidation.diagnostics;
|
|
1410
|
+
diagnostics.push(...sceneDiagnostics);
|
|
1411
|
+
if (hasErrors(sceneDiagnostics)) {
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
await writeFile(sceneFile, formatScene(sceneResult.document), "utf8");
|
|
1415
|
+
files.push(sceneFile);
|
|
1416
|
+
}
|
|
1417
|
+
return { files, diagnostics };
|
|
1418
|
+
}
|
|
1419
|
+
function applyPatchDocument(scene, patch, options = {}) {
|
|
1420
|
+
const patchFile = options.patchFile ?? "patch.json";
|
|
1421
|
+
const sceneFile = options.sceneFile ?? patch.target.scene;
|
|
1422
|
+
const diagnostics = validatePatchDocument(patch, patchFile);
|
|
1423
|
+
const summary = createPatchSummary();
|
|
1424
|
+
if (hasErrors(diagnostics)) {
|
|
1425
|
+
return {
|
|
1426
|
+
file: sceneFile,
|
|
1427
|
+
diagnostics,
|
|
1428
|
+
summary
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
const nextScene = cloneJson(scene);
|
|
1432
|
+
patch.operations.forEach((operation, index) => {
|
|
1433
|
+
if (hasErrors(diagnostics)) {
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
diagnostics.push(...applyPatchOperation(nextScene, operation, index, patchFile, summary));
|
|
1437
|
+
});
|
|
1438
|
+
if (!hasErrors(diagnostics)) {
|
|
1439
|
+
diagnostics.push(...validateSceneDocument(nextScene, sceneFile));
|
|
1440
|
+
}
|
|
1441
|
+
return {
|
|
1442
|
+
file: sceneFile,
|
|
1443
|
+
document: hasErrors(diagnostics) ? void 0 : nextScene,
|
|
1444
|
+
diagnostics,
|
|
1445
|
+
summary
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
async function applyPatchDocumentWithFileValidation(scene, patch, options) {
|
|
1449
|
+
const patchFile = options.patchFile ?? "patch.json";
|
|
1450
|
+
const sceneFile = options.sceneFile;
|
|
1451
|
+
const patched = applyPatchDocument(scene, patch, { patchFile, sceneFile });
|
|
1452
|
+
const diagnostics = [...patched.diagnostics];
|
|
1453
|
+
const assetContext = await loadNearestProjectAssetManifestContext(sceneFile);
|
|
1454
|
+
diagnostics.push(...assetContext.diagnostics);
|
|
1455
|
+
if (!patched.document || hasErrors(diagnostics)) {
|
|
1456
|
+
return {
|
|
1457
|
+
file: sceneFile,
|
|
1458
|
+
diagnostics,
|
|
1459
|
+
summary: patched.summary
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
diagnostics.push(...await validateInstantiatePrefabOperations(patch, patchFile, sceneFile));
|
|
1463
|
+
if (hasErrors(diagnostics)) {
|
|
1464
|
+
return {
|
|
1465
|
+
file: sceneFile,
|
|
1466
|
+
diagnostics,
|
|
1467
|
+
summary: patched.summary
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
const expandedPatched = await expandSceneDocument(patched.document, { sceneFile, assets: assetContext });
|
|
1471
|
+
diagnostics.push(...expandedPatched.diagnostics);
|
|
1472
|
+
return {
|
|
1473
|
+
file: sceneFile,
|
|
1474
|
+
document: hasErrors(diagnostics) ? void 0 : patched.document,
|
|
1475
|
+
diagnostics,
|
|
1476
|
+
summary: patched.summary
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
async function applyPatchFile(patchPath, options = {}) {
|
|
1480
|
+
const patchFile = path.resolve(options.cwd ?? process.cwd(), patchPath);
|
|
1481
|
+
const patchResult = await loadPatchFile(patchFile);
|
|
1482
|
+
const diagnostics = [...patchResult.diagnostics];
|
|
1483
|
+
if (patchResult.document === void 0) {
|
|
1484
|
+
return {
|
|
1485
|
+
patchPath: patchFile,
|
|
1486
|
+
diagnostics,
|
|
1487
|
+
changed: false,
|
|
1488
|
+
summary: createPatchSummary()
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
diagnostics.push(...validatePatchDocument(patchResult.document, patchFile));
|
|
1492
|
+
if (hasErrors(diagnostics)) {
|
|
1493
|
+
return {
|
|
1494
|
+
patchPath: patchFile,
|
|
1495
|
+
scenePath: resolvePatchScenePath(patchFile, patchResult.document.target?.scene),
|
|
1496
|
+
diagnostics,
|
|
1497
|
+
changed: false,
|
|
1498
|
+
summary: createPatchSummary()
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
const sceneFile = path.resolve(path.dirname(patchFile), patchResult.document.target.scene);
|
|
1502
|
+
const sceneResult = await loadSceneFile(sceneFile);
|
|
1503
|
+
diagnostics.push(...sceneResult.diagnostics);
|
|
1504
|
+
if (sceneResult.document === void 0 || hasErrors(diagnostics)) {
|
|
1505
|
+
return {
|
|
1506
|
+
patchPath: patchFile,
|
|
1507
|
+
scenePath: sceneFile,
|
|
1508
|
+
diagnostics,
|
|
1509
|
+
changed: false,
|
|
1510
|
+
summary: createPatchSummary()
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
const patched = await applyPatchDocumentWithFileValidation(sceneResult.document, patchResult.document, {
|
|
1514
|
+
patchFile,
|
|
1515
|
+
sceneFile
|
|
1516
|
+
});
|
|
1517
|
+
diagnostics.push(...patched.diagnostics);
|
|
1518
|
+
if (!patched.document || hasErrors(diagnostics)) {
|
|
1519
|
+
return {
|
|
1520
|
+
patchPath: patchFile,
|
|
1521
|
+
scenePath: sceneFile,
|
|
1522
|
+
diagnostics,
|
|
1523
|
+
changed: false,
|
|
1524
|
+
summary: patched.summary
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
await writeFile(sceneFile, formatScene(patched.document), "utf8");
|
|
1528
|
+
return {
|
|
1529
|
+
patchPath: patchFile,
|
|
1530
|
+
scenePath: sceneFile,
|
|
1531
|
+
diagnostics,
|
|
1532
|
+
changed: true,
|
|
1533
|
+
summary: patched.summary
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
function inspectSceneDocument(scene, scenePath = "scene.yml", expandedScene = scene, assets = createAssetManifestContext()) {
|
|
1537
|
+
const expandedById = new Map(expandedScene.entities.map((entity) => [entity.id, entity]));
|
|
1538
|
+
const hierarchy = deriveHierarchy(expandedScene, scenePath);
|
|
1539
|
+
const worldTransforms = computeWorldTransforms(expandedScene, scenePath);
|
|
1540
|
+
const hierarchySummary = createHierarchySummary(hierarchy);
|
|
1541
|
+
const animationSummary = inspectSceneAnimations(expandedScene, assets);
|
|
1542
|
+
const assetSummary = createSceneAssetSummary(expandedScene, assets);
|
|
1543
|
+
return {
|
|
1544
|
+
scenePath,
|
|
1545
|
+
name: scene.name,
|
|
1546
|
+
entityCount: scene.entities.length,
|
|
1547
|
+
hierarchy: hierarchySummary,
|
|
1548
|
+
animations: animationSummary,
|
|
1549
|
+
assets: assetSummary,
|
|
1550
|
+
entities: scene.entities.map((entity) => {
|
|
1551
|
+
const expandedEntity = expandedById.get(entity.id);
|
|
1552
|
+
const parent = hierarchy.parentByEntityId[entity.id];
|
|
1553
|
+
const worldTransform = worldTransforms.transforms[entity.id];
|
|
1554
|
+
return {
|
|
1555
|
+
id: entity.id,
|
|
1556
|
+
...entity.prefab ? { prefab: entity.prefab } : {},
|
|
1557
|
+
components: Object.keys(expandedEntity?.components ?? entity.components ?? {}),
|
|
1558
|
+
...entity.overrides ? { overrides: Object.keys(entity.overrides) } : {},
|
|
1559
|
+
...parent ? { parent } : {},
|
|
1560
|
+
children: hierarchy.childrenByEntityId[entity.id] ?? [],
|
|
1561
|
+
...worldTransform ? { worldTransform } : {}
|
|
1562
|
+
};
|
|
1563
|
+
})
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
async function inspectSceneFile(scenePath, options = {}) {
|
|
1567
|
+
const sceneResult = await loadSceneFile(scenePath, options);
|
|
1568
|
+
if (sceneResult.document === void 0) {
|
|
1569
|
+
return {
|
|
1570
|
+
file: sceneResult.file,
|
|
1571
|
+
diagnostics: sceneResult.diagnostics
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
const assetContext = await loadNearestProjectAssetManifestContext(sceneResult.file);
|
|
1575
|
+
const expandedResult = await expandSceneDocument(sceneResult.document, {
|
|
1576
|
+
sceneFile: sceneResult.file,
|
|
1577
|
+
assets: assetContext
|
|
1578
|
+
});
|
|
1579
|
+
const diagnostics = [...sceneResult.diagnostics, ...assetContext.diagnostics, ...expandedResult.diagnostics];
|
|
1580
|
+
if (!expandedResult.document || hasErrors(diagnostics)) {
|
|
1581
|
+
return {
|
|
1582
|
+
file: sceneResult.file,
|
|
1583
|
+
diagnostics
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
return {
|
|
1587
|
+
file: sceneResult.file,
|
|
1588
|
+
document: inspectSceneDocument(sceneResult.document, sceneResult.file, expandedResult.document, assetContext),
|
|
1589
|
+
diagnostics
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
async function inspectEntityFile(scenePath, entityId, options = {}) {
|
|
1593
|
+
const sceneResult = await loadExpandedSceneFile(scenePath, options);
|
|
1594
|
+
if (!sceneResult.document || hasErrors(sceneResult.diagnostics)) {
|
|
1595
|
+
return {
|
|
1596
|
+
file: sceneResult.file,
|
|
1597
|
+
diagnostics: sceneResult.diagnostics
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
const entity = sceneResult.document.entities.find((candidate) => candidate.id === entityId);
|
|
1601
|
+
if (!entity) {
|
|
1602
|
+
return {
|
|
1603
|
+
file: sceneResult.file,
|
|
1604
|
+
diagnostics: [
|
|
1605
|
+
{
|
|
1606
|
+
severity: "error",
|
|
1607
|
+
code: "SCENE_ENTITY_NOT_FOUND",
|
|
1608
|
+
file: sceneResult.file,
|
|
1609
|
+
path: "$.entities",
|
|
1610
|
+
message: `Entity '${entityId}' was not found.`,
|
|
1611
|
+
suggestion: "Inspect the scene to list available entity ids."
|
|
1612
|
+
}
|
|
1613
|
+
]
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
const inspection = inspectSceneDocument(sceneResult.document, sceneResult.file);
|
|
1617
|
+
const entitySummary = inspection.entities.find((candidate) => candidate.id === entityId);
|
|
1618
|
+
return {
|
|
1619
|
+
file: sceneResult.file,
|
|
1620
|
+
document: {
|
|
1621
|
+
scenePath: sceneResult.file,
|
|
1622
|
+
entity,
|
|
1623
|
+
hierarchy: {
|
|
1624
|
+
...entitySummary?.parent ? { parent: entitySummary.parent } : {},
|
|
1625
|
+
children: entitySummary?.children ?? [],
|
|
1626
|
+
depth: inspection.hierarchy.nodes.find((node) => node.entityId === entityId)?.depth ?? 0,
|
|
1627
|
+
...entitySummary?.worldTransform ? { worldTransform: entitySummary.worldTransform } : {}
|
|
1628
|
+
}
|
|
1629
|
+
},
|
|
1630
|
+
diagnostics: sceneResult.diagnostics
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
function inspectPrefabDocument(prefab, prefabPath = "prefab.yml") {
|
|
1634
|
+
return {
|
|
1635
|
+
prefabPath,
|
|
1636
|
+
prefab: prefab.prefab,
|
|
1637
|
+
components: Object.keys(prefab.components ?? {})
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
async function inspectPrefabFile(prefabPath, options = {}) {
|
|
1641
|
+
const result = await validatePrefabFile(prefabPath, options);
|
|
1642
|
+
if (!result.document || hasErrors(result.diagnostics)) {
|
|
1643
|
+
return {
|
|
1644
|
+
file: result.file,
|
|
1645
|
+
diagnostics: result.diagnostics
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
return {
|
|
1649
|
+
file: result.file,
|
|
1650
|
+
document: inspectPrefabDocument(result.document, result.file),
|
|
1651
|
+
diagnostics: result.diagnostics
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
function inspectAssetManifestDocument(assetManifest, assetManifestPath = "agent.assets.yml") {
|
|
1655
|
+
return {
|
|
1656
|
+
assetManifestPath,
|
|
1657
|
+
...assetManifest.assetManifest ? { assetManifest: assetManifest.assetManifest } : {},
|
|
1658
|
+
assetCount: assetManifest.assets.length,
|
|
1659
|
+
assetsByType: countAssetsByType(assetManifest.assets),
|
|
1660
|
+
assets: assetManifest.assets.map((asset) => {
|
|
1661
|
+
if (asset.type === "sprite") {
|
|
1662
|
+
return {
|
|
1663
|
+
id: asset.id,
|
|
1664
|
+
type: asset.type,
|
|
1665
|
+
path: asset.path,
|
|
1666
|
+
...asset.texture ? { texture: asset.texture } : {},
|
|
1667
|
+
...asset.rect ? { rect: asset.rect } : {},
|
|
1668
|
+
...asset.atlas ? { atlas: asset.atlas } : {},
|
|
1669
|
+
...asset.frame ? { frame: asset.frame } : {}
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
if (asset.type === "atlas") {
|
|
1673
|
+
const frames = asset.frames.map((frame) => ({
|
|
1674
|
+
id: frame.id,
|
|
1675
|
+
rect: frame.rect
|
|
1676
|
+
})).sort((left, right) => left.id.localeCompare(right.id));
|
|
1677
|
+
return {
|
|
1678
|
+
id: asset.id,
|
|
1679
|
+
type: asset.type,
|
|
1680
|
+
path: asset.path,
|
|
1681
|
+
...asset.texture ? { texture: asset.texture } : {},
|
|
1682
|
+
frameCount: frames.length,
|
|
1683
|
+
frames
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
return {
|
|
1687
|
+
id: asset.id,
|
|
1688
|
+
type: asset.type,
|
|
1689
|
+
path: asset.path
|
|
1690
|
+
};
|
|
1691
|
+
}).sort((left, right) => left.id.localeCompare(right.id))
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
async function inspectAssetManifestFile(assetManifestPath, options = {}) {
|
|
1695
|
+
const result = await validateAssetManifestFile(assetManifestPath, options);
|
|
1696
|
+
if (!result.document || hasErrors(result.diagnostics)) {
|
|
1697
|
+
return {
|
|
1698
|
+
file: result.file,
|
|
1699
|
+
diagnostics: result.diagnostics
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
return {
|
|
1703
|
+
file: result.file,
|
|
1704
|
+
document: inspectAssetManifestDocument(result.document, result.file),
|
|
1705
|
+
diagnostics: result.diagnostics
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
async function inspectProjectFile(projectPath = ".", options = {}) {
|
|
1709
|
+
const projectFile = resolveProjectFile(projectPath, options.cwd);
|
|
1710
|
+
const projectResult = await validateProjectFile(projectFile);
|
|
1711
|
+
const rootPath = path.dirname(projectFile);
|
|
1712
|
+
const inspection = {
|
|
1713
|
+
projectPath: projectFile,
|
|
1714
|
+
rootPath,
|
|
1715
|
+
scenes: [],
|
|
1716
|
+
assetManifests: [],
|
|
1717
|
+
diagnostics: projectResult.diagnostics
|
|
1718
|
+
};
|
|
1719
|
+
if (projectResult.document === void 0 || hasErrors(projectResult.diagnostics)) {
|
|
1720
|
+
return inspection;
|
|
1721
|
+
}
|
|
1722
|
+
inspection.name = projectResult.document.name;
|
|
1723
|
+
inspection.defaultScene = projectResult.document.defaultScene;
|
|
1724
|
+
for (const manifestRef of projectResult.document.assetManifests ?? []) {
|
|
1725
|
+
const manifestFile = path.resolve(rootPath, manifestRef);
|
|
1726
|
+
const manifestResult = await loadYamlFile(manifestFile);
|
|
1727
|
+
inspection.assetManifests.push({
|
|
1728
|
+
path: manifestRef,
|
|
1729
|
+
assetCount: Array.isArray(manifestResult.document?.assets) ? manifestResult.document.assets.length : void 0
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
for (const sceneRef of projectResult.document.scenes) {
|
|
1733
|
+
const sceneFile = path.resolve(rootPath, sceneRef);
|
|
1734
|
+
const sceneResult = await loadYamlFile(sceneFile);
|
|
1735
|
+
if (sceneResult.document !== void 0) {
|
|
1736
|
+
inspection.scenes.push({
|
|
1737
|
+
path: sceneRef,
|
|
1738
|
+
name: sceneResult.document.name,
|
|
1739
|
+
entityCount: Array.isArray(sceneResult.document.entities) ? sceneResult.document.entities.length : void 0
|
|
1740
|
+
});
|
|
1741
|
+
} else {
|
|
1742
|
+
inspection.scenes.push({ path: sceneRef });
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
return inspection;
|
|
1746
|
+
}
|
|
1747
|
+
function applyPatchOperation(scene, operation, index, patchFile, summary) {
|
|
1748
|
+
switch (operation.op) {
|
|
1749
|
+
case "add_entity":
|
|
1750
|
+
return applyAddEntity(scene, operation, index, patchFile);
|
|
1751
|
+
case "add_component":
|
|
1752
|
+
return applyAddComponent(scene, operation, index, patchFile);
|
|
1753
|
+
case "set_component_field":
|
|
1754
|
+
return applySetComponentField(scene, operation, index, patchFile);
|
|
1755
|
+
case "remove_component":
|
|
1756
|
+
return applyRemoveComponent(scene, operation, index, patchFile);
|
|
1757
|
+
case "remove_entity":
|
|
1758
|
+
return applyRemoveEntity(scene, operation, index, patchFile);
|
|
1759
|
+
case "rename_entity":
|
|
1760
|
+
return applyRenameEntity(scene, operation, index, patchFile, summary);
|
|
1761
|
+
case "rename_entity_at_source":
|
|
1762
|
+
return applyRenameEntityAtSource(scene, operation, index, patchFile);
|
|
1763
|
+
case "replace_reference":
|
|
1764
|
+
return applyReplaceReference(scene, operation, index, patchFile, summary);
|
|
1765
|
+
case "update_component":
|
|
1766
|
+
return applyUpdateComponent(scene, operation, index, patchFile);
|
|
1767
|
+
case "duplicate_entity":
|
|
1768
|
+
return applyDuplicateEntity(scene, operation, index, patchFile);
|
|
1769
|
+
case "instantiate_prefab":
|
|
1770
|
+
return applyInstantiatePrefab(scene, operation, index, patchFile);
|
|
1771
|
+
case "set_parent":
|
|
1772
|
+
return applySetParent(scene, operation, index, patchFile);
|
|
1773
|
+
case "clear_parent":
|
|
1774
|
+
return applyClearParent(scene, operation, index, patchFile);
|
|
1775
|
+
case "add_animation_frame":
|
|
1776
|
+
return applyAddAnimationFrame(scene, operation, index, patchFile);
|
|
1777
|
+
case "update_animation_frame":
|
|
1778
|
+
return applyUpdateAnimationFrame(scene, operation, index, patchFile);
|
|
1779
|
+
case "remove_animation_frame":
|
|
1780
|
+
return applyRemoveAnimationFrame(scene, operation, index, patchFile);
|
|
1781
|
+
case "set_animator_clip":
|
|
1782
|
+
return applySetAnimatorClip(scene, operation, index, patchFile);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
function applyAddEntity(scene, operation, index, patchFile) {
|
|
1786
|
+
const operationPath = `$.operations[${index}]`;
|
|
1787
|
+
if (!isRecord(operation.entity) || typeof operation.entity.id !== "string" || !isRecord(operation.entity.components)) {
|
|
1788
|
+
return [
|
|
1789
|
+
{
|
|
1790
|
+
severity: "error",
|
|
1791
|
+
code: "PATCH_INVALID_ENTITY",
|
|
1792
|
+
file: patchFile,
|
|
1793
|
+
path: `${operationPath}.entity`,
|
|
1794
|
+
message: "add_entity requires an entity with stable id and components.",
|
|
1795
|
+
suggestion: "Provide entity.id and entity.components."
|
|
1796
|
+
}
|
|
1797
|
+
];
|
|
1798
|
+
}
|
|
1799
|
+
if (findEntity(scene, operation.entity.id)) {
|
|
1800
|
+
return [
|
|
1801
|
+
{
|
|
1802
|
+
severity: "error",
|
|
1803
|
+
code: "PATCH_DUPLICATE_ENTITY_ID",
|
|
1804
|
+
file: patchFile,
|
|
1805
|
+
path: `${operationPath}.entity.id`,
|
|
1806
|
+
message: `Entity id '${operation.entity.id}' already exists.`,
|
|
1807
|
+
suggestion: "Use a new stable entity id."
|
|
1808
|
+
}
|
|
1809
|
+
];
|
|
1810
|
+
}
|
|
1811
|
+
scene.entities.push(cloneJson(operation.entity));
|
|
1812
|
+
return [];
|
|
1813
|
+
}
|
|
1814
|
+
function applyAddComponent(scene, operation, index, patchFile) {
|
|
1815
|
+
const operationPath = `$.operations[${index}]`;
|
|
1816
|
+
const entity = findEntity(scene, operation.entityId);
|
|
1817
|
+
if (!entity) {
|
|
1818
|
+
return targetEntityNotFound(operation.entityId, patchFile, `${operationPath}.entityId`);
|
|
1819
|
+
}
|
|
1820
|
+
if (!isRecord(entity.components)) {
|
|
1821
|
+
return entityComponentsUnavailable(operation.entityId, patchFile, operationPath);
|
|
1822
|
+
}
|
|
1823
|
+
if (!componentValidators[operation.component]) {
|
|
1824
|
+
return unknownComponent(operation.component, patchFile, `${operationPath}.component`);
|
|
1825
|
+
}
|
|
1826
|
+
if (Object.hasOwn(entity.components, operation.component)) {
|
|
1827
|
+
return [
|
|
1828
|
+
{
|
|
1829
|
+
severity: "error",
|
|
1830
|
+
code: "PATCH_COMPONENT_ALREADY_EXISTS",
|
|
1831
|
+
file: patchFile,
|
|
1832
|
+
path: `${operationPath}.component`,
|
|
1833
|
+
message: `Entity '${operation.entityId}' already has component '${operation.component}'.`,
|
|
1834
|
+
suggestion: "Use set_component_field to modify an existing component."
|
|
1835
|
+
}
|
|
1836
|
+
];
|
|
1837
|
+
}
|
|
1838
|
+
const componentDiagnostics = validateComponentValue(
|
|
1839
|
+
operation.component,
|
|
1840
|
+
operation.value,
|
|
1841
|
+
patchFile,
|
|
1842
|
+
`${operationPath}.value`,
|
|
1843
|
+
"PATCH_COMPONENT_INVALID"
|
|
1844
|
+
);
|
|
1845
|
+
if (componentDiagnostics.length > 0) {
|
|
1846
|
+
return componentDiagnostics;
|
|
1847
|
+
}
|
|
1848
|
+
entity.components[operation.component] = cloneJson(operation.value);
|
|
1849
|
+
return [];
|
|
1850
|
+
}
|
|
1851
|
+
function applySetComponentField(scene, operation, index, patchFile) {
|
|
1852
|
+
const operationPath = `$.operations[${index}]`;
|
|
1853
|
+
const entity = findEntity(scene, operation.entityId);
|
|
1854
|
+
if (!entity) {
|
|
1855
|
+
return targetEntityNotFound(operation.entityId, patchFile, `${operationPath}.entityId`);
|
|
1856
|
+
}
|
|
1857
|
+
if (!isRecord(entity.components)) {
|
|
1858
|
+
return entityComponentsUnavailable(operation.entityId, patchFile, operationPath);
|
|
1859
|
+
}
|
|
1860
|
+
if (!componentValidators[operation.component]) {
|
|
1861
|
+
return unknownComponent(operation.component, patchFile, `${operationPath}.component`);
|
|
1862
|
+
}
|
|
1863
|
+
if (!Object.hasOwn(entity.components, operation.component)) {
|
|
1864
|
+
return [
|
|
1865
|
+
{
|
|
1866
|
+
severity: "error",
|
|
1867
|
+
code: "PATCH_COMPONENT_NOT_FOUND",
|
|
1868
|
+
file: patchFile,
|
|
1869
|
+
path: `${operationPath}.component`,
|
|
1870
|
+
message: `Entity '${operation.entityId}' does not have component '${operation.component}'.`,
|
|
1871
|
+
suggestion: "Add the component before setting its fields."
|
|
1872
|
+
}
|
|
1873
|
+
];
|
|
1874
|
+
}
|
|
1875
|
+
const componentValue = entity.components[operation.component];
|
|
1876
|
+
const pointerDiagnostics = setJsonPointer(componentValue, operation.path, operation.value, patchFile, operationPath);
|
|
1877
|
+
if (pointerDiagnostics.length > 0) {
|
|
1878
|
+
return pointerDiagnostics;
|
|
1879
|
+
}
|
|
1880
|
+
return validateComponentValue(
|
|
1881
|
+
operation.component,
|
|
1882
|
+
componentValue,
|
|
1883
|
+
patchFile,
|
|
1884
|
+
operationPath,
|
|
1885
|
+
"PATCH_COMPONENT_INVALID"
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
function applyRemoveComponent(scene, operation, index, patchFile) {
|
|
1889
|
+
const operationPath = `$.operations[${index}]`;
|
|
1890
|
+
const entity = findEntity(scene, operation.entityId);
|
|
1891
|
+
if (!entity) {
|
|
1892
|
+
return targetEntityNotFound(operation.entityId, patchFile, `${operationPath}.entityId`);
|
|
1893
|
+
}
|
|
1894
|
+
if (!isRecord(entity.components)) {
|
|
1895
|
+
return entityComponentsUnavailable(operation.entityId, patchFile, operationPath);
|
|
1896
|
+
}
|
|
1897
|
+
if (!Object.hasOwn(entity.components, operation.component)) {
|
|
1898
|
+
return [
|
|
1899
|
+
{
|
|
1900
|
+
severity: "error",
|
|
1901
|
+
code: "PATCH_COMPONENT_NOT_FOUND",
|
|
1902
|
+
file: patchFile,
|
|
1903
|
+
path: `${operationPath}.component`,
|
|
1904
|
+
message: `Entity '${operation.entityId}' does not have component '${operation.component}'.`,
|
|
1905
|
+
suggestion: "Inspect the entity before removing components."
|
|
1906
|
+
}
|
|
1907
|
+
];
|
|
1908
|
+
}
|
|
1909
|
+
delete entity.components[operation.component];
|
|
1910
|
+
return [];
|
|
1911
|
+
}
|
|
1912
|
+
function applyRemoveEntity(scene, operation, index, patchFile) {
|
|
1913
|
+
const operationPath = `$.operations[${index}]`;
|
|
1914
|
+
const entityIndex = scene.entities.findIndex((entity) => entity.id === operation.entityId);
|
|
1915
|
+
if (entityIndex === -1) {
|
|
1916
|
+
return targetEntityNotFound(operation.entityId, patchFile, `${operationPath}.entityId`);
|
|
1917
|
+
}
|
|
1918
|
+
if (!operation.allowDanglingReferences) {
|
|
1919
|
+
const references = findEntityReferences(scene, operation.entityId);
|
|
1920
|
+
if (references.length > 0) {
|
|
1921
|
+
return references.map((reference) => ({
|
|
1922
|
+
severity: "error",
|
|
1923
|
+
code: "PATCH_ENTITY_HAS_REFERENCES",
|
|
1924
|
+
file: patchFile,
|
|
1925
|
+
path: `${operationPath}.entityId`,
|
|
1926
|
+
message: `Entity '${operation.entityId}' is referenced by entity '${reference.entityId}' at ${reference.component}.${reference.field}.`,
|
|
1927
|
+
suggestion: "Use rename_entity, replace_reference, or set allowDanglingReferences only when a later operation repairs references."
|
|
1928
|
+
}));
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
scene.entities.splice(entityIndex, 1);
|
|
1932
|
+
return [];
|
|
1933
|
+
}
|
|
1934
|
+
function applyRenameEntity(scene, operation, index, patchFile, summary) {
|
|
1935
|
+
const operationPath = `$.operations[${index}]`;
|
|
1936
|
+
const entity = findEntity(scene, operation.entityId);
|
|
1937
|
+
if (!entity) {
|
|
1938
|
+
return targetEntityNotFound(operation.entityId, patchFile, `${operationPath}.entityId`);
|
|
1939
|
+
}
|
|
1940
|
+
if (findEntity(scene, operation.newEntityId)) {
|
|
1941
|
+
return [
|
|
1942
|
+
{
|
|
1943
|
+
severity: "error",
|
|
1944
|
+
code: "PATCH_DUPLICATE_ENTITY_ID",
|
|
1945
|
+
file: patchFile,
|
|
1946
|
+
path: `${operationPath}.newEntityId`,
|
|
1947
|
+
message: `Entity id '${operation.newEntityId}' already exists.`,
|
|
1948
|
+
suggestion: "Use a new stable entity id."
|
|
1949
|
+
}
|
|
1950
|
+
];
|
|
1951
|
+
}
|
|
1952
|
+
entity.id = operation.newEntityId;
|
|
1953
|
+
replaceReferences(scene, operation.entityId, operation.newEntityId, summary);
|
|
1954
|
+
return [];
|
|
1955
|
+
}
|
|
1956
|
+
function applyRenameEntityAtSource(scene, operation, index, patchFile) {
|
|
1957
|
+
const operationPath = `$.operations[${index}]`;
|
|
1958
|
+
const sourceIndex = entityIndexFromSourcePath(operation.sourcePath);
|
|
1959
|
+
if (sourceIndex === void 0) {
|
|
1960
|
+
return patchSourceSelectorInvalid(
|
|
1961
|
+
patchFile,
|
|
1962
|
+
`${operationPath}.sourcePath`,
|
|
1963
|
+
"rename_entity_at_source sourcePath must target an entity array item.",
|
|
1964
|
+
"Use a source path such as $.entities[1]."
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
const keepIndex = entityIndexFromSourcePath(operation.keepEntitySourcePath);
|
|
1968
|
+
if (keepIndex === void 0) {
|
|
1969
|
+
return patchSourceSelectorInvalid(
|
|
1970
|
+
patchFile,
|
|
1971
|
+
`${operationPath}.keepEntitySourcePath`,
|
|
1972
|
+
"rename_entity_at_source keepEntitySourcePath must target an entity array item.",
|
|
1973
|
+
"Use a source path such as $.entities[0]."
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
const selectedEntity = scene.entities[sourceIndex];
|
|
1977
|
+
if (!selectedEntity || selectedEntity.id !== operation.entityId) {
|
|
1978
|
+
return patchSourceSelectorMismatch(
|
|
1979
|
+
patchFile,
|
|
1980
|
+
`${operationPath}.sourcePath`,
|
|
1981
|
+
`Selected source path '${operation.sourcePath}' no longer identifies duplicate entity '${operation.entityId}'.`
|
|
1982
|
+
);
|
|
1983
|
+
}
|
|
1984
|
+
const duplicateIndexes = scene.entities.map((entity, entityIndex) => entity.id === operation.entityId ? entityIndex : -1).filter((entityIndex) => entityIndex >= 0);
|
|
1985
|
+
if (duplicateIndexes.length !== operation.occurrenceCount || duplicateIndexes.length < 2) {
|
|
1986
|
+
return patchSourceSelectorMismatch(
|
|
1987
|
+
patchFile,
|
|
1988
|
+
`${operationPath}.occurrenceCount`,
|
|
1989
|
+
`Duplicate occurrence count for '${operation.entityId}' changed from ${operation.occurrenceCount} to ${duplicateIndexes.length}.`
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
const currentOccurrenceIndex = duplicateIndexes.indexOf(sourceIndex) + 1;
|
|
1993
|
+
if (currentOccurrenceIndex !== operation.occurrenceIndex) {
|
|
1994
|
+
return patchSourceSelectorMismatch(
|
|
1995
|
+
patchFile,
|
|
1996
|
+
`${operationPath}.occurrenceIndex`,
|
|
1997
|
+
`Selected source path '${operation.sourcePath}' is occurrence ${currentOccurrenceIndex}, not occurrence ${operation.occurrenceIndex}.`
|
|
1998
|
+
);
|
|
1999
|
+
}
|
|
2000
|
+
const keptEntity = scene.entities[keepIndex];
|
|
2001
|
+
if (keepIndex === sourceIndex || !keptEntity || keptEntity.id !== operation.entityId || !duplicateIndexes.includes(keepIndex)) {
|
|
2002
|
+
return patchSourceSelectorMismatch(
|
|
2003
|
+
patchFile,
|
|
2004
|
+
`${operationPath}.keepEntitySourcePath`,
|
|
2005
|
+
"keepEntitySourcePath must identify a different current duplicate occurrence that keeps the original id."
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
if (operation.newEntityId === operation.entityId || findEntity(scene, operation.newEntityId)) {
|
|
2009
|
+
return [
|
|
2010
|
+
{
|
|
2011
|
+
severity: "error",
|
|
2012
|
+
code: "PATCH_DUPLICATE_ENTITY_ID",
|
|
2013
|
+
file: patchFile,
|
|
2014
|
+
path: `${operationPath}.newEntityId`,
|
|
2015
|
+
message: `Entity id '${operation.newEntityId}' already exists or matches the duplicate id.`,
|
|
2016
|
+
suggestion: "Use a new stable entity id for only the selected duplicate occurrence."
|
|
2017
|
+
}
|
|
2018
|
+
];
|
|
2019
|
+
}
|
|
2020
|
+
if (operation.referenceOwnership.trim().length === 0) {
|
|
2021
|
+
return patchSourceSelectorMismatch(
|
|
2022
|
+
patchFile,
|
|
2023
|
+
`${operationPath}.referenceOwnership`,
|
|
2024
|
+
"rename_entity_at_source requires a reviewed reference ownership note."
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
selectedEntity.id = operation.newEntityId;
|
|
2028
|
+
return [];
|
|
2029
|
+
}
|
|
2030
|
+
function applyReplaceReference(scene, operation, index, patchFile, summary) {
|
|
2031
|
+
const operationPath = `$.operations[${index}]`;
|
|
2032
|
+
if (!findEntity(scene, operation.toEntityId)) {
|
|
2033
|
+
return targetEntityNotFound(operation.toEntityId, patchFile, `${operationPath}.toEntityId`);
|
|
2034
|
+
}
|
|
2035
|
+
replaceReferences(scene, operation.fromEntityId, operation.toEntityId, summary);
|
|
2036
|
+
return [];
|
|
2037
|
+
}
|
|
2038
|
+
function applyUpdateComponent(scene, operation, index, patchFile) {
|
|
2039
|
+
const operationPath = `$.operations[${index}]`;
|
|
2040
|
+
const entity = findEntity(scene, operation.entityId);
|
|
2041
|
+
if (!entity) {
|
|
2042
|
+
return targetEntityNotFound(operation.entityId, patchFile, `${operationPath}.entityId`);
|
|
2043
|
+
}
|
|
2044
|
+
if (!isRecord(entity.components)) {
|
|
2045
|
+
return entityComponentsUnavailable(operation.entityId, patchFile, operationPath);
|
|
2046
|
+
}
|
|
2047
|
+
if (!componentValidators[operation.component]) {
|
|
2048
|
+
return unknownComponent(operation.component, patchFile, `${operationPath}.component`);
|
|
2049
|
+
}
|
|
2050
|
+
if (!Object.hasOwn(entity.components, operation.component)) {
|
|
2051
|
+
return [
|
|
2052
|
+
{
|
|
2053
|
+
severity: "error",
|
|
2054
|
+
code: "PATCH_COMPONENT_NOT_FOUND",
|
|
2055
|
+
file: patchFile,
|
|
2056
|
+
path: `${operationPath}.component`,
|
|
2057
|
+
message: `Entity '${operation.entityId}' does not have component '${operation.component}'.`,
|
|
2058
|
+
suggestion: "Add the component before updating it."
|
|
2059
|
+
}
|
|
2060
|
+
];
|
|
2061
|
+
}
|
|
2062
|
+
if (!isRecord(operation.value)) {
|
|
2063
|
+
return [
|
|
2064
|
+
{
|
|
2065
|
+
severity: "error",
|
|
2066
|
+
code: "PATCH_COMPONENT_UPDATE_INVALID",
|
|
2067
|
+
file: patchFile,
|
|
2068
|
+
path: `${operationPath}.value`,
|
|
2069
|
+
message: "update_component requires an object value to deep-merge into the component.",
|
|
2070
|
+
suggestion: "Use an object containing only the component fields to update."
|
|
2071
|
+
}
|
|
2072
|
+
];
|
|
2073
|
+
}
|
|
2074
|
+
entity.components[operation.component] = deepMergeObjects(entity.components[operation.component], operation.value);
|
|
2075
|
+
return validateComponentValue(
|
|
2076
|
+
operation.component,
|
|
2077
|
+
entity.components[operation.component],
|
|
2078
|
+
patchFile,
|
|
2079
|
+
`${operationPath}.value`,
|
|
2080
|
+
"PATCH_COMPONENT_INVALID"
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
function applyDuplicateEntity(scene, operation, index, patchFile) {
|
|
2084
|
+
const operationPath = `$.operations[${index}]`;
|
|
2085
|
+
const sourceEntity = findEntity(scene, operation.entityId);
|
|
2086
|
+
if (!sourceEntity) {
|
|
2087
|
+
return targetEntityNotFound(operation.entityId, patchFile, `${operationPath}.entityId`);
|
|
2088
|
+
}
|
|
2089
|
+
if (findEntity(scene, operation.newEntityId)) {
|
|
2090
|
+
return [
|
|
2091
|
+
{
|
|
2092
|
+
severity: "error",
|
|
2093
|
+
code: "PATCH_DUPLICATE_ENTITY_ID",
|
|
2094
|
+
file: patchFile,
|
|
2095
|
+
path: `${operationPath}.newEntityId`,
|
|
2096
|
+
message: `Entity id '${operation.newEntityId}' already exists.`,
|
|
2097
|
+
suggestion: "Use a new stable entity id."
|
|
2098
|
+
}
|
|
2099
|
+
];
|
|
2100
|
+
}
|
|
2101
|
+
if (!isRecord(sourceEntity.components)) {
|
|
2102
|
+
return entityComponentsUnavailable(operation.entityId, patchFile, operationPath);
|
|
2103
|
+
}
|
|
2104
|
+
const duplicatedComponents = cloneJson(sourceEntity.components);
|
|
2105
|
+
const duplicatedEntity = {
|
|
2106
|
+
id: operation.newEntityId,
|
|
2107
|
+
components: duplicatedComponents
|
|
2108
|
+
};
|
|
2109
|
+
if (operation.overrides) {
|
|
2110
|
+
for (const [componentName, override] of Object.entries(operation.overrides)) {
|
|
2111
|
+
if (!componentValidators[componentName]) {
|
|
2112
|
+
return unknownComponent(componentName, patchFile, `${operationPath}.overrides.${componentName}`);
|
|
2113
|
+
}
|
|
2114
|
+
duplicatedComponents[componentName] = Object.hasOwn(duplicatedComponents, componentName) ? deepMergeObjects(duplicatedComponents[componentName], override) : cloneJson(override);
|
|
2115
|
+
const componentDiagnostics = validateComponentValue(
|
|
2116
|
+
componentName,
|
|
2117
|
+
duplicatedComponents[componentName],
|
|
2118
|
+
patchFile,
|
|
2119
|
+
`${operationPath}.overrides.${componentName}`,
|
|
2120
|
+
"PATCH_COMPONENT_INVALID"
|
|
2121
|
+
);
|
|
2122
|
+
if (componentDiagnostics.length > 0) {
|
|
2123
|
+
return componentDiagnostics;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
scene.entities.push(duplicatedEntity);
|
|
2128
|
+
return [];
|
|
2129
|
+
}
|
|
2130
|
+
function applyInstantiatePrefab(scene, operation, index, patchFile) {
|
|
2131
|
+
const operationPath = `$.operations[${index}]`;
|
|
2132
|
+
if (findEntity(scene, operation.entityId)) {
|
|
2133
|
+
return [
|
|
2134
|
+
{
|
|
2135
|
+
severity: "error",
|
|
2136
|
+
code: "PATCH_DUPLICATE_ENTITY_ID",
|
|
2137
|
+
file: patchFile,
|
|
2138
|
+
path: `${operationPath}.entityId`,
|
|
2139
|
+
message: `Entity id '${operation.entityId}' already exists.`,
|
|
2140
|
+
suggestion: "Use a new stable entity id for the prefab instance."
|
|
2141
|
+
}
|
|
2142
|
+
];
|
|
2143
|
+
}
|
|
2144
|
+
if (typeof operation.prefab !== "string" || operation.prefab.trim().length === 0) {
|
|
2145
|
+
return [
|
|
2146
|
+
{
|
|
2147
|
+
severity: "error",
|
|
2148
|
+
code: "PATCH_INVALID_PREFAB_PATH",
|
|
2149
|
+
file: patchFile,
|
|
2150
|
+
path: `${operationPath}.prefab`,
|
|
2151
|
+
message: "instantiate_prefab requires a non-empty prefab path.",
|
|
2152
|
+
suggestion: "Use a prefab path relative to the target scene, such as ../prefabs/slime.prefab.yml."
|
|
2153
|
+
}
|
|
2154
|
+
];
|
|
2155
|
+
}
|
|
2156
|
+
const entity = {
|
|
2157
|
+
id: operation.entityId,
|
|
2158
|
+
prefab: operation.prefab
|
|
2159
|
+
};
|
|
2160
|
+
if (operation.overrides !== void 0) {
|
|
2161
|
+
entity.overrides = cloneJson(operation.overrides);
|
|
2162
|
+
}
|
|
2163
|
+
scene.entities.push(entity);
|
|
2164
|
+
return [];
|
|
2165
|
+
}
|
|
2166
|
+
function applySetParent(scene, operation, index, patchFile) {
|
|
2167
|
+
const operationPath = `$.operations[${index}]`;
|
|
2168
|
+
const entity = findEntity(scene, operation.entityId);
|
|
2169
|
+
if (!entity) {
|
|
2170
|
+
return targetEntityNotFound(operation.entityId, patchFile, `${operationPath}.entityId`);
|
|
2171
|
+
}
|
|
2172
|
+
if (!findEntity(scene, operation.parentEntityId)) {
|
|
2173
|
+
return targetEntityNotFound(operation.parentEntityId, patchFile, `${operationPath}.parentEntityId`);
|
|
2174
|
+
}
|
|
2175
|
+
if (operation.entityId === operation.parentEntityId) {
|
|
2176
|
+
return [
|
|
2177
|
+
{
|
|
2178
|
+
severity: "error",
|
|
2179
|
+
code: "PATCH_SELF_PARENT",
|
|
2180
|
+
file: patchFile,
|
|
2181
|
+
path: `${operationPath}.parentEntityId`,
|
|
2182
|
+
message: `Entity '${operation.entityId}' cannot parent itself.`,
|
|
2183
|
+
suggestion: "Use a different parent entity id or clear the parent."
|
|
2184
|
+
}
|
|
2185
|
+
];
|
|
2186
|
+
}
|
|
2187
|
+
if (!isRecord(entity.components)) {
|
|
2188
|
+
return entityComponentsUnavailable(operation.entityId, patchFile, operationPath);
|
|
2189
|
+
}
|
|
2190
|
+
entity.components.Parent = { entityId: operation.parentEntityId };
|
|
2191
|
+
return [];
|
|
2192
|
+
}
|
|
2193
|
+
function applyClearParent(scene, operation, index, patchFile) {
|
|
2194
|
+
const operationPath = `$.operations[${index}]`;
|
|
2195
|
+
const entity = findEntity(scene, operation.entityId);
|
|
2196
|
+
if (!entity) {
|
|
2197
|
+
return targetEntityNotFound(operation.entityId, patchFile, `${operationPath}.entityId`);
|
|
2198
|
+
}
|
|
2199
|
+
if (!isRecord(entity.components)) {
|
|
2200
|
+
return entityComponentsUnavailable(operation.entityId, patchFile, operationPath);
|
|
2201
|
+
}
|
|
2202
|
+
delete entity.components.Parent;
|
|
2203
|
+
return [];
|
|
2204
|
+
}
|
|
2205
|
+
function applyAddAnimationFrame(scene, operation, index, patchFile) {
|
|
2206
|
+
const operationPath = `$.operations[${index}]`;
|
|
2207
|
+
const clipResult = findAnimationClipForPatch(scene, operation.entityId, patchFile, operationPath);
|
|
2208
|
+
if (Array.isArray(clipResult)) {
|
|
2209
|
+
return clipResult;
|
|
2210
|
+
}
|
|
2211
|
+
if (clipResult.frames.some((frame) => isRecord(frame) && frame.id === operation.frame.id)) {
|
|
2212
|
+
return [
|
|
2213
|
+
{
|
|
2214
|
+
severity: "error",
|
|
2215
|
+
code: "PATCH_ANIMATION_FRAME_DUPLICATE",
|
|
2216
|
+
file: patchFile,
|
|
2217
|
+
path: `${operationPath}.frame.id`,
|
|
2218
|
+
message: `Animation frame '${operation.frame.id}' already exists in clip '${String(clipResult.clip.id)}'.`,
|
|
2219
|
+
suggestion: "Use a new stable SpriteFrame.id or update the existing frame."
|
|
2220
|
+
}
|
|
2221
|
+
];
|
|
2222
|
+
}
|
|
2223
|
+
const insertIndex = animationFrameInsertIndex(clipResult.frames, operation.afterFrameId);
|
|
2224
|
+
if (insertIndex === void 0) {
|
|
2225
|
+
return animationFrameNotFound(
|
|
2226
|
+
operation.afterFrameId ?? "<unknown>",
|
|
2227
|
+
patchFile,
|
|
2228
|
+
`${operationPath}.afterFrameId`,
|
|
2229
|
+
"Choose an existing SpriteFrame.id from the target AnimationClip."
|
|
2230
|
+
);
|
|
2231
|
+
}
|
|
2232
|
+
if (Array.isArray(clipResult.clip.frameOrder) && operation.afterFrameId) {
|
|
2233
|
+
const frameOrderIndex = clipResult.clip.frameOrder.indexOf(operation.afterFrameId);
|
|
2234
|
+
if (frameOrderIndex === -1) {
|
|
2235
|
+
return animationFrameNotFound(
|
|
2236
|
+
operation.afterFrameId,
|
|
2237
|
+
patchFile,
|
|
2238
|
+
`${operationPath}.afterFrameId`,
|
|
2239
|
+
"When frameOrder is explicit, afterFrameId must also appear in AnimationClip.frameOrder."
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
clipResult.clip.frameOrder.splice(frameOrderIndex + 1, 0, operation.frame.id);
|
|
2243
|
+
} else if (Array.isArray(clipResult.clip.frameOrder)) {
|
|
2244
|
+
clipResult.clip.frameOrder.push(operation.frame.id);
|
|
2245
|
+
}
|
|
2246
|
+
clipResult.frames.splice(insertIndex, 0, cloneJson(operation.frame));
|
|
2247
|
+
return validateComponentValue("AnimationClip", clipResult.clip, patchFile, operationPath, "PATCH_COMPONENT_INVALID");
|
|
2248
|
+
}
|
|
2249
|
+
function applyUpdateAnimationFrame(scene, operation, index, patchFile) {
|
|
2250
|
+
const operationPath = `$.operations[${index}]`;
|
|
2251
|
+
const clipResult = findAnimationClipForPatch(scene, operation.entityId, patchFile, operationPath);
|
|
2252
|
+
if (Array.isArray(clipResult)) {
|
|
2253
|
+
return clipResult;
|
|
2254
|
+
}
|
|
2255
|
+
if (Object.keys(operation.value).length === 0) {
|
|
2256
|
+
return [
|
|
2257
|
+
{
|
|
2258
|
+
severity: "error",
|
|
2259
|
+
code: "PATCH_ANIMATION_FRAME_UPDATE_EMPTY",
|
|
2260
|
+
file: patchFile,
|
|
2261
|
+
path: `${operationPath}.value`,
|
|
2262
|
+
message: "update_animation_frame requires at least one frame field to update.",
|
|
2263
|
+
suggestion: "Set durationFrames, sprite, or rect."
|
|
2264
|
+
}
|
|
2265
|
+
];
|
|
2266
|
+
}
|
|
2267
|
+
const frame = clipResult.frames.find((candidate) => isRecord(candidate) && candidate.id === operation.frameId);
|
|
2268
|
+
if (!frame || !isRecord(frame)) {
|
|
2269
|
+
return animationFrameNotFound(
|
|
2270
|
+
operation.frameId,
|
|
2271
|
+
patchFile,
|
|
2272
|
+
`${operationPath}.frameId`,
|
|
2273
|
+
"Inspect the target AnimationClip and choose an existing SpriteFrame.id."
|
|
2274
|
+
);
|
|
2275
|
+
}
|
|
2276
|
+
Object.assign(frame, cloneJson(operation.value));
|
|
2277
|
+
return validateComponentValue("AnimationClip", clipResult.clip, patchFile, operationPath, "PATCH_COMPONENT_INVALID");
|
|
2278
|
+
}
|
|
2279
|
+
function applyRemoveAnimationFrame(scene, operation, index, patchFile) {
|
|
2280
|
+
const operationPath = `$.operations[${index}]`;
|
|
2281
|
+
const clipResult = findAnimationClipForPatch(scene, operation.entityId, patchFile, operationPath);
|
|
2282
|
+
if (Array.isArray(clipResult)) {
|
|
2283
|
+
return clipResult;
|
|
2284
|
+
}
|
|
2285
|
+
const frameIndex = clipResult.frames.findIndex((frame) => isRecord(frame) && frame.id === operation.frameId);
|
|
2286
|
+
if (frameIndex === -1) {
|
|
2287
|
+
return animationFrameNotFound(
|
|
2288
|
+
operation.frameId,
|
|
2289
|
+
patchFile,
|
|
2290
|
+
`${operationPath}.frameId`,
|
|
2291
|
+
"Inspect the target AnimationClip and choose an existing SpriteFrame.id."
|
|
2292
|
+
);
|
|
2293
|
+
}
|
|
2294
|
+
if (clipResult.frames.length <= 1) {
|
|
2295
|
+
return [
|
|
2296
|
+
{
|
|
2297
|
+
severity: "error",
|
|
2298
|
+
code: "PATCH_ANIMATION_FRAME_REQUIRED",
|
|
2299
|
+
file: patchFile,
|
|
2300
|
+
path: `${operationPath}.frameId`,
|
|
2301
|
+
message: `AnimationClip '${String(clipResult.clip.id)}' must keep at least one frame.`,
|
|
2302
|
+
suggestion: "Update the frame instead of removing the final frame, or add a replacement frame first."
|
|
2303
|
+
}
|
|
2304
|
+
];
|
|
2305
|
+
}
|
|
2306
|
+
clipResult.frames.splice(frameIndex, 1);
|
|
2307
|
+
if (Array.isArray(clipResult.clip.frameOrder)) {
|
|
2308
|
+
clipResult.clip.frameOrder = clipResult.clip.frameOrder.filter((frameId) => frameId !== operation.frameId);
|
|
2309
|
+
}
|
|
2310
|
+
return validateComponentValue("AnimationClip", clipResult.clip, patchFile, operationPath, "PATCH_COMPONENT_INVALID");
|
|
2311
|
+
}
|
|
2312
|
+
function applySetAnimatorClip(scene, operation, index, patchFile) {
|
|
2313
|
+
const operationPath = `$.operations[${index}]`;
|
|
2314
|
+
if (!findAnimationClipById(scene, operation.clipId)) {
|
|
2315
|
+
return [
|
|
2316
|
+
{
|
|
2317
|
+
severity: "error",
|
|
2318
|
+
code: "PATCH_ANIMATION_CLIP_NOT_FOUND",
|
|
2319
|
+
file: patchFile,
|
|
2320
|
+
path: `${operationPath}.clipId`,
|
|
2321
|
+
message: `AnimationClip '${operation.clipId}' does not exist in the scene.`,
|
|
2322
|
+
suggestion: "Use an AnimationClip.id from the target scene."
|
|
2323
|
+
}
|
|
2324
|
+
];
|
|
2325
|
+
}
|
|
2326
|
+
const animatorResult = findAnimatorForPatch(scene, operation.entityId, patchFile, operationPath);
|
|
2327
|
+
if (Array.isArray(animatorResult)) {
|
|
2328
|
+
return animatorResult;
|
|
2329
|
+
}
|
|
2330
|
+
if (!Array.isArray(animatorResult.clips)) {
|
|
2331
|
+
return [
|
|
2332
|
+
{
|
|
2333
|
+
severity: "error",
|
|
2334
|
+
code: "PATCH_ANIMATOR_CLIPS_INVALID",
|
|
2335
|
+
file: patchFile,
|
|
2336
|
+
path: `${operationPath}.entityId`,
|
|
2337
|
+
message: `Entity '${operation.entityId}' Animator.clips must be an array before setting a clip.`,
|
|
2338
|
+
suggestion: "Run scene validation and repair Animator.clips first."
|
|
2339
|
+
}
|
|
2340
|
+
];
|
|
2341
|
+
}
|
|
2342
|
+
if (!animatorResult.clips.includes(operation.clipId)) {
|
|
2343
|
+
animatorResult.clips.push(operation.clipId);
|
|
2344
|
+
}
|
|
2345
|
+
switch (operation.slot ?? "current") {
|
|
2346
|
+
case "current":
|
|
2347
|
+
animatorResult.currentClip = operation.clipId;
|
|
2348
|
+
break;
|
|
2349
|
+
case "default":
|
|
2350
|
+
animatorResult.defaultClip = operation.clipId;
|
|
2351
|
+
break;
|
|
2352
|
+
case "active":
|
|
2353
|
+
animatorResult.activeClip = operation.clipId;
|
|
2354
|
+
break;
|
|
2355
|
+
}
|
|
2356
|
+
return validateComponentValue("Animator", animatorResult, patchFile, operationPath, "PATCH_COMPONENT_INVALID");
|
|
2357
|
+
}
|
|
2358
|
+
function findAnimationClipForPatch(scene, entityId, patchFile, operationPath) {
|
|
2359
|
+
const entity = findEntity(scene, entityId);
|
|
2360
|
+
if (!entity) {
|
|
2361
|
+
return targetEntityNotFound(entityId, patchFile, `${operationPath}.entityId`);
|
|
2362
|
+
}
|
|
2363
|
+
if (!isRecord(entity.components)) {
|
|
2364
|
+
return entityComponentsUnavailable(entityId, patchFile, operationPath);
|
|
2365
|
+
}
|
|
2366
|
+
const clip = getComponentRecord(entity, "AnimationClip");
|
|
2367
|
+
if (!clip || !Array.isArray(clip.frames)) {
|
|
2368
|
+
return [
|
|
2369
|
+
{
|
|
2370
|
+
severity: "error",
|
|
2371
|
+
code: "PATCH_ANIMATION_CLIP_NOT_FOUND",
|
|
2372
|
+
file: patchFile,
|
|
2373
|
+
path: `${operationPath}.entityId`,
|
|
2374
|
+
message: `Entity '${entityId}' does not have an AnimationClip component.`,
|
|
2375
|
+
suggestion: "Target an entity that owns the AnimationClip component."
|
|
2376
|
+
}
|
|
2377
|
+
];
|
|
2378
|
+
}
|
|
2379
|
+
return { entity, clip, frames: clip.frames };
|
|
2380
|
+
}
|
|
2381
|
+
function findAnimatorForPatch(scene, entityId, patchFile, operationPath) {
|
|
2382
|
+
const entity = findEntity(scene, entityId);
|
|
2383
|
+
if (!entity) {
|
|
2384
|
+
return targetEntityNotFound(entityId, patchFile, `${operationPath}.entityId`);
|
|
2385
|
+
}
|
|
2386
|
+
if (!isRecord(entity.components)) {
|
|
2387
|
+
return entityComponentsUnavailable(entityId, patchFile, operationPath);
|
|
2388
|
+
}
|
|
2389
|
+
const animator = getComponentRecord(entity, "Animator");
|
|
2390
|
+
if (!animator) {
|
|
2391
|
+
return [
|
|
2392
|
+
{
|
|
2393
|
+
severity: "error",
|
|
2394
|
+
code: "PATCH_ANIMATOR_NOT_FOUND",
|
|
2395
|
+
file: patchFile,
|
|
2396
|
+
path: `${operationPath}.entityId`,
|
|
2397
|
+
message: `Entity '${entityId}' does not have an Animator component.`,
|
|
2398
|
+
suggestion: "Target an entity that owns the Animator component."
|
|
2399
|
+
}
|
|
2400
|
+
];
|
|
2401
|
+
}
|
|
2402
|
+
return animator;
|
|
2403
|
+
}
|
|
2404
|
+
function findAnimationClipById(scene, clipId) {
|
|
2405
|
+
for (const entity of scene.entities) {
|
|
2406
|
+
const clip = getComponentRecord(entity, "AnimationClip");
|
|
2407
|
+
if (clip?.id === clipId) {
|
|
2408
|
+
return clip;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
return void 0;
|
|
2412
|
+
}
|
|
2413
|
+
function animationFrameInsertIndex(frames, afterFrameId) {
|
|
2414
|
+
if (afterFrameId === void 0) {
|
|
2415
|
+
return frames.length;
|
|
2416
|
+
}
|
|
2417
|
+
const frameIndex = frames.findIndex((frame) => isRecord(frame) && frame.id === afterFrameId);
|
|
2418
|
+
return frameIndex === -1 ? void 0 : frameIndex + 1;
|
|
2419
|
+
}
|
|
2420
|
+
function animationFrameNotFound(frameId, patchFile, dataPath, suggestion) {
|
|
2421
|
+
return [
|
|
2422
|
+
{
|
|
2423
|
+
severity: "error",
|
|
2424
|
+
code: "PATCH_ANIMATION_FRAME_NOT_FOUND",
|
|
2425
|
+
file: patchFile,
|
|
2426
|
+
path: dataPath,
|
|
2427
|
+
message: `Animation frame '${frameId}' was not found.`,
|
|
2428
|
+
suggestion
|
|
2429
|
+
}
|
|
2430
|
+
];
|
|
2431
|
+
}
|
|
2432
|
+
async function validateInstantiatePrefabOperations(patch, patchFile, sceneFile) {
|
|
2433
|
+
const diagnostics = [];
|
|
2434
|
+
for (const [index, operation] of patch.operations.entries()) {
|
|
2435
|
+
if (operation.op !== "instantiate_prefab") {
|
|
2436
|
+
continue;
|
|
2437
|
+
}
|
|
2438
|
+
const operationPath = `$.operations[${index}]`;
|
|
2439
|
+
if (typeof operation.prefab !== "string" || operation.prefab.trim().length === 0) {
|
|
2440
|
+
diagnostics.push({
|
|
2441
|
+
severity: "error",
|
|
2442
|
+
code: "PATCH_INVALID_PREFAB_PATH",
|
|
2443
|
+
file: patchFile,
|
|
2444
|
+
path: `${operationPath}.prefab`,
|
|
2445
|
+
message: "instantiate_prefab requires a non-empty prefab path.",
|
|
2446
|
+
suggestion: "Use a prefab path relative to the target scene, such as ../prefabs/slime.prefab.yml."
|
|
2447
|
+
});
|
|
2448
|
+
continue;
|
|
2449
|
+
}
|
|
2450
|
+
const prefabFile = resolveScenePrefabPath(sceneFile, operation.prefab);
|
|
2451
|
+
const prefabResult = await loadPrefabFile(prefabFile);
|
|
2452
|
+
if (prefabResult.document === void 0 && prefabResult.diagnostics.some((diagnostic) => diagnostic.code === "FILE_READ_FAILED")) {
|
|
2453
|
+
diagnostics.push({
|
|
2454
|
+
severity: "error",
|
|
2455
|
+
code: "PATCH_MISSING_PREFAB",
|
|
2456
|
+
file: patchFile,
|
|
2457
|
+
path: `${operationPath}.prefab`,
|
|
2458
|
+
message: `Prefab file '${operation.prefab}' was not found.`,
|
|
2459
|
+
suggestion: "Create the prefab file or update the prefab path relative to the target scene."
|
|
2460
|
+
});
|
|
2461
|
+
continue;
|
|
2462
|
+
}
|
|
2463
|
+
const prefabDiagnostics = [...prefabResult.diagnostics];
|
|
2464
|
+
if (prefabResult.document !== void 0) {
|
|
2465
|
+
prefabDiagnostics.push(...validatePrefabDocument(prefabResult.document, prefabResult.file));
|
|
2466
|
+
}
|
|
2467
|
+
if (prefabResult.document === void 0 || hasErrors(prefabDiagnostics)) {
|
|
2468
|
+
diagnostics.push({
|
|
2469
|
+
severity: "error",
|
|
2470
|
+
code: "PATCH_INVALID_PREFAB",
|
|
2471
|
+
file: patchFile,
|
|
2472
|
+
path: `${operationPath}.prefab`,
|
|
2473
|
+
message: `Prefab file '${operation.prefab}' is invalid.`,
|
|
2474
|
+
suggestion: "Validate the prefab file before instantiating it."
|
|
2475
|
+
});
|
|
2476
|
+
diagnostics.push(...prefabDiagnostics);
|
|
2477
|
+
continue;
|
|
2478
|
+
}
|
|
2479
|
+
diagnostics.push(
|
|
2480
|
+
...validatePrefabOverrideEntries(operation.overrides, prefabResult.document, patchFile, `${operationPath}.overrides`, {
|
|
2481
|
+
invalidOverrides: "PATCH_INVALID_PREFAB_OVERRIDES",
|
|
2482
|
+
unknownComponent: "PATCH_INVALID_PREFAB_OVERRIDES",
|
|
2483
|
+
componentNotFound: "PATCH_INVALID_PREFAB_OVERRIDES",
|
|
2484
|
+
invalidOverride: "PATCH_INVALID_PREFAB_OVERRIDES"
|
|
2485
|
+
})
|
|
2486
|
+
);
|
|
2487
|
+
}
|
|
2488
|
+
return diagnostics;
|
|
2489
|
+
}
|
|
2490
|
+
function validateComponentValue(componentName, value, file, dataPath, code) {
|
|
2491
|
+
const validator = componentValidators[componentName];
|
|
2492
|
+
if (!validator) {
|
|
2493
|
+
return unknownComponent(componentName, file, dataPath);
|
|
2494
|
+
}
|
|
2495
|
+
return schemaDiagnostics(validator, value, file, code, dataPath);
|
|
2496
|
+
}
|
|
2497
|
+
function validatePrefabOverrides(entity, prefab, sceneFile, entityIndex) {
|
|
2498
|
+
return validatePrefabOverrideEntries(
|
|
2499
|
+
entity.overrides,
|
|
2500
|
+
prefab,
|
|
2501
|
+
sceneFile,
|
|
2502
|
+
`$.entities[${entityIndex}].overrides`,
|
|
2503
|
+
{
|
|
2504
|
+
invalidOverrides: "SCENE_PREFAB_OVERRIDES_INVALID",
|
|
2505
|
+
unknownComponent: "SCENE_PREFAB_OVERRIDE_UNKNOWN_COMPONENT",
|
|
2506
|
+
componentNotFound: "SCENE_PREFAB_OVERRIDE_COMPONENT_NOT_FOUND",
|
|
2507
|
+
invalidOverride: "SCENE_PREFAB_OVERRIDE_INVALID"
|
|
2508
|
+
}
|
|
2509
|
+
);
|
|
2510
|
+
}
|
|
2511
|
+
function validatePrefabOverrideEntries(overrides, prefab, file, pathPrefix, codes) {
|
|
2512
|
+
const diagnostics = [];
|
|
2513
|
+
if (overrides === void 0) {
|
|
2514
|
+
return diagnostics;
|
|
2515
|
+
}
|
|
2516
|
+
if (!isRecord(overrides)) {
|
|
2517
|
+
return [
|
|
2518
|
+
{
|
|
2519
|
+
severity: "error",
|
|
2520
|
+
code: codes.invalidOverrides,
|
|
2521
|
+
file,
|
|
2522
|
+
path: pathPrefix,
|
|
2523
|
+
message: "Prefab overrides must be an object keyed by component name.",
|
|
2524
|
+
suggestion: "Use component names such as Transform under overrides."
|
|
2525
|
+
}
|
|
2526
|
+
];
|
|
2527
|
+
}
|
|
2528
|
+
for (const [componentName, overrideValue] of Object.entries(overrides)) {
|
|
2529
|
+
const overridePath = `${pathPrefix}.${componentName}`;
|
|
2530
|
+
if (!componentValidators[componentName]) {
|
|
2531
|
+
diagnostics.push({
|
|
2532
|
+
severity: "error",
|
|
2533
|
+
code: codes.unknownComponent,
|
|
2534
|
+
file,
|
|
2535
|
+
path: overridePath,
|
|
2536
|
+
message: `Unknown component '${componentName}' in prefab override.`,
|
|
2537
|
+
suggestion: `Use one of: ${componentOrder.join(", ")}.`
|
|
2538
|
+
});
|
|
2539
|
+
continue;
|
|
2540
|
+
}
|
|
2541
|
+
if (!Object.hasOwn(prefab.components, componentName)) {
|
|
2542
|
+
diagnostics.push({
|
|
2543
|
+
severity: "error",
|
|
2544
|
+
code: codes.componentNotFound,
|
|
2545
|
+
file,
|
|
2546
|
+
path: overridePath,
|
|
2547
|
+
message: `Prefab '${prefab.prefab}' does not define component '${componentName}'.`,
|
|
2548
|
+
suggestion: "Override only components defined by the prefab in v0."
|
|
2549
|
+
});
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2552
|
+
if (!isRecord(overrideValue)) {
|
|
2553
|
+
diagnostics.push({
|
|
2554
|
+
severity: "error",
|
|
2555
|
+
code: codes.invalidOverride,
|
|
2556
|
+
file,
|
|
2557
|
+
path: overridePath,
|
|
2558
|
+
message: `Override for component '${componentName}' must be an object.`,
|
|
2559
|
+
suggestion: "Provide only the component fields to override."
|
|
2560
|
+
});
|
|
2561
|
+
continue;
|
|
2562
|
+
}
|
|
2563
|
+
const mergedValue = deepMergeObjects(prefab.components[componentName], overrideValue);
|
|
2564
|
+
diagnostics.push(
|
|
2565
|
+
...validateComponentValue(componentName, mergedValue, file, overridePath, codes.invalidOverride)
|
|
2566
|
+
);
|
|
2567
|
+
}
|
|
2568
|
+
return diagnostics;
|
|
2569
|
+
}
|
|
2570
|
+
function targetEntityNotFound(entityId, file, dataPath) {
|
|
2571
|
+
return [
|
|
2572
|
+
{
|
|
2573
|
+
severity: "error",
|
|
2574
|
+
code: "PATCH_TARGET_ENTITY_NOT_FOUND",
|
|
2575
|
+
file,
|
|
2576
|
+
path: dataPath,
|
|
2577
|
+
message: `Target entity '${entityId}' was not found.`,
|
|
2578
|
+
suggestion: "Inspect the scene and use an existing stable entity id."
|
|
2579
|
+
}
|
|
2580
|
+
];
|
|
2581
|
+
}
|
|
2582
|
+
function entityComponentsUnavailable(entityId, file, operationPath) {
|
|
2583
|
+
return [
|
|
2584
|
+
{
|
|
2585
|
+
severity: "error",
|
|
2586
|
+
code: "PATCH_ENTITY_COMPONENTS_UNAVAILABLE",
|
|
2587
|
+
file,
|
|
2588
|
+
path: `${operationPath}.entityId`,
|
|
2589
|
+
message: `Entity '${entityId}' does not define direct components.`,
|
|
2590
|
+
suggestion: "For prefab instances, edit overrides or instantiate a direct entity before component patch operations."
|
|
2591
|
+
}
|
|
2592
|
+
];
|
|
2593
|
+
}
|
|
2594
|
+
function unknownComponent(componentName, file, dataPath) {
|
|
2595
|
+
return [
|
|
2596
|
+
{
|
|
2597
|
+
severity: "error",
|
|
2598
|
+
code: "PATCH_UNKNOWN_COMPONENT",
|
|
2599
|
+
file,
|
|
2600
|
+
path: dataPath,
|
|
2601
|
+
message: `Unknown component '${componentName}'.`,
|
|
2602
|
+
suggestion: `Use one of: ${componentOrder.join(", ")}.`
|
|
2603
|
+
}
|
|
2604
|
+
];
|
|
2605
|
+
}
|
|
2606
|
+
function entityIndexFromSourcePath(sourcePath) {
|
|
2607
|
+
const match = /^\$\.entities\[(\d+)\]$/.exec(sourcePath);
|
|
2608
|
+
if (!match) {
|
|
2609
|
+
return void 0;
|
|
2610
|
+
}
|
|
2611
|
+
return Number(match[1]);
|
|
2612
|
+
}
|
|
2613
|
+
function patchSourceSelectorInvalid(file, dataPath, message, suggestion) {
|
|
2614
|
+
return [
|
|
2615
|
+
{
|
|
2616
|
+
severity: "error",
|
|
2617
|
+
code: "PATCH_SOURCE_SELECTOR_INVALID",
|
|
2618
|
+
file,
|
|
2619
|
+
path: dataPath,
|
|
2620
|
+
message,
|
|
2621
|
+
suggestion
|
|
2622
|
+
}
|
|
2623
|
+
];
|
|
2624
|
+
}
|
|
2625
|
+
function patchSourceSelectorMismatch(file, dataPath, message) {
|
|
2626
|
+
return [
|
|
2627
|
+
{
|
|
2628
|
+
severity: "error",
|
|
2629
|
+
code: "PATCH_SOURCE_SELECTOR_MISMATCH",
|
|
2630
|
+
file,
|
|
2631
|
+
path: dataPath,
|
|
2632
|
+
message,
|
|
2633
|
+
suggestion: "Re-run validation and regenerate the selector-based repair artifact from current source evidence."
|
|
2634
|
+
}
|
|
2635
|
+
];
|
|
2636
|
+
}
|
|
2637
|
+
function findEntity(scene, entityId) {
|
|
2638
|
+
return scene.entities.find((entity) => entity.id === entityId);
|
|
2639
|
+
}
|
|
2640
|
+
function findEntityReferences(scene, targetEntityId) {
|
|
2641
|
+
const references = [];
|
|
2642
|
+
scene.entities.forEach((entity, entityIndex) => {
|
|
2643
|
+
if (!isRecord(entity) || typeof entity.id !== "string" || !isRecord(entity.components)) {
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
for (const [componentName, componentValue] of Object.entries(entity.components ?? {})) {
|
|
2647
|
+
if (!isRecord(componentValue)) {
|
|
2648
|
+
continue;
|
|
2649
|
+
}
|
|
2650
|
+
for (const referenceField of componentReferenceFields[componentName] ?? []) {
|
|
2651
|
+
const referencedId = componentValue[referenceField];
|
|
2652
|
+
if (typeof referencedId !== "string") {
|
|
2653
|
+
continue;
|
|
2654
|
+
}
|
|
2655
|
+
if (targetEntityId && referencedId !== targetEntityId) {
|
|
2656
|
+
continue;
|
|
2657
|
+
}
|
|
2658
|
+
references.push({
|
|
2659
|
+
entityId: entity.id,
|
|
2660
|
+
component: componentName,
|
|
2661
|
+
field: referenceField,
|
|
2662
|
+
path: `$.entities[${entityIndex}].components.${componentName}.${referenceField}`,
|
|
2663
|
+
value: referencedId
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
});
|
|
2668
|
+
return references;
|
|
2669
|
+
}
|
|
2670
|
+
function replaceReferences(scene, fromEntityId, toEntityId, summary) {
|
|
2671
|
+
const changes = [];
|
|
2672
|
+
for (const reference of findEntityReferences(scene, fromEntityId)) {
|
|
2673
|
+
const entity = findEntity(scene, reference.entityId);
|
|
2674
|
+
const componentValue = entity?.components?.[reference.component];
|
|
2675
|
+
if (!isRecord(componentValue)) {
|
|
2676
|
+
continue;
|
|
2677
|
+
}
|
|
2678
|
+
componentValue[reference.field] = toEntityId;
|
|
2679
|
+
const change = {
|
|
2680
|
+
...reference,
|
|
2681
|
+
from: fromEntityId,
|
|
2682
|
+
to: toEntityId
|
|
2683
|
+
};
|
|
2684
|
+
changes.push(change);
|
|
2685
|
+
summary.changedReferences.push(change);
|
|
2686
|
+
}
|
|
2687
|
+
return changes;
|
|
2688
|
+
}
|
|
2689
|
+
function deriveHierarchy(scene, file = "scene.yml") {
|
|
2690
|
+
const ids = scene.entities.map((entity) => entity.id).filter((entityId) => typeof entityId === "string").sort();
|
|
2691
|
+
const idSet = new Set(ids);
|
|
2692
|
+
const parentByEntityId = {};
|
|
2693
|
+
const childrenByEntityId = Object.fromEntries(ids.map((entityId) => [entityId, []]));
|
|
2694
|
+
const diagnostics = [];
|
|
2695
|
+
for (const [entityIndex, entity] of scene.entities.entries()) {
|
|
2696
|
+
const parentEntityId = getParentEntityId(entity);
|
|
2697
|
+
if (typeof entity.id !== "string" || parentEntityId === void 0) {
|
|
2698
|
+
continue;
|
|
2699
|
+
}
|
|
2700
|
+
const parentPath = `$.entities[${entityIndex}].components.Parent.entityId`;
|
|
2701
|
+
if (!idSet.has(parentEntityId)) {
|
|
2702
|
+
diagnostics.push({
|
|
2703
|
+
severity: "error",
|
|
2704
|
+
code: "SCENE_BROKEN_ENTITY_REFERENCE",
|
|
2705
|
+
file,
|
|
2706
|
+
path: parentPath,
|
|
2707
|
+
message: `Parent entity reference '${parentEntityId}' does not resolve.`,
|
|
2708
|
+
suggestion: "Use the stable id of an entity in this scene."
|
|
2709
|
+
});
|
|
2710
|
+
continue;
|
|
2711
|
+
}
|
|
2712
|
+
if (parentEntityId === entity.id) {
|
|
2713
|
+
diagnostics.push(selfParentDiagnostic(entity.id, file, parentPath));
|
|
2714
|
+
continue;
|
|
2715
|
+
}
|
|
2716
|
+
parentByEntityId[entity.id] = parentEntityId;
|
|
2717
|
+
childrenByEntityId[parentEntityId]?.push(entity.id);
|
|
2718
|
+
}
|
|
2719
|
+
for (const children of Object.values(childrenByEntityId)) {
|
|
2720
|
+
children.sort();
|
|
2721
|
+
}
|
|
2722
|
+
diagnostics.push(...findParentCycleDiagnostics(scene, file, parentByEntityId));
|
|
2723
|
+
return {
|
|
2724
|
+
roots: ids.filter((entityId) => !Object.hasOwn(parentByEntityId, entityId)),
|
|
2725
|
+
childrenByEntityId,
|
|
2726
|
+
parentByEntityId,
|
|
2727
|
+
depthByEntityId: computeHierarchyDepths(ids, parentByEntityId),
|
|
2728
|
+
diagnostics
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
function computeWorldTransforms(scene, file = "scene.yml") {
|
|
2732
|
+
const hierarchy = deriveHierarchy(scene, file);
|
|
2733
|
+
const entityById = new Map(scene.entities.map((entity) => [entity.id, entity]));
|
|
2734
|
+
const transforms = {};
|
|
2735
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
2736
|
+
function resolveWorldTransform(entityId) {
|
|
2737
|
+
if (Object.hasOwn(transforms, entityId)) {
|
|
2738
|
+
return transforms[entityId];
|
|
2739
|
+
}
|
|
2740
|
+
if (visiting.has(entityId)) {
|
|
2741
|
+
return void 0;
|
|
2742
|
+
}
|
|
2743
|
+
visiting.add(entityId);
|
|
2744
|
+
const entity = entityById.get(entityId);
|
|
2745
|
+
const localTransform = getTransformSummary(entity);
|
|
2746
|
+
if (!localTransform) {
|
|
2747
|
+
visiting.delete(entityId);
|
|
2748
|
+
return void 0;
|
|
2749
|
+
}
|
|
2750
|
+
const parentId = hierarchy.parentByEntityId[entityId];
|
|
2751
|
+
const parentTransform = parentId ? resolveWorldTransform(parentId) : void 0;
|
|
2752
|
+
const worldTransform = parentTransform ? composeTransform(parentTransform, localTransform) : localTransform;
|
|
2753
|
+
transforms[entityId] = worldTransform;
|
|
2754
|
+
visiting.delete(entityId);
|
|
2755
|
+
return worldTransform;
|
|
2756
|
+
}
|
|
2757
|
+
for (const entityId of scene.entities.map((entity) => entity.id).filter((id) => typeof id === "string").sort()) {
|
|
2758
|
+
resolveWorldTransform(entityId);
|
|
2759
|
+
}
|
|
2760
|
+
return {
|
|
2761
|
+
transforms,
|
|
2762
|
+
diagnostics: hierarchy.diagnostics
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
function validateHierarchyRules(scene, file, ids) {
|
|
2766
|
+
const diagnostics = [];
|
|
2767
|
+
const parentByEntityId = {};
|
|
2768
|
+
for (const [entityIndex, entity] of scene.entities.entries()) {
|
|
2769
|
+
const parentEntityId = getParentEntityId(entity);
|
|
2770
|
+
if (typeof entity.id !== "string" || parentEntityId === void 0 || !ids.has(parentEntityId)) {
|
|
2771
|
+
continue;
|
|
2772
|
+
}
|
|
2773
|
+
const parentPath = `$.entities[${entityIndex}].components.Parent.entityId`;
|
|
2774
|
+
if (parentEntityId === entity.id) {
|
|
2775
|
+
diagnostics.push(selfParentDiagnostic(entity.id, file, parentPath));
|
|
2776
|
+
continue;
|
|
2777
|
+
}
|
|
2778
|
+
parentByEntityId[entity.id] = parentEntityId;
|
|
2779
|
+
}
|
|
2780
|
+
diagnostics.push(...findParentCycleDiagnostics(scene, file, parentByEntityId));
|
|
2781
|
+
return diagnostics;
|
|
2782
|
+
}
|
|
2783
|
+
function validateAnimationRules(scene, file) {
|
|
2784
|
+
const diagnostics = [];
|
|
2785
|
+
const clips = /* @__PURE__ */ new Map();
|
|
2786
|
+
for (const [entityIndex, entity] of scene.entities.entries()) {
|
|
2787
|
+
const clip = getComponentRecord(entity, "AnimationClip");
|
|
2788
|
+
if (!clip || typeof clip.id !== "string") {
|
|
2789
|
+
continue;
|
|
2790
|
+
}
|
|
2791
|
+
const frames = Array.isArray(clip.frames) ? clip.frames : [];
|
|
2792
|
+
const frameIds = /* @__PURE__ */ new Set();
|
|
2793
|
+
frames.forEach((frame, frameIndex) => {
|
|
2794
|
+
if (!isRecord(frame) || typeof frame.id !== "string") {
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2797
|
+
if (frameIds.has(frame.id)) {
|
|
2798
|
+
diagnostics.push({
|
|
2799
|
+
severity: "error",
|
|
2800
|
+
code: "SCENE_DUPLICATE_ANIMATION_FRAME_ID",
|
|
2801
|
+
file,
|
|
2802
|
+
path: `$.entities[${entityIndex}].components.AnimationClip.frames[${frameIndex}].id`,
|
|
2803
|
+
message: `Duplicate animation frame id '${frame.id}' in clip '${clip.id}'.`,
|
|
2804
|
+
suggestion: "Use stable SpriteFrame.id values that are unique within each AnimationClip."
|
|
2805
|
+
});
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2808
|
+
frameIds.add(frame.id);
|
|
2809
|
+
});
|
|
2810
|
+
if (Array.isArray(clip.frameOrder)) {
|
|
2811
|
+
clip.frameOrder.forEach((frameId, frameOrderIndex) => {
|
|
2812
|
+
if (typeof frameId === "string" && !frameIds.has(frameId)) {
|
|
2813
|
+
diagnostics.push({
|
|
2814
|
+
severity: "error",
|
|
2815
|
+
code: "SCENE_BROKEN_ANIMATION_FRAME_REFERENCE",
|
|
2816
|
+
file,
|
|
2817
|
+
path: `$.entities[${entityIndex}].components.AnimationClip.frameOrder[${frameOrderIndex}]`,
|
|
2818
|
+
message: `AnimationClip frameOrder references missing frame '${frameId}'.`,
|
|
2819
|
+
suggestion: "Use a SpriteFrame.id defined in AnimationClip.frames."
|
|
2820
|
+
});
|
|
2821
|
+
}
|
|
2822
|
+
});
|
|
2823
|
+
}
|
|
2824
|
+
const clipPath = `$.entities[${entityIndex}].components.AnimationClip.id`;
|
|
2825
|
+
if (clips.has(clip.id)) {
|
|
2826
|
+
diagnostics.push({
|
|
2827
|
+
severity: "error",
|
|
2828
|
+
code: "SCENE_DUPLICATE_ANIMATION_CLIP_ID",
|
|
2829
|
+
file,
|
|
2830
|
+
path: clipPath,
|
|
2831
|
+
message: `Duplicate animation clip id '${clip.id}'.`,
|
|
2832
|
+
suggestion: "Use one stable AnimationClip.id per scene."
|
|
2833
|
+
});
|
|
2834
|
+
continue;
|
|
2835
|
+
}
|
|
2836
|
+
clips.set(clip.id, {
|
|
2837
|
+
entityId: entity.id,
|
|
2838
|
+
frameCount: Array.isArray(clip.frameOrder) ? clip.frameOrder.length : frames.length,
|
|
2839
|
+
frameIds
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
for (const [entityIndex, entity] of scene.entities.entries()) {
|
|
2843
|
+
const animator = getComponentRecord(entity, "Animator");
|
|
2844
|
+
if (!animator) {
|
|
2845
|
+
continue;
|
|
2846
|
+
}
|
|
2847
|
+
if (Array.isArray(animator.clips)) {
|
|
2848
|
+
animator.clips.forEach((clipId, clipIndex) => {
|
|
2849
|
+
if (typeof clipId === "string" && !clips.has(clipId)) {
|
|
2850
|
+
diagnostics.push({
|
|
2851
|
+
severity: "error",
|
|
2852
|
+
code: "SCENE_BROKEN_ANIMATION_CLIP_REFERENCE",
|
|
2853
|
+
file,
|
|
2854
|
+
path: `$.entities[${entityIndex}].components.Animator.clips[${clipIndex}]`,
|
|
2855
|
+
message: `Animator references missing animation clip '${clipId}'.`,
|
|
2856
|
+
suggestion: "Use an AnimationClip.id defined in this scene."
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
if (typeof animator.defaultClip === "string") {
|
|
2862
|
+
const defaultClipPath = `$.entities[${entityIndex}].components.Animator.defaultClip`;
|
|
2863
|
+
if (!clips.has(animator.defaultClip)) {
|
|
2864
|
+
diagnostics.push({
|
|
2865
|
+
severity: "error",
|
|
2866
|
+
code: "SCENE_BROKEN_ANIMATION_CLIP_REFERENCE",
|
|
2867
|
+
file,
|
|
2868
|
+
path: defaultClipPath,
|
|
2869
|
+
message: `Animator defaultClip '${animator.defaultClip}' does not resolve.`,
|
|
2870
|
+
suggestion: "Use an AnimationClip.id defined in this scene."
|
|
2871
|
+
});
|
|
2872
|
+
} else if (Array.isArray(animator.clips) && !animator.clips.includes(animator.defaultClip)) {
|
|
2873
|
+
diagnostics.push({
|
|
2874
|
+
severity: "error",
|
|
2875
|
+
code: "SCENE_ANIMATOR_DEFAULT_CLIP_NOT_LISTED",
|
|
2876
|
+
file,
|
|
2877
|
+
path: defaultClipPath,
|
|
2878
|
+
message: `Animator defaultClip '${animator.defaultClip}' is not listed in Animator.clips.`,
|
|
2879
|
+
suggestion: "Add the default clip id to Animator.clips or choose a listed clip."
|
|
2880
|
+
});
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
const activeClipId = typeof animator.activeClip === "string" ? animator.activeClip : void 0;
|
|
2884
|
+
const activeClip = activeClipId ? clips.get(activeClipId) : void 0;
|
|
2885
|
+
if (activeClipId) {
|
|
2886
|
+
const activeClipPath = `$.entities[${entityIndex}].components.Animator.activeClip`;
|
|
2887
|
+
if (!activeClip) {
|
|
2888
|
+
diagnostics.push({
|
|
2889
|
+
severity: "error",
|
|
2890
|
+
code: "SCENE_BROKEN_ANIMATION_CLIP_REFERENCE",
|
|
2891
|
+
file,
|
|
2892
|
+
path: activeClipPath,
|
|
2893
|
+
message: `Animator activeClip '${activeClipId}' does not resolve.`,
|
|
2894
|
+
suggestion: "Use an AnimationClip.id defined in this scene."
|
|
2895
|
+
});
|
|
2896
|
+
} else if (Array.isArray(animator.clips) && !animator.clips.includes(activeClipId)) {
|
|
2897
|
+
diagnostics.push({
|
|
2898
|
+
severity: "error",
|
|
2899
|
+
code: "SCENE_ANIMATOR_ACTIVE_CLIP_NOT_LISTED",
|
|
2900
|
+
file,
|
|
2901
|
+
path: activeClipPath,
|
|
2902
|
+
message: `Animator activeClip '${activeClipId}' is not listed in Animator.clips.`,
|
|
2903
|
+
suggestion: "Add the active clip id to Animator.clips or choose a listed clip."
|
|
2904
|
+
});
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
if (typeof animator.currentClip !== "string") {
|
|
2908
|
+
continue;
|
|
2909
|
+
}
|
|
2910
|
+
const currentClipPath = `$.entities[${entityIndex}].components.Animator.currentClip`;
|
|
2911
|
+
const currentClip = clips.get(animator.currentClip);
|
|
2912
|
+
if (!currentClip) {
|
|
2913
|
+
diagnostics.push({
|
|
2914
|
+
severity: "error",
|
|
2915
|
+
code: "SCENE_BROKEN_ANIMATION_CLIP_REFERENCE",
|
|
2916
|
+
file,
|
|
2917
|
+
path: currentClipPath,
|
|
2918
|
+
message: `Animator currentClip '${animator.currentClip}' does not resolve.`,
|
|
2919
|
+
suggestion: "Use an AnimationClip.id defined in this scene."
|
|
2920
|
+
});
|
|
2921
|
+
continue;
|
|
2922
|
+
}
|
|
2923
|
+
if (Array.isArray(animator.clips) && !animator.clips.includes(animator.currentClip)) {
|
|
2924
|
+
diagnostics.push({
|
|
2925
|
+
severity: "error",
|
|
2926
|
+
code: "SCENE_ANIMATOR_CURRENT_CLIP_NOT_LISTED",
|
|
2927
|
+
file,
|
|
2928
|
+
path: currentClipPath,
|
|
2929
|
+
message: `Animator currentClip '${animator.currentClip}' is not listed in Animator.clips.`,
|
|
2930
|
+
suggestion: "Add the current clip id to Animator.clips or choose a listed clip."
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
const playbackClip = activeClip ?? currentClip;
|
|
2934
|
+
if (typeof animator.activeFrameId === "string" && playbackClip && !playbackClip.frameIds.has(animator.activeFrameId)) {
|
|
2935
|
+
diagnostics.push({
|
|
2936
|
+
severity: "error",
|
|
2937
|
+
code: "SCENE_BROKEN_ANIMATION_FRAME_REFERENCE",
|
|
2938
|
+
file,
|
|
2939
|
+
path: `$.entities[${entityIndex}].components.Animator.activeFrameId`,
|
|
2940
|
+
message: `Animator activeFrameId '${animator.activeFrameId}' does not resolve in '${activeClipId ?? animator.currentClip}'.`,
|
|
2941
|
+
suggestion: "Use a SpriteFrame.id from the active animation clip."
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
if (typeof animator.currentFrameIndex === "number" && Number.isInteger(animator.currentFrameIndex) && animator.currentFrameIndex >= playbackClip.frameCount) {
|
|
2945
|
+
diagnostics.push({
|
|
2946
|
+
severity: "error",
|
|
2947
|
+
code: "SCENE_ANIMATOR_FRAME_OUT_OF_RANGE",
|
|
2948
|
+
file,
|
|
2949
|
+
path: `$.entities[${entityIndex}].components.Animator.currentFrameIndex`,
|
|
2950
|
+
message: `Animator currentFrameIndex ${animator.currentFrameIndex} is outside '${activeClipId ?? animator.currentClip}'.`,
|
|
2951
|
+
suggestion: `Use a frame index from 0 to ${Math.max(playbackClip.frameCount - 1, 0)} or omit currentFrameIndex.`
|
|
2952
|
+
});
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
return diagnostics;
|
|
2956
|
+
}
|
|
2957
|
+
function validate3DContractRules(scene, file) {
|
|
2958
|
+
const diagnostics = [];
|
|
2959
|
+
const meshRefs = /* @__PURE__ */ new Map();
|
|
2960
|
+
for (const [entityIndex, entity] of scene.entities.entries()) {
|
|
2961
|
+
const transform = getComponentRecord(entity, "Transform3D");
|
|
2962
|
+
if (transform) {
|
|
2963
|
+
diagnostics.push(...validateTransform3DValue(transform, file, `$.entities[${entityIndex}].components.Transform3D`));
|
|
2964
|
+
}
|
|
2965
|
+
const camera = getComponentRecord(entity, "Camera3D");
|
|
2966
|
+
if (camera) {
|
|
2967
|
+
diagnostics.push(...validateCamera3DValue(camera, file, `$.entities[${entityIndex}].components.Camera3D`));
|
|
2968
|
+
}
|
|
2969
|
+
const collider = getComponentRecord(entity, "Collider3D");
|
|
2970
|
+
if (collider) {
|
|
2971
|
+
diagnostics.push(...validateCollider3DValue(collider, file, `$.entities[${entityIndex}].components.Collider3D`));
|
|
2972
|
+
}
|
|
2973
|
+
const meshRef = getComponentRecord(entity, "MeshRef");
|
|
2974
|
+
if (!meshRef || typeof meshRef.id !== "string") {
|
|
2975
|
+
continue;
|
|
2976
|
+
}
|
|
2977
|
+
const meshPath = `$.entities[${entityIndex}].components.MeshRef`;
|
|
2978
|
+
if (meshRefs.has(meshRef.id)) {
|
|
2979
|
+
diagnostics.push({
|
|
2980
|
+
severity: "error",
|
|
2981
|
+
code: "SCENE_DUPLICATE_MESH_REF_ID",
|
|
2982
|
+
file,
|
|
2983
|
+
path: `${meshPath}.id`,
|
|
2984
|
+
message: `Duplicate MeshRef id '${meshRef.id}'.`,
|
|
2985
|
+
suggestion: "Use one stable MeshRef.id per expanded scene."
|
|
2986
|
+
});
|
|
2987
|
+
} else {
|
|
2988
|
+
meshRefs.set(meshRef.id, {
|
|
2989
|
+
entityId: entity.id,
|
|
2990
|
+
path: meshPath
|
|
2991
|
+
});
|
|
2992
|
+
}
|
|
2993
|
+
if (typeof meshRef.path === "string" && !isPortableAssetPath(meshRef.path)) {
|
|
2994
|
+
diagnostics.push({
|
|
2995
|
+
severity: "error",
|
|
2996
|
+
code: "SCENE_MESH_PATH_INVALID",
|
|
2997
|
+
file,
|
|
2998
|
+
path: `${meshPath}.path`,
|
|
2999
|
+
message: `Mesh path '${meshRef.path}' is not a portable project-relative path.`,
|
|
3000
|
+
suggestion: "Use a relative path with forward slashes, no drive letters, no leading slash, and no '..' segments."
|
|
3001
|
+
});
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
for (const [entityIndex, entity] of scene.entities.entries()) {
|
|
3005
|
+
const renderer = getComponentRecord(entity, "MeshRenderer3D");
|
|
3006
|
+
if (!renderer || typeof renderer.mesh !== "string") {
|
|
3007
|
+
continue;
|
|
3008
|
+
}
|
|
3009
|
+
if (!meshRefs.has(renderer.mesh)) {
|
|
3010
|
+
diagnostics.push({
|
|
3011
|
+
severity: "error",
|
|
3012
|
+
code: "SCENE_BROKEN_MESH_REFERENCE",
|
|
3013
|
+
file,
|
|
3014
|
+
path: `$.entities[${entityIndex}].components.MeshRenderer3D.mesh`,
|
|
3015
|
+
message: `MeshRenderer3D references missing MeshRef '${renderer.mesh}'.`,
|
|
3016
|
+
suggestion: "Use a MeshRef.id defined in this expanded scene."
|
|
3017
|
+
});
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
return diagnostics;
|
|
3021
|
+
}
|
|
3022
|
+
function createHierarchySummary(hierarchy) {
|
|
3023
|
+
const entityIds = Object.keys(hierarchy.childrenByEntityId).sort();
|
|
3024
|
+
return {
|
|
3025
|
+
roots: [...hierarchy.roots].sort(),
|
|
3026
|
+
nodes: entityIds.map((entityId) => ({
|
|
3027
|
+
entityId,
|
|
3028
|
+
...hierarchy.parentByEntityId[entityId] ? { parent: hierarchy.parentByEntityId[entityId] } : {},
|
|
3029
|
+
children: hierarchy.childrenByEntityId[entityId] ?? [],
|
|
3030
|
+
depth: hierarchy.depthByEntityId[entityId] ?? 0
|
|
3031
|
+
}))
|
|
3032
|
+
};
|
|
3033
|
+
}
|
|
3034
|
+
function inspectSceneAnimations(scene, assets = createAssetManifestContext()) {
|
|
3035
|
+
const clips = [];
|
|
3036
|
+
const animators = [];
|
|
3037
|
+
for (const entity of scene.entities) {
|
|
3038
|
+
const clip = getComponentRecord(entity, "AnimationClip");
|
|
3039
|
+
if (clip && typeof clip.id === "string" && Array.isArray(clip.frames)) {
|
|
3040
|
+
const frameOrder = Array.isArray(clip.frameOrder) ? clip.frameOrder.filter((frameId) => typeof frameId === "string") : void 0;
|
|
3041
|
+
const orderedFrameIds = orderedAnimationFrameIds(clip.frames, frameOrder);
|
|
3042
|
+
clips.push({
|
|
3043
|
+
id: clip.id,
|
|
3044
|
+
entityId: entity.id,
|
|
3045
|
+
frameCount: clip.frames.length,
|
|
3046
|
+
durationFrames: sumFrameDurations(clip.frames),
|
|
3047
|
+
...typeof clip.fps === "number" ? { fps: clip.fps } : {},
|
|
3048
|
+
...frameOrder ? { frameOrder } : {},
|
|
3049
|
+
orderedFrameIds,
|
|
3050
|
+
spriteAssetIds: [
|
|
3051
|
+
...new Set(
|
|
3052
|
+
clip.frames.map((frame) => isRecord(frame) && typeof frame.sprite === "string" ? frame.sprite : void 0).filter((sprite) => typeof sprite === "string")
|
|
3053
|
+
)
|
|
3054
|
+
].sort(),
|
|
3055
|
+
frames: createAnimationFrameSummaries(clip.frames, orderedFrameIds, assets)
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
const animator = getComponentRecord(entity, "Animator");
|
|
3059
|
+
if (animator && Array.isArray(animator.clips) && typeof animator.currentClip === "string") {
|
|
3060
|
+
animators.push({
|
|
3061
|
+
entityId: entity.id,
|
|
3062
|
+
clips: animator.clips.filter((clipId) => typeof clipId === "string"),
|
|
3063
|
+
...typeof animator.defaultClip === "string" ? { defaultClip: animator.defaultClip } : {},
|
|
3064
|
+
currentClip: animator.currentClip,
|
|
3065
|
+
...typeof animator.activeClip === "string" ? { activeClip: animator.activeClip } : {},
|
|
3066
|
+
...typeof animator.activeFrameId === "string" ? { activeFrameId: animator.activeFrameId } : {},
|
|
3067
|
+
...typeof animator.currentFrameIndex === "number" ? { currentFrameIndex: animator.currentFrameIndex } : {},
|
|
3068
|
+
...typeof animator.elapsedFrames === "number" ? { elapsedFrames: animator.elapsedFrames } : {},
|
|
3069
|
+
...typeof animator.elapsedSeconds === "number" ? { elapsedSeconds: animator.elapsedSeconds } : {}
|
|
3070
|
+
});
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
return {
|
|
3074
|
+
clips: clips.sort((left, right) => left.id.localeCompare(right.id) || left.entityId.localeCompare(right.entityId)),
|
|
3075
|
+
animators: animators.sort((left, right) => left.entityId.localeCompare(right.entityId))
|
|
3076
|
+
};
|
|
3077
|
+
}
|
|
3078
|
+
function orderedAnimationFrameIds(frames, frameOrder) {
|
|
3079
|
+
const frameIds = frames.map((frame) => isRecord(frame) && typeof frame.id === "string" ? frame.id : void 0).filter((frameId) => typeof frameId === "string");
|
|
3080
|
+
if (!frameOrder || frameOrder.length === 0) {
|
|
3081
|
+
return frameIds;
|
|
3082
|
+
}
|
|
3083
|
+
const frameIdSet = new Set(frameIds);
|
|
3084
|
+
return frameOrder.filter((frameId) => frameIdSet.has(frameId));
|
|
3085
|
+
}
|
|
3086
|
+
function createAnimationFrameSummaries(frames, orderedFrameIds, assets) {
|
|
3087
|
+
const playbackOrderByFrameId = new Map(orderedFrameIds.map((frameId, index) => [frameId, index]));
|
|
3088
|
+
return frames.flatMap((frame, index) => {
|
|
3089
|
+
if (!isRecord(frame) || typeof frame.id !== "string" || typeof frame.durationFrames !== "number") {
|
|
3090
|
+
return [];
|
|
3091
|
+
}
|
|
3092
|
+
const sprite = typeof frame.sprite === "string" ? frame.sprite : void 0;
|
|
3093
|
+
const asset = sprite ? assets.assetsById.get(sprite) : void 0;
|
|
3094
|
+
const atlasFrame = asset ? resolveAssetAtlasFrame(asset, assets) : void 0;
|
|
3095
|
+
const rect = readRectTuple(frame.rect);
|
|
3096
|
+
return [
|
|
3097
|
+
{
|
|
3098
|
+
id: frame.id,
|
|
3099
|
+
durationFrames: frame.durationFrames,
|
|
3100
|
+
order: index,
|
|
3101
|
+
playbackOrder: playbackOrderByFrameId.get(frame.id) ?? index,
|
|
3102
|
+
...sprite ? { sprite } : {},
|
|
3103
|
+
...rect ? { rect } : {},
|
|
3104
|
+
...sprite ? { assetResolved: asset?.type === "sprite" } : {},
|
|
3105
|
+
...asset ? { assetType: asset.type, assetPath: asset.path } : {},
|
|
3106
|
+
...asset?.texture ? { texture: asset.texture } : {},
|
|
3107
|
+
...asset?.rect ? { assetRect: asset.rect } : {},
|
|
3108
|
+
...asset?.atlas ? { assetAtlas: asset.atlas } : {},
|
|
3109
|
+
...asset?.frame ? { assetFrame: asset.frame } : {},
|
|
3110
|
+
...atlasFrame ? { assetFrameRect: atlasFrame.rect } : {}
|
|
3111
|
+
}
|
|
3112
|
+
];
|
|
3113
|
+
});
|
|
3114
|
+
}
|
|
3115
|
+
function resolveAssetAtlasFrame(asset, assets) {
|
|
3116
|
+
if (!asset.atlas || !asset.frame) {
|
|
3117
|
+
return void 0;
|
|
3118
|
+
}
|
|
3119
|
+
const atlas = assets.assetsById.get(asset.atlas);
|
|
3120
|
+
if (atlas?.type !== "atlas") {
|
|
3121
|
+
return void 0;
|
|
3122
|
+
}
|
|
3123
|
+
return atlas.frames?.find((frame) => frame.id === asset.frame);
|
|
3124
|
+
}
|
|
3125
|
+
function readRectTuple(value) {
|
|
3126
|
+
if (!Array.isArray(value) || value.length !== 4 || !value.every((item) => typeof item === "number" && Number.isFinite(item))) {
|
|
3127
|
+
return void 0;
|
|
3128
|
+
}
|
|
3129
|
+
return [value[0], value[1], value[2], value[3]];
|
|
3130
|
+
}
|
|
3131
|
+
function validateSceneAssetReferences(scene, file, assets) {
|
|
3132
|
+
const diagnostics = [];
|
|
3133
|
+
for (const reference of findSceneAssetReferences(scene)) {
|
|
3134
|
+
const asset = assets.assetsById.get(reference.assetId);
|
|
3135
|
+
if (!asset) {
|
|
3136
|
+
diagnostics.push({
|
|
3137
|
+
severity: "error",
|
|
3138
|
+
code: "SCENE_BROKEN_ASSET_REFERENCE",
|
|
3139
|
+
file,
|
|
3140
|
+
path: reference.path,
|
|
3141
|
+
message: `Asset reference '${reference.assetId}' does not resolve.`,
|
|
3142
|
+
suggestion: "Add the asset id to a project asset manifest or update the scene reference."
|
|
3143
|
+
});
|
|
3144
|
+
continue;
|
|
3145
|
+
}
|
|
3146
|
+
if (asset.type !== reference.expectedType) {
|
|
3147
|
+
diagnostics.push({
|
|
3148
|
+
severity: "error",
|
|
3149
|
+
code: "SCENE_ASSET_TYPE_MISMATCH",
|
|
3150
|
+
file,
|
|
3151
|
+
path: reference.path,
|
|
3152
|
+
message: `Asset reference '${reference.assetId}' is '${asset.type}', not '${reference.expectedType}'.`,
|
|
3153
|
+
suggestion: `Use an asset id with type '${reference.expectedType}' for this field.`
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
return diagnostics;
|
|
3158
|
+
}
|
|
3159
|
+
function createSceneAssetSummary(scene, assets) {
|
|
3160
|
+
const references = findSceneAssetReferences(scene).map((reference) => {
|
|
3161
|
+
const asset = assets.assetsById.get(reference.assetId);
|
|
3162
|
+
return {
|
|
3163
|
+
assetId: reference.assetId,
|
|
3164
|
+
expectedType: reference.expectedType,
|
|
3165
|
+
path: reference.path,
|
|
3166
|
+
entityId: reference.entityId,
|
|
3167
|
+
component: reference.component,
|
|
3168
|
+
field: reference.field,
|
|
3169
|
+
resolved: asset?.type === reference.expectedType,
|
|
3170
|
+
...asset ? { actualType: asset.type } : {}
|
|
3171
|
+
};
|
|
3172
|
+
});
|
|
3173
|
+
return {
|
|
3174
|
+
manifestCount: assets.manifestFiles.length,
|
|
3175
|
+
assetCount: assets.assetsById.size,
|
|
3176
|
+
atlasCount: [...assets.assetsById.values()].filter((asset) => asset.type === "atlas").length,
|
|
3177
|
+
atlasFrameCount: [...assets.assetsById.values()].reduce(
|
|
3178
|
+
(total, asset) => total + (asset.type === "atlas" ? asset.frames?.length ?? 0 : 0),
|
|
3179
|
+
0
|
|
3180
|
+
),
|
|
3181
|
+
referencedAssets: references,
|
|
3182
|
+
unresolvedReferences: references.filter((reference) => !reference.resolved)
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
3185
|
+
function findSceneAssetReferences(scene) {
|
|
3186
|
+
const references = [];
|
|
3187
|
+
scene.entities.forEach((entity, entityIndex) => {
|
|
3188
|
+
const clip = getComponentRecord(entity, "AnimationClip");
|
|
3189
|
+
if (!clip || !Array.isArray(clip.frames)) {
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
clip.frames.forEach((frame, frameIndex) => {
|
|
3193
|
+
if (!isRecord(frame) || typeof frame.sprite !== "string") {
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
references.push({
|
|
3197
|
+
assetId: frame.sprite,
|
|
3198
|
+
expectedType: "sprite",
|
|
3199
|
+
path: `$.entities[${entityIndex}].components.AnimationClip.frames[${frameIndex}].sprite`,
|
|
3200
|
+
entityId: entity.id,
|
|
3201
|
+
component: "AnimationClip",
|
|
3202
|
+
field: "frames[].sprite"
|
|
3203
|
+
});
|
|
3204
|
+
});
|
|
3205
|
+
});
|
|
3206
|
+
return references;
|
|
3207
|
+
}
|
|
3208
|
+
function getComponentRecord(entity, componentName) {
|
|
3209
|
+
if (!isRecord(entity.components)) {
|
|
3210
|
+
return void 0;
|
|
3211
|
+
}
|
|
3212
|
+
const component = entity.components[componentName];
|
|
3213
|
+
return isRecord(component) ? component : void 0;
|
|
3214
|
+
}
|
|
3215
|
+
function sumFrameDurations(frames) {
|
|
3216
|
+
return frames.reduce((total, frame) => {
|
|
3217
|
+
if (!isRecord(frame) || typeof frame.durationFrames !== "number") {
|
|
3218
|
+
return total;
|
|
3219
|
+
}
|
|
3220
|
+
return total + frame.durationFrames;
|
|
3221
|
+
}, 0);
|
|
3222
|
+
}
|
|
3223
|
+
function selfParentDiagnostic(entityId, file, dataPath) {
|
|
3224
|
+
return {
|
|
3225
|
+
severity: "error",
|
|
3226
|
+
code: "SCENE_SELF_PARENT",
|
|
3227
|
+
file,
|
|
3228
|
+
path: dataPath,
|
|
3229
|
+
message: `Entity '${entityId}' cannot parent itself.`,
|
|
3230
|
+
suggestion: "Use a different parent entity id or remove the Parent component."
|
|
3231
|
+
};
|
|
3232
|
+
}
|
|
3233
|
+
function findParentCycleDiagnostics(scene, file, parentByEntityId) {
|
|
3234
|
+
const diagnostics = [];
|
|
3235
|
+
const state = /* @__PURE__ */ new Map();
|
|
3236
|
+
const stack = [];
|
|
3237
|
+
const reportedCycles = /* @__PURE__ */ new Set();
|
|
3238
|
+
function visit(entityId) {
|
|
3239
|
+
const currentState = state.get(entityId);
|
|
3240
|
+
if (currentState === "visited") {
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
3243
|
+
if (currentState === "visiting") {
|
|
3244
|
+
const cycleStart = stack.indexOf(entityId);
|
|
3245
|
+
const cycleEntities = cycleStart >= 0 ? stack.slice(cycleStart) : [entityId];
|
|
3246
|
+
const cycleKey = [...cycleEntities].sort().join(">");
|
|
3247
|
+
if (!reportedCycles.has(cycleKey)) {
|
|
3248
|
+
reportedCycles.add(cycleKey);
|
|
3249
|
+
const cyclePath = [...cycleEntities, entityId].join(" -> ");
|
|
3250
|
+
diagnostics.push({
|
|
3251
|
+
severity: "error",
|
|
3252
|
+
code: "SCENE_PARENT_CYCLE",
|
|
3253
|
+
file,
|
|
3254
|
+
path: parentFieldPath(scene, cycleEntities[0] ?? entityId),
|
|
3255
|
+
message: `Parent cycle detected: ${cyclePath}.`,
|
|
3256
|
+
suggestion: "Clear or retarget one Parent component so the hierarchy is a tree or forest."
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
3259
|
+
return;
|
|
3260
|
+
}
|
|
3261
|
+
state.set(entityId, "visiting");
|
|
3262
|
+
stack.push(entityId);
|
|
3263
|
+
const parentEntityId = parentByEntityId[entityId];
|
|
3264
|
+
if (parentEntityId) {
|
|
3265
|
+
visit(parentEntityId);
|
|
3266
|
+
}
|
|
3267
|
+
stack.pop();
|
|
3268
|
+
state.set(entityId, "visited");
|
|
3269
|
+
}
|
|
3270
|
+
for (const entityId of Object.keys(parentByEntityId).sort()) {
|
|
3271
|
+
visit(entityId);
|
|
3272
|
+
}
|
|
3273
|
+
return diagnostics;
|
|
3274
|
+
}
|
|
3275
|
+
function computeHierarchyDepths(ids, parentByEntityId) {
|
|
3276
|
+
const depths = {};
|
|
3277
|
+
function depthOf(entityId, visiting = /* @__PURE__ */ new Set()) {
|
|
3278
|
+
if (Object.hasOwn(depths, entityId)) {
|
|
3279
|
+
return depths[entityId];
|
|
3280
|
+
}
|
|
3281
|
+
const parentEntityId = parentByEntityId[entityId];
|
|
3282
|
+
if (!parentEntityId || visiting.has(entityId)) {
|
|
3283
|
+
depths[entityId] = 0;
|
|
3284
|
+
return depths[entityId];
|
|
3285
|
+
}
|
|
3286
|
+
visiting.add(entityId);
|
|
3287
|
+
depths[entityId] = depthOf(parentEntityId, visiting) + 1;
|
|
3288
|
+
visiting.delete(entityId);
|
|
3289
|
+
return depths[entityId];
|
|
3290
|
+
}
|
|
3291
|
+
for (const entityId of ids) {
|
|
3292
|
+
depthOf(entityId);
|
|
3293
|
+
}
|
|
3294
|
+
return depths;
|
|
3295
|
+
}
|
|
3296
|
+
function parentFieldPath(scene, entityId) {
|
|
3297
|
+
const entityIndex = scene.entities.findIndex((entity) => entity.id === entityId);
|
|
3298
|
+
return entityIndex >= 0 ? `$.entities[${entityIndex}].components.Parent.entityId` : "$.entities";
|
|
3299
|
+
}
|
|
3300
|
+
function getParentEntityId(entity) {
|
|
3301
|
+
const parent = entity.components?.Parent;
|
|
3302
|
+
if (!isRecord(parent) || typeof parent.entityId !== "string") {
|
|
3303
|
+
return void 0;
|
|
3304
|
+
}
|
|
3305
|
+
return parent.entityId;
|
|
3306
|
+
}
|
|
3307
|
+
function getTransformSummary(entity) {
|
|
3308
|
+
const transform = entity?.components?.Transform;
|
|
3309
|
+
if (!isRecord(transform)) {
|
|
3310
|
+
return void 0;
|
|
3311
|
+
}
|
|
3312
|
+
const position = transform.position;
|
|
3313
|
+
const scale = transform.scale;
|
|
3314
|
+
const rotation = transform.rotation;
|
|
3315
|
+
if (!isVector2(position) || !isVector2(scale) || typeof rotation !== "number") {
|
|
3316
|
+
return void 0;
|
|
3317
|
+
}
|
|
3318
|
+
return {
|
|
3319
|
+
position: [position[0], position[1]],
|
|
3320
|
+
rotation,
|
|
3321
|
+
scale: [scale[0], scale[1]]
|
|
3322
|
+
};
|
|
3323
|
+
}
|
|
3324
|
+
function composeTransform(parent, local) {
|
|
3325
|
+
const scaledLocalX = local.position[0] * parent.scale[0];
|
|
3326
|
+
const scaledLocalY = local.position[1] * parent.scale[1];
|
|
3327
|
+
const cos = Math.cos(parent.rotation);
|
|
3328
|
+
const sin = Math.sin(parent.rotation);
|
|
3329
|
+
return {
|
|
3330
|
+
position: [
|
|
3331
|
+
roundDeterministic(parent.position[0] + scaledLocalX * cos - scaledLocalY * sin),
|
|
3332
|
+
roundDeterministic(parent.position[1] + scaledLocalX * sin + scaledLocalY * cos)
|
|
3333
|
+
],
|
|
3334
|
+
rotation: roundDeterministic(parent.rotation + local.rotation),
|
|
3335
|
+
scale: [
|
|
3336
|
+
roundDeterministic(parent.scale[0] * local.scale[0]),
|
|
3337
|
+
roundDeterministic(parent.scale[1] * local.scale[1])
|
|
3338
|
+
]
|
|
3339
|
+
};
|
|
3340
|
+
}
|
|
3341
|
+
function isVector2(value) {
|
|
3342
|
+
return Array.isArray(value) && value.length === 2 && value.every((item) => typeof item === "number");
|
|
3343
|
+
}
|
|
3344
|
+
function isVector3(value) {
|
|
3345
|
+
return Array.isArray(value) && value.length === 3 && value.every((item) => typeof item === "number");
|
|
3346
|
+
}
|
|
3347
|
+
function validateTransform3DValue(transform, file, basePath) {
|
|
3348
|
+
const diagnostics = [];
|
|
3349
|
+
diagnostics.push(...validateFiniteVector3(transform.position, file, `${basePath}.position`, "SCENE_3D_COORDINATE_INVALID"));
|
|
3350
|
+
diagnostics.push(...validateFiniteVector3(transform.rotation, file, `${basePath}.rotation`, "SCENE_3D_ROTATION_INVALID"));
|
|
3351
|
+
diagnostics.push(
|
|
3352
|
+
...validateFiniteVector3(transform.scale, file, `${basePath}.scale`, "SCENE_3D_SCALE_INVALID", {
|
|
3353
|
+
requirePositive: true
|
|
3354
|
+
})
|
|
3355
|
+
);
|
|
3356
|
+
return diagnostics;
|
|
3357
|
+
}
|
|
3358
|
+
function validateCamera3DValue(camera, file, basePath) {
|
|
3359
|
+
const diagnostics = [];
|
|
3360
|
+
if (typeof camera.near === "number" && typeof camera.far === "number" && camera.far <= camera.near) {
|
|
3361
|
+
diagnostics.push({
|
|
3362
|
+
severity: "error",
|
|
3363
|
+
code: "SCENE_3D_CAMERA_RANGE_INVALID",
|
|
3364
|
+
file,
|
|
3365
|
+
path: `${basePath}.far`,
|
|
3366
|
+
message: `Camera3D far plane ${camera.far} must be greater than near plane ${camera.near}.`,
|
|
3367
|
+
suggestion: "Use a far value greater than near."
|
|
3368
|
+
});
|
|
3369
|
+
}
|
|
3370
|
+
if (camera.projection === "perspective" && typeof camera.fov !== "number") {
|
|
3371
|
+
diagnostics.push({
|
|
3372
|
+
severity: "error",
|
|
3373
|
+
code: "SCENE_3D_CAMERA_FOV_REQUIRED",
|
|
3374
|
+
file,
|
|
3375
|
+
path: `${basePath}.fov`,
|
|
3376
|
+
message: "Perspective Camera3D requires fov.",
|
|
3377
|
+
suggestion: "Add fov with a value greater than 0 and less than 180."
|
|
3378
|
+
});
|
|
3379
|
+
}
|
|
3380
|
+
if (camera.projection === "orthographic" && typeof camera.orthographicSize !== "number") {
|
|
3381
|
+
diagnostics.push({
|
|
3382
|
+
severity: "error",
|
|
3383
|
+
code: "SCENE_3D_CAMERA_SIZE_REQUIRED",
|
|
3384
|
+
file,
|
|
3385
|
+
path: `${basePath}.orthographicSize`,
|
|
3386
|
+
message: "Orthographic Camera3D requires orthographicSize.",
|
|
3387
|
+
suggestion: "Add orthographicSize with a positive value."
|
|
3388
|
+
});
|
|
3389
|
+
}
|
|
3390
|
+
return diagnostics;
|
|
3391
|
+
}
|
|
3392
|
+
function validateCollider3DValue(collider, file, basePath) {
|
|
3393
|
+
return validateFiniteVector3(collider.size, file, `${basePath}.size`, "SCENE_3D_COLLIDER_SIZE_INVALID", {
|
|
3394
|
+
requirePositive: true
|
|
3395
|
+
});
|
|
3396
|
+
}
|
|
3397
|
+
function validateFiniteVector3(value, file, dataPath, code, options = {}) {
|
|
3398
|
+
if (!isVector3(value)) {
|
|
3399
|
+
return [];
|
|
3400
|
+
}
|
|
3401
|
+
const diagnostics = [];
|
|
3402
|
+
value.forEach((item, index) => {
|
|
3403
|
+
if (!Number.isFinite(item) || Math.abs(item) > 1e6) {
|
|
3404
|
+
diagnostics.push({
|
|
3405
|
+
severity: "error",
|
|
3406
|
+
code,
|
|
3407
|
+
file,
|
|
3408
|
+
path: `${dataPath}[${index}]`,
|
|
3409
|
+
message: `3D vector value '${String(item)}' is outside the supported experimental range.`,
|
|
3410
|
+
suggestion: "Use finite coordinate values with absolute magnitude no greater than 1000000."
|
|
3411
|
+
});
|
|
3412
|
+
}
|
|
3413
|
+
if (options.requirePositive && item <= 0) {
|
|
3414
|
+
diagnostics.push({
|
|
3415
|
+
severity: "error",
|
|
3416
|
+
code,
|
|
3417
|
+
file,
|
|
3418
|
+
path: `${dataPath}[${index}]`,
|
|
3419
|
+
message: `3D vector value '${String(item)}' must be positive.`,
|
|
3420
|
+
suggestion: "Use positive non-zero scale and collider size values."
|
|
3421
|
+
});
|
|
3422
|
+
}
|
|
3423
|
+
});
|
|
3424
|
+
return diagnostics;
|
|
3425
|
+
}
|
|
3426
|
+
function roundDeterministic(value) {
|
|
3427
|
+
const rounded = Number(value.toFixed(12));
|
|
3428
|
+
const nearestInteger = Math.round(rounded);
|
|
3429
|
+
if (Math.abs(rounded - nearestInteger) < 1e-9) {
|
|
3430
|
+
return nearestInteger;
|
|
3431
|
+
}
|
|
3432
|
+
return rounded;
|
|
3433
|
+
}
|
|
3434
|
+
function createPatchSummary() {
|
|
3435
|
+
return {
|
|
3436
|
+
changedReferences: []
|
|
3437
|
+
};
|
|
3438
|
+
}
|
|
3439
|
+
function mergePrefabComponents(components, overrides) {
|
|
3440
|
+
const merged = cloneJson(components);
|
|
3441
|
+
if (!overrides) {
|
|
3442
|
+
return merged;
|
|
3443
|
+
}
|
|
3444
|
+
for (const [componentName, overrideValue] of Object.entries(overrides)) {
|
|
3445
|
+
if (Object.hasOwn(merged, componentName)) {
|
|
3446
|
+
merged[componentName] = deepMergeObjects(merged[componentName], overrideValue);
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
return merged;
|
|
3450
|
+
}
|
|
3451
|
+
function deepMergeObjects(existingValue, patchValue) {
|
|
3452
|
+
if (!isRecord(existingValue) || !isRecord(patchValue)) {
|
|
3453
|
+
return cloneJson(patchValue);
|
|
3454
|
+
}
|
|
3455
|
+
const merged = cloneJson(existingValue);
|
|
3456
|
+
for (const [key, value] of Object.entries(patchValue)) {
|
|
3457
|
+
merged[key] = isRecord(value) && isRecord(merged[key]) ? deepMergeObjects(merged[key], value) : cloneJson(value);
|
|
3458
|
+
}
|
|
3459
|
+
return merged;
|
|
3460
|
+
}
|
|
3461
|
+
function setJsonPointer(root, pointer, value, file, operationPath) {
|
|
3462
|
+
if (!pointer.startsWith("/")) {
|
|
3463
|
+
return [
|
|
3464
|
+
{
|
|
3465
|
+
severity: "error",
|
|
3466
|
+
code: "PATCH_INVALID_FIELD_PATH",
|
|
3467
|
+
file,
|
|
3468
|
+
path: `${operationPath}.path`,
|
|
3469
|
+
message: `Field path '${pointer}' is not a JSON Pointer.`,
|
|
3470
|
+
suggestion: "Use a JSON Pointer path such as /position/0 or /current."
|
|
3471
|
+
}
|
|
3472
|
+
];
|
|
3473
|
+
}
|
|
3474
|
+
const tokens = pointer.slice(1).split("/").map((token) => token.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
3475
|
+
let parent = root;
|
|
3476
|
+
for (const token of tokens.slice(0, -1)) {
|
|
3477
|
+
const next = getPointerChild(parent, token);
|
|
3478
|
+
if (next === void 0) {
|
|
3479
|
+
return [
|
|
3480
|
+
{
|
|
3481
|
+
severity: "error",
|
|
3482
|
+
code: "PATCH_FIELD_PATH_NOT_FOUND",
|
|
3483
|
+
file,
|
|
3484
|
+
path: `${operationPath}.path`,
|
|
3485
|
+
message: `Field path '${pointer}' does not resolve.`,
|
|
3486
|
+
suggestion: "Inspect the component and target an existing nested object or array element."
|
|
3487
|
+
}
|
|
3488
|
+
];
|
|
3489
|
+
}
|
|
3490
|
+
parent = next;
|
|
3491
|
+
}
|
|
3492
|
+
const finalToken = tokens[tokens.length - 1];
|
|
3493
|
+
if (finalToken === void 0) {
|
|
3494
|
+
return [];
|
|
3495
|
+
}
|
|
3496
|
+
if (Array.isArray(parent)) {
|
|
3497
|
+
if (!/^\d+$/.test(finalToken)) {
|
|
3498
|
+
return [
|
|
3499
|
+
{
|
|
3500
|
+
severity: "error",
|
|
3501
|
+
code: "PATCH_INVALID_ARRAY_INDEX",
|
|
3502
|
+
file,
|
|
3503
|
+
path: `${operationPath}.path`,
|
|
3504
|
+
message: `Array path token '${finalToken}' is not a numeric index.`,
|
|
3505
|
+
suggestion: "Use a numeric array index."
|
|
3506
|
+
}
|
|
3507
|
+
];
|
|
3508
|
+
}
|
|
3509
|
+
const index = Number(finalToken);
|
|
3510
|
+
if (index < 0 || index >= parent.length) {
|
|
3511
|
+
return [
|
|
3512
|
+
{
|
|
3513
|
+
severity: "error",
|
|
3514
|
+
code: "PATCH_ARRAY_INDEX_OUT_OF_RANGE",
|
|
3515
|
+
file,
|
|
3516
|
+
path: `${operationPath}.path`,
|
|
3517
|
+
message: `Array index '${index}' is out of range.`,
|
|
3518
|
+
suggestion: "Inspect the component and target an existing array index."
|
|
3519
|
+
}
|
|
3520
|
+
];
|
|
3521
|
+
}
|
|
3522
|
+
parent[index] = cloneJson(value);
|
|
3523
|
+
return [];
|
|
3524
|
+
}
|
|
3525
|
+
if (isRecord(parent)) {
|
|
3526
|
+
parent[finalToken] = cloneJson(value);
|
|
3527
|
+
return [];
|
|
3528
|
+
}
|
|
3529
|
+
return [
|
|
3530
|
+
{
|
|
3531
|
+
severity: "error",
|
|
3532
|
+
code: "PATCH_FIELD_PATH_NOT_FOUND",
|
|
3533
|
+
file,
|
|
3534
|
+
path: `${operationPath}.path`,
|
|
3535
|
+
message: `Field path '${pointer}' does not resolve to an object or array.`,
|
|
3536
|
+
suggestion: "Inspect the component and target an object field or array element."
|
|
3537
|
+
}
|
|
3538
|
+
];
|
|
3539
|
+
}
|
|
3540
|
+
function getPointerChild(parent, token) {
|
|
3541
|
+
if (Array.isArray(parent)) {
|
|
3542
|
+
if (!/^\d+$/.test(token)) {
|
|
3543
|
+
return void 0;
|
|
3544
|
+
}
|
|
3545
|
+
return parent[Number(token)];
|
|
3546
|
+
}
|
|
3547
|
+
if (isRecord(parent)) {
|
|
3548
|
+
return parent[token];
|
|
3549
|
+
}
|
|
3550
|
+
return void 0;
|
|
3551
|
+
}
|
|
3552
|
+
function resolvePatchScenePath(patchFile, sceneRef) {
|
|
3553
|
+
if (typeof sceneRef !== "string") {
|
|
3554
|
+
return void 0;
|
|
3555
|
+
}
|
|
3556
|
+
return path.resolve(path.dirname(patchFile), sceneRef);
|
|
3557
|
+
}
|
|
3558
|
+
function resolveScenePrefabPath(sceneFile, prefabRef) {
|
|
3559
|
+
return path.resolve(path.dirname(sceneFile), prefabRef);
|
|
3560
|
+
}
|
|
3561
|
+
function cloneJson(value) {
|
|
3562
|
+
return JSON.parse(JSON.stringify(value));
|
|
3563
|
+
}
|
|
3564
|
+
function hasErrors(diagnostics) {
|
|
3565
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
3566
|
+
}
|
|
3567
|
+
function resolveProjectFile(projectPath = ".", cwd = process.cwd()) {
|
|
3568
|
+
const resolved = path.resolve(cwd, projectPath);
|
|
3569
|
+
if (existsSync(resolved) && statSync(resolved).isDirectory()) {
|
|
3570
|
+
return path.join(resolved, "agent.project.yml");
|
|
3571
|
+
}
|
|
3572
|
+
return resolved;
|
|
3573
|
+
}
|
|
3574
|
+
async function loadYamlFile(file) {
|
|
3575
|
+
try {
|
|
3576
|
+
const source = await readFile(file, "utf8");
|
|
3577
|
+
return parseYamlSource(source, file);
|
|
3578
|
+
} catch (error) {
|
|
3579
|
+
return {
|
|
3580
|
+
file,
|
|
3581
|
+
diagnostics: [
|
|
3582
|
+
{
|
|
3583
|
+
severity: "error",
|
|
3584
|
+
code: "FILE_READ_FAILED",
|
|
3585
|
+
file,
|
|
3586
|
+
path: "$",
|
|
3587
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3588
|
+
suggestion: "Check that the path exists and is readable."
|
|
3589
|
+
}
|
|
3590
|
+
]
|
|
3591
|
+
};
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
function parseYamlSource(source, file) {
|
|
3595
|
+
try {
|
|
3596
|
+
const document = parse(source);
|
|
3597
|
+
if (document === null || document === void 0) {
|
|
3598
|
+
return {
|
|
3599
|
+
file,
|
|
3600
|
+
diagnostics: [
|
|
3601
|
+
{
|
|
3602
|
+
severity: "error",
|
|
3603
|
+
code: "YAML_EMPTY_DOCUMENT",
|
|
3604
|
+
file,
|
|
3605
|
+
path: "$",
|
|
3606
|
+
message: "YAML document is empty.",
|
|
3607
|
+
suggestion: "Add the required document fields before running validation again."
|
|
3608
|
+
}
|
|
3609
|
+
]
|
|
3610
|
+
};
|
|
3611
|
+
}
|
|
3612
|
+
return {
|
|
3613
|
+
file,
|
|
3614
|
+
document,
|
|
3615
|
+
diagnostics: []
|
|
3616
|
+
};
|
|
3617
|
+
} catch (error) {
|
|
3618
|
+
return {
|
|
3619
|
+
file,
|
|
3620
|
+
diagnostics: [
|
|
3621
|
+
{
|
|
3622
|
+
severity: "error",
|
|
3623
|
+
code: "YAML_PARSE_ERROR",
|
|
3624
|
+
file,
|
|
3625
|
+
path: "$",
|
|
3626
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3627
|
+
suggestion: "Fix the YAML syntax before running validation again."
|
|
3628
|
+
}
|
|
3629
|
+
]
|
|
3630
|
+
};
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
async function loadNearestProjectAssetManifestContext(sceneFile) {
|
|
3634
|
+
const projectFile = findNearestProjectFile(path.dirname(sceneFile));
|
|
3635
|
+
if (!projectFile) {
|
|
3636
|
+
return createAssetManifestContext();
|
|
3637
|
+
}
|
|
3638
|
+
const projectResult = await loadYamlFile(projectFile);
|
|
3639
|
+
const diagnostics = [...projectResult.diagnostics];
|
|
3640
|
+
if (projectResult.document === void 0) {
|
|
3641
|
+
return {
|
|
3642
|
+
...createAssetManifestContext(),
|
|
3643
|
+
diagnostics
|
|
3644
|
+
};
|
|
3645
|
+
}
|
|
3646
|
+
diagnostics.push(...validateProjectDocument(projectResult.document, projectFile));
|
|
3647
|
+
if (hasErrors(diagnostics)) {
|
|
3648
|
+
return {
|
|
3649
|
+
...createAssetManifestContext(),
|
|
3650
|
+
diagnostics
|
|
3651
|
+
};
|
|
3652
|
+
}
|
|
3653
|
+
const context = await loadProjectAssetManifestContext(projectFile, projectResult.document);
|
|
3654
|
+
return {
|
|
3655
|
+
manifestFiles: context.manifestFiles,
|
|
3656
|
+
assets: context.assets,
|
|
3657
|
+
assetsById: context.assetsById,
|
|
3658
|
+
diagnostics: [...diagnostics, ...context.diagnostics]
|
|
3659
|
+
};
|
|
3660
|
+
}
|
|
3661
|
+
function findNearestProjectFile(startDir) {
|
|
3662
|
+
let currentDir = path.resolve(startDir);
|
|
3663
|
+
while (true) {
|
|
3664
|
+
const candidate = path.join(currentDir, "agent.project.yml");
|
|
3665
|
+
if (existsSync(candidate)) {
|
|
3666
|
+
return candidate;
|
|
3667
|
+
}
|
|
3668
|
+
const parentDir = path.dirname(currentDir);
|
|
3669
|
+
if (parentDir === currentDir) {
|
|
3670
|
+
return void 0;
|
|
3671
|
+
}
|
|
3672
|
+
currentDir = parentDir;
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
function createAssetManifestContext() {
|
|
3676
|
+
return {
|
|
3677
|
+
manifestFiles: [],
|
|
3678
|
+
assets: [],
|
|
3679
|
+
assetsById: /* @__PURE__ */ new Map(),
|
|
3680
|
+
diagnostics: []
|
|
3681
|
+
};
|
|
3682
|
+
}
|
|
3683
|
+
function sortedAssetDefinitions(assetsById) {
|
|
3684
|
+
return [...assetsById.values()].sort((left, right) => left.id.localeCompare(right.id)).map((asset) => {
|
|
3685
|
+
if (asset.type === "sprite") {
|
|
3686
|
+
return {
|
|
3687
|
+
id: asset.id,
|
|
3688
|
+
type: "sprite",
|
|
3689
|
+
path: asset.path,
|
|
3690
|
+
...typeof asset.texture === "string" ? { texture: asset.texture } : {},
|
|
3691
|
+
...asset.rect ? { rect: asset.rect } : {},
|
|
3692
|
+
...typeof asset.atlas === "string" ? { atlas: asset.atlas } : {},
|
|
3693
|
+
...typeof asset.frame === "string" ? { frame: asset.frame } : {}
|
|
3694
|
+
};
|
|
3695
|
+
}
|
|
3696
|
+
if (asset.type === "atlas") {
|
|
3697
|
+
return {
|
|
3698
|
+
id: asset.id,
|
|
3699
|
+
type: "atlas",
|
|
3700
|
+
path: asset.path,
|
|
3701
|
+
...typeof asset.texture === "string" ? { texture: asset.texture } : {},
|
|
3702
|
+
frames: (asset.frames ?? []).map((frame) => ({
|
|
3703
|
+
id: frame.id,
|
|
3704
|
+
rect: frame.rect
|
|
3705
|
+
})).sort((left, right) => left.id.localeCompare(right.id))
|
|
3706
|
+
};
|
|
3707
|
+
}
|
|
3708
|
+
return {
|
|
3709
|
+
id: asset.id,
|
|
3710
|
+
type: asset.type,
|
|
3711
|
+
path: asset.path
|
|
3712
|
+
};
|
|
3713
|
+
});
|
|
3714
|
+
}
|
|
3715
|
+
function countAssetsByType(assets) {
|
|
3716
|
+
const counts = {};
|
|
3717
|
+
for (const asset of assets) {
|
|
3718
|
+
counts[asset.type] = (counts[asset.type] ?? 0) + 1;
|
|
3719
|
+
}
|
|
3720
|
+
return Object.fromEntries(Object.entries(counts).sort(([left], [right]) => left.localeCompare(right)));
|
|
3721
|
+
}
|
|
3722
|
+
function isPortableAssetPath(assetPath) {
|
|
3723
|
+
if (assetPath.length === 0) {
|
|
3724
|
+
return false;
|
|
3725
|
+
}
|
|
3726
|
+
if (assetPath.includes("\\") || assetPath.includes("://") || assetPath.startsWith("/") || path.isAbsolute(assetPath)) {
|
|
3727
|
+
return false;
|
|
3728
|
+
}
|
|
3729
|
+
return assetPath.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
3730
|
+
}
|
|
3731
|
+
function isPortableSourcePath(sourcePath) {
|
|
3732
|
+
if (sourcePath.length === 0) {
|
|
3733
|
+
return false;
|
|
3734
|
+
}
|
|
3735
|
+
if (sourcePath.includes("\\") || sourcePath.includes("://") || sourcePath.startsWith("/") || path.isAbsolute(sourcePath)) {
|
|
3736
|
+
return false;
|
|
3737
|
+
}
|
|
3738
|
+
return sourcePath.split("/").every((segment) => segment.length > 0 && segment !== ".");
|
|
3739
|
+
}
|
|
3740
|
+
function hasPrefabRole(prefabRefs, role) {
|
|
3741
|
+
return prefabRefs.some((prefabRef) => isRecord(prefabRef) && prefabRef.role === role);
|
|
3742
|
+
}
|
|
3743
|
+
function findMapPrefabRefByRole(prefabRefs, role) {
|
|
3744
|
+
return prefabRefs.find((prefabRef) => prefabRef.role === role);
|
|
3745
|
+
}
|
|
3746
|
+
async function validateMapPrefabRefs(config, file) {
|
|
3747
|
+
const diagnostics = [];
|
|
3748
|
+
for (const [index, prefabRef] of config.prefabRefs.entries()) {
|
|
3749
|
+
const prefabFile = resolveMapPrefabPath(file, prefabRef.prefab);
|
|
3750
|
+
const prefabResult = await loadPrefabFile(prefabFile);
|
|
3751
|
+
const prefabDiagnostics = [...prefabResult.diagnostics];
|
|
3752
|
+
if (prefabResult.document !== void 0) {
|
|
3753
|
+
prefabDiagnostics.push(...validatePrefabDocument(prefabResult.document, prefabResult.file));
|
|
3754
|
+
}
|
|
3755
|
+
if (prefabResult.document === void 0 || hasErrors(prefabDiagnostics)) {
|
|
3756
|
+
if (prefabResult.diagnostics.some((diagnostic) => diagnostic.code === "FILE_READ_FAILED")) {
|
|
3757
|
+
diagnostics.push({
|
|
3758
|
+
severity: "error",
|
|
3759
|
+
code: "MAP_PREFAB_REF_NOT_FOUND",
|
|
3760
|
+
file,
|
|
3761
|
+
path: `$.prefabRefs[${index}].prefab`,
|
|
3762
|
+
message: `Prefab file '${prefabRef.prefab}' was not found.`,
|
|
3763
|
+
suggestion: "Create the prefab file or update the prefab path relative to the map config file."
|
|
3764
|
+
});
|
|
3765
|
+
}
|
|
3766
|
+
diagnostics.push(...prefabDiagnostics);
|
|
3767
|
+
continue;
|
|
3768
|
+
}
|
|
3769
|
+
diagnostics.push(...validateMapPrefabRoleConsistency(prefabRef, prefabResult.document, file, index));
|
|
3770
|
+
}
|
|
3771
|
+
return diagnostics;
|
|
3772
|
+
}
|
|
3773
|
+
function validateMapPrefabRoleConsistency(prefabRef, prefab, file, index) {
|
|
3774
|
+
const diagnostics = [];
|
|
3775
|
+
const components = prefab.components ?? {};
|
|
3776
|
+
if (!Object.hasOwn(components, "Transform")) {
|
|
3777
|
+
diagnostics.push({
|
|
3778
|
+
severity: "error",
|
|
3779
|
+
code: "MAP_PREFAB_ROLE_MISMATCH",
|
|
3780
|
+
file,
|
|
3781
|
+
path: `$.prefabRefs[${index}].prefab`,
|
|
3782
|
+
message: `Prefab ref '${prefabRef.id}' with role '${prefabRef.role}' must provide Transform.`,
|
|
3783
|
+
suggestion: "Add Transform to the prefab or use a prefab intended for generated map placement."
|
|
3784
|
+
});
|
|
3785
|
+
}
|
|
3786
|
+
if (prefabRef.role === "platform" && !Object.hasOwn(components, "Collider")) {
|
|
3787
|
+
diagnostics.push({
|
|
3788
|
+
severity: "error",
|
|
3789
|
+
code: "MAP_PREFAB_ROLE_MISMATCH",
|
|
3790
|
+
file,
|
|
3791
|
+
path: `$.prefabRefs[${index}].prefab`,
|
|
3792
|
+
message: `Platform prefab ref '${prefabRef.id}' must provide Collider.`,
|
|
3793
|
+
suggestion: "Use a platform prefab with Transform and Collider."
|
|
3794
|
+
});
|
|
3795
|
+
}
|
|
3796
|
+
return diagnostics;
|
|
3797
|
+
}
|
|
3798
|
+
function resolveMapPrefabPath(configFile, prefabRef) {
|
|
3799
|
+
return path.resolve(path.dirname(configFile), prefabRef);
|
|
3800
|
+
}
|
|
3801
|
+
function resolveGeneratedScenePrefabRef(prefabRef, options) {
|
|
3802
|
+
if (!options.configFile || !options.sceneFile) {
|
|
3803
|
+
return prefabRef.prefab;
|
|
3804
|
+
}
|
|
3805
|
+
const prefabFile = resolveMapPrefabPath(options.configFile, prefabRef.prefab);
|
|
3806
|
+
const relativePath = path.relative(path.dirname(options.sceneFile), prefabFile).replace(/\\/g, "/");
|
|
3807
|
+
return relativePath.length > 0 ? relativePath : path.basename(prefabFile);
|
|
3808
|
+
}
|
|
3809
|
+
function hashStableObject(value) {
|
|
3810
|
+
const stable = JSON.stringify(orderObject(value));
|
|
3811
|
+
let hash = 2166136261;
|
|
3812
|
+
for (let index = 0; index < stable.length; index += 1) {
|
|
3813
|
+
hash ^= stable.charCodeAt(index);
|
|
3814
|
+
hash = Math.imul(hash, 16777619);
|
|
3815
|
+
}
|
|
3816
|
+
return `fnv1a-${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
3817
|
+
}
|
|
3818
|
+
function createSeededRandom(seed) {
|
|
3819
|
+
let state = seed >>> 0;
|
|
3820
|
+
return () => {
|
|
3821
|
+
state = Math.imul(state, 1664525) + 1013904223 >>> 0;
|
|
3822
|
+
return state / 4294967296;
|
|
3823
|
+
};
|
|
3824
|
+
}
|
|
3825
|
+
function randomInteger(rng, min, max) {
|
|
3826
|
+
const low = Math.ceil(Math.min(min, max));
|
|
3827
|
+
const high = Math.floor(Math.max(min, max));
|
|
3828
|
+
return low + Math.floor(rng() * (high - low + 1));
|
|
3829
|
+
}
|
|
3830
|
+
function clampInteger(value, min, max) {
|
|
3831
|
+
return Math.min(Math.max(Math.round(value), min), max);
|
|
3832
|
+
}
|
|
3833
|
+
function padNumber(value) {
|
|
3834
|
+
return String(value).padStart(3, "0");
|
|
3835
|
+
}
|
|
3836
|
+
function schemaDiagnostics(validator, value, file, code, basePath = "$") {
|
|
3837
|
+
const valid = validator(value);
|
|
3838
|
+
if (valid) {
|
|
3839
|
+
return [];
|
|
3840
|
+
}
|
|
3841
|
+
return (validator.errors ?? []).map((error) => ({
|
|
3842
|
+
severity: "error",
|
|
3843
|
+
code,
|
|
3844
|
+
file,
|
|
3845
|
+
path: joinDataPath(basePath, errorPath(error)),
|
|
3846
|
+
message: schemaMessage(error),
|
|
3847
|
+
suggestion: schemaSuggestion(error)
|
|
3848
|
+
}));
|
|
3849
|
+
}
|
|
3850
|
+
function errorPath(error) {
|
|
3851
|
+
const base = instancePathToDataPath(error.instancePath);
|
|
3852
|
+
if (error.keyword === "required" && isRecord(error.params) && typeof error.params.missingProperty === "string") {
|
|
3853
|
+
return `${base}.${error.params.missingProperty}`;
|
|
3854
|
+
}
|
|
3855
|
+
return base;
|
|
3856
|
+
}
|
|
3857
|
+
function instancePathToDataPath(instancePath) {
|
|
3858
|
+
if (!instancePath) {
|
|
3859
|
+
return "$";
|
|
3860
|
+
}
|
|
3861
|
+
return `$${instancePath.split("/").slice(1).map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~")).map((segment) => /^\d+$/.test(segment) ? `[${segment}]` : `.${segment}`).join("")}`;
|
|
3862
|
+
}
|
|
3863
|
+
function joinDataPath(basePath, childPath) {
|
|
3864
|
+
if (basePath === "$") {
|
|
3865
|
+
return childPath;
|
|
3866
|
+
}
|
|
3867
|
+
if (childPath === "$") {
|
|
3868
|
+
return basePath;
|
|
3869
|
+
}
|
|
3870
|
+
return `${basePath}${childPath.slice(1)}`;
|
|
3871
|
+
}
|
|
3872
|
+
function schemaMessage(error) {
|
|
3873
|
+
if (error.keyword === "required" && isRecord(error.params) && typeof error.params.missingProperty === "string") {
|
|
3874
|
+
return `Missing required property '${error.params.missingProperty}'.`;
|
|
3875
|
+
}
|
|
3876
|
+
if (error.keyword === "additionalProperties" && isRecord(error.params)) {
|
|
3877
|
+
return `Unknown property '${String(error.params.additionalProperty)}'.`;
|
|
3878
|
+
}
|
|
3879
|
+
return error.message ?? "Value does not match schema.";
|
|
3880
|
+
}
|
|
3881
|
+
function schemaSuggestion(error) {
|
|
3882
|
+
if (error.keyword === "required" && isRecord(error.params) && typeof error.params.missingProperty === "string") {
|
|
3883
|
+
return `Add '${error.params.missingProperty}'.`;
|
|
3884
|
+
}
|
|
3885
|
+
if (error.keyword === "type") {
|
|
3886
|
+
return "Use the expected value type for this field.";
|
|
3887
|
+
}
|
|
3888
|
+
if (error.keyword === "additionalProperties") {
|
|
3889
|
+
return "Remove the unknown property or add it to the engine schema.";
|
|
3890
|
+
}
|
|
3891
|
+
return void 0;
|
|
3892
|
+
}
|
|
3893
|
+
function toYaml(value) {
|
|
3894
|
+
return `${stringify(value, { lineWidth: 0 }).trimEnd()}
|
|
3895
|
+
`;
|
|
3896
|
+
}
|
|
3897
|
+
function orderComponents(components) {
|
|
3898
|
+
const ordered = {};
|
|
3899
|
+
const componentNames = [
|
|
3900
|
+
...componentOrder.filter((name) => Object.hasOwn(components, name)),
|
|
3901
|
+
...Object.keys(components).filter((name) => !componentOrder.includes(name)).sort()
|
|
3902
|
+
];
|
|
3903
|
+
for (const componentName of componentNames) {
|
|
3904
|
+
ordered[componentName] = orderObject(components[componentName], componentFieldOrder[componentName]);
|
|
3905
|
+
}
|
|
3906
|
+
return ordered;
|
|
3907
|
+
}
|
|
3908
|
+
function orderObject(value, preferredKeys = []) {
|
|
3909
|
+
if (Array.isArray(value)) {
|
|
3910
|
+
return value.map((item) => orderObject(item));
|
|
3911
|
+
}
|
|
3912
|
+
if (!isRecord(value)) {
|
|
3913
|
+
return value;
|
|
3914
|
+
}
|
|
3915
|
+
const result = {};
|
|
3916
|
+
const keys = [
|
|
3917
|
+
...preferredKeys.filter((key) => Object.hasOwn(value, key)),
|
|
3918
|
+
...Object.keys(value).filter((key) => !preferredKeys.includes(key)).sort()
|
|
3919
|
+
];
|
|
3920
|
+
for (const key of keys) {
|
|
3921
|
+
result[key] = orderObject(value[key]);
|
|
3922
|
+
}
|
|
3923
|
+
return result;
|
|
3924
|
+
}
|
|
3925
|
+
function isRecord(value) {
|
|
3926
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3927
|
+
}
|
|
3928
|
+
export {
|
|
3929
|
+
applyPatchDocument,
|
|
3930
|
+
applyPatchDocumentWithFileValidation,
|
|
3931
|
+
applyPatchFile,
|
|
3932
|
+
assetManifestSchema2 as assetManifestSchema,
|
|
3933
|
+
componentSchemas2 as componentSchemas,
|
|
3934
|
+
computeWorldTransforms,
|
|
3935
|
+
createMapGenerationSummary,
|
|
3936
|
+
deriveHierarchy,
|
|
3937
|
+
expandSceneDocument,
|
|
3938
|
+
formatGeneratedMap,
|
|
3939
|
+
formatMapGenerationConfig,
|
|
3940
|
+
formatPrefab,
|
|
3941
|
+
formatProject,
|
|
3942
|
+
formatProjectFiles,
|
|
3943
|
+
formatScene,
|
|
3944
|
+
generateMapDocument,
|
|
3945
|
+
generateMapFromConfigFile,
|
|
3946
|
+
generateMapSceneFile,
|
|
3947
|
+
generatedMapSchema2 as generatedMapSchema,
|
|
3948
|
+
generatedMapToScene,
|
|
3949
|
+
hasErrors,
|
|
3950
|
+
inspectAssetManifestDocument,
|
|
3951
|
+
inspectAssetManifestFile,
|
|
3952
|
+
inspectEntityFile,
|
|
3953
|
+
inspectPrefabDocument,
|
|
3954
|
+
inspectPrefabFile,
|
|
3955
|
+
inspectProjectFile,
|
|
3956
|
+
inspectSceneAnimations,
|
|
3957
|
+
inspectSceneDocument,
|
|
3958
|
+
inspectSceneFile,
|
|
3959
|
+
loadAssetManifestFile,
|
|
3960
|
+
loadExpandedSceneFile,
|
|
3961
|
+
loadGeneratedMapFile,
|
|
3962
|
+
loadMapGenerationConfigFile,
|
|
3963
|
+
loadPatchFile,
|
|
3964
|
+
loadPrefabFile,
|
|
3965
|
+
loadProjectAssetManifestContext,
|
|
3966
|
+
loadProjectAssetManifests,
|
|
3967
|
+
loadProjectFile,
|
|
3968
|
+
loadSceneFile,
|
|
3969
|
+
mapGenerationConfigSchema2 as mapGenerationConfigSchema,
|
|
3970
|
+
parseAssetManifestSource,
|
|
3971
|
+
parseGeneratedMapSource,
|
|
3972
|
+
parseMapGenerationConfigSource,
|
|
3973
|
+
parsePatchSource,
|
|
3974
|
+
parsePrefabSource,
|
|
3975
|
+
parseProjectSource,
|
|
3976
|
+
parseSceneSource,
|
|
3977
|
+
patchSchema2 as patchSchema,
|
|
3978
|
+
prefabSchema2 as prefabSchema,
|
|
3979
|
+
projectSchema2 as projectSchema,
|
|
3980
|
+
resolveProjectFile,
|
|
3981
|
+
sceneSchema2 as sceneSchema,
|
|
3982
|
+
validateAssetManifestDocument,
|
|
3983
|
+
validateAssetManifestFile,
|
|
3984
|
+
validateGeneratedMapDocument,
|
|
3985
|
+
validateGeneratedMapFile,
|
|
3986
|
+
validateMapGenerationConfigDocument,
|
|
3987
|
+
validateMapGenerationConfigFile,
|
|
3988
|
+
validatePatchDocument,
|
|
3989
|
+
validatePrefabDocument,
|
|
3990
|
+
validatePrefabFile,
|
|
3991
|
+
validateProjectDocument,
|
|
3992
|
+
validateProjectFile,
|
|
3993
|
+
validateSceneDocument,
|
|
3994
|
+
validateSceneFile
|
|
3995
|
+
};
|
|
3996
|
+
//# sourceMappingURL=index.js.map
|