pelican-engine 0.0.0-stage → 0.3.1
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/AGENTS.md +147 -0
- package/LICENSE +22 -0
- package/README.md +129 -2
- package/distribution.json +197 -0
- package/docs/AI-WORKFLOW.md +517 -0
- package/docs/ARCHITECTURE.md +268 -0
- package/docs/CAPABILITIES.md +264 -0
- package/docs/DECISIONS.md +107 -0
- package/docs/M3-USAGE.md +192 -0
- package/docs/PELICAN-MIGRATION.md +38 -0
- package/docs/RENDER-BACKENDS.md +50 -0
- package/docs/ROADMAP.md +239 -0
- package/docs/THIRD-PARTY-NOTICES.md +151 -0
- package/docs/api/REFERENCE.md +1435 -0
- package/docs/api/index.json +1139 -0
- package/docs/npm/DISTRIBUTION.md +130 -0
- package/docs/npm/GAME-AUTHOR.md +147 -0
- package/engine.manifest.json +15 -0
- package/lib/assets/audio.js +67 -0
- package/lib/assets/errors.js +36 -0
- package/lib/assets/glb.js +277 -0
- package/lib/assets/index.js +53 -0
- package/lib/assets/jpeg.js +199 -0
- package/lib/assets/model.js +558 -0
- package/lib/assets/optimize.js +95 -0
- package/lib/assets/texture.js +187 -0
- package/lib/compiler/distribution.js +17 -0
- package/lib/compiler/extract.js +425 -0
- package/lib/compiler/graph.js +366 -0
- package/lib/compiler/index.js +574 -0
- package/lib/compiler/normalize.js +1194 -0
- package/lib/compiler/source-map.js +25 -0
- package/lib/compiler/types.js +2 -0
- package/lib/compiler/version.js +93 -0
- package/lib/experiments/simulation/index.js +2 -0
- package/lib/experiments/simulation/math.js +41 -0
- package/lib/experiments/simulation/probe.js +215 -0
- package/lib/experiments/simulation/scenes.js +192 -0
- package/lib/experiments/simulation/solver.js +346 -0
- package/lib/experiments/simulation/types.js +1 -0
- package/lib/experiments/world/adapters.js +30 -0
- package/lib/experiments/world/geometry.js +180 -0
- package/lib/experiments/world/index.js +2 -0
- package/lib/experiments/world/probe.js +288 -0
- package/lib/experiments/world/types.js +1 -0
- package/lib/experiments/world/world.js +599 -0
- package/lib/navigation/index.js +157 -0
- package/lib/physics/index.js +909 -0
- package/lib/protocol/index.js +92 -0
- package/lib/renderer/acceleration.js +609 -0
- package/lib/renderer/browser.js +2015 -0
- package/lib/renderer/bsdf.js +260 -0
- package/lib/renderer/checkpoint.js +88 -0
- package/lib/renderer/compute-executor.js +1034 -0
- package/lib/renderer/compute-texture-assets.js +49 -0
- package/lib/renderer/compute-view-contract.js +1 -0
- package/lib/renderer/denoiser.js +361 -0
- package/lib/renderer/dielectric.js +360 -0
- package/lib/renderer/embedded-image-pixels.js +368 -0
- package/lib/renderer/emissive-lights.js +67 -0
- package/lib/renderer/environment-importance.js +55 -0
- package/lib/renderer/environment.js +346 -0
- package/lib/renderer/gameplay.js +464 -0
- package/lib/renderer/geometry.js +103 -0
- package/lib/renderer/graph-expressions.js +485 -0
- package/lib/renderer/graph-tsl.js +141 -0
- package/lib/renderer/graph.js +214 -0
- package/lib/renderer/managed-texture-sampling.js +29 -0
- package/lib/renderer/material-graphs.js +297 -0
- package/lib/renderer/medium.js +348 -0
- package/lib/renderer/models.js +1053 -0
- package/lib/renderer/motion-shader.js +40 -0
- package/lib/renderer/path-shader.js +196 -0
- package/lib/renderer/pathtrace.js +324 -0
- package/lib/renderer/postprocess-contract.js +1 -0
- package/lib/renderer/postprocess.js +494 -0
- package/lib/renderer/render-buffers.js +132 -0
- package/lib/renderer/render-textures.js +155 -0
- package/lib/renderer/startup.js +50 -0
- package/lib/renderer/surface-capture.js +199 -0
- package/lib/renderer/surface-gpu.js +270 -0
- package/lib/renderer/surface-motion.js +84 -0
- package/lib/renderer/surface-ray-shader.js +91 -0
- package/lib/renderer/surface-shader.js +88 -0
- package/lib/renderer/surface-tracer.js +1720 -0
- package/lib/renderer/surface.js +1425 -0
- package/lib/renderer/temporal-filter.js +512 -0
- package/lib/renderer/texture-pixels.js +207 -0
- package/lib/renderer/traversal-nodes.js +54 -0
- package/lib/renderer/ui.js +446 -0
- package/lib/renderer/vertex-bounds.js +127 -0
- package/lib/renderer/vertex-contract.js +1 -0
- package/lib/renderer/vertex-graphs.js +197 -0
- package/lib/renderer/volume-shader.js +50 -0
- package/lib/runtime/compute-contract.js +1 -0
- package/lib/runtime/compute-service.js +674 -0
- package/lib/runtime/contracts.js +1 -0
- package/lib/runtime/error.js +37 -0
- package/lib/runtime/events.js +189 -0
- package/lib/runtime/game-services.js +772 -0
- package/lib/runtime/host.js +839 -0
- package/lib/runtime/material-parameters.js +145 -0
- package/lib/runtime/plugins.js +1 -0
- package/lib/runtime/postprocess-contract.js +1 -0
- package/lib/runtime/postprocess.js +189 -0
- package/lib/runtime/prefabs.js +13 -0
- package/lib/runtime/random.js +88 -0
- package/lib/runtime/render-buffer-contract.js +1 -0
- package/lib/runtime/render-buffers.js +243 -0
- package/lib/runtime/render-capabilities.js +17 -0
- package/lib/runtime/render-settings.js +249 -0
- package/lib/runtime/render-texture-contract.js +1 -0
- package/lib/runtime/render-textures.js +271 -0
- package/lib/runtime/resources.js +648 -0
- package/lib/runtime/scheduler.js +765 -0
- package/lib/runtime/storage.js +169 -0
- package/lib/runtime/world.js +594 -0
- package/lib/schema/index.js +24 -0
- package/lib/sdk/graph-values.js +16 -0
- package/lib/sdk/index.d.ts +800 -0
- package/lib/sdk/index.js +82 -0
- package/lib/tools/api-index.js +113 -0
- package/lib/tools/browser-session.js +89 -0
- package/lib/tools/cli.js +513 -0
- package/lib/tools/compress-dist.js +59 -0
- package/lib/tools/control-client.js +153 -0
- package/lib/tools/control-server.js +633 -0
- package/lib/tools/diagnostic-code.js +3 -0
- package/lib/tools/doctor.js +496 -0
- package/lib/tools/npm/browser-cli.js +6 -0
- package/lib/tools/npm/entry.js +40 -0
- package/lib/tools/npm/layout.js +195 -0
- package/lib/tools/setup.js +153 -0
- package/licenses/@dimforge/rapier2d-compat/LICENSE +201 -0
- package/licenses/@dimforge/rapier3d-compat/LICENSE +201 -0
- package/licenses/@gltf-transform/core/LICENSE.md +21 -0
- package/licenses/@gltf-transform/extensions/LICENSE.md +21 -0
- package/licenses/@gltf-transform/functions/LICENSE.md +21 -0
- package/licenses/@img/colour/LICENSE.md +82 -0
- package/licenses/@img/sharp-win32-x64/LICENSE +191 -0
- package/licenses/@oxc-project/types/LICENSE +22 -0
- package/licenses/@rolldown/pluginutils/LICENSE +21 -0
- package/licenses/@tweenjs/tween.js/LICENSE +23 -0
- package/licenses/@types/ndarray/LICENSE +21 -0
- package/licenses/@types/node/LICENSE +21 -0
- package/licenses/@types/stats.js/LICENSE +21 -0
- package/licenses/@types/three/@dimforge/rapier3d-compat/LICENSE +201 -0
- package/licenses/@types/three/LICENSE +21 -0
- package/licenses/@types/webxr/LICENSE +21 -0
- package/licenses/@webgpu/types/LICENSE +26 -0
- package/licenses/THIRD-PARTY-NOTICES.md +139 -0
- package/licenses/cwise-compiler/LICENSE +22 -0
- package/licenses/detect-libc/LICENSE +201 -0
- package/licenses/esbuild/LICENSE.md +21 -0
- package/licenses/fast-png/LICENSE +21 -0
- package/licenses/fdir/LICENSE +7 -0
- package/licenses/fflate/LICENSE +21 -0
- package/licenses/iobuffer/LICENSE +22 -0
- package/licenses/iota-array/LICENSE +22 -0
- package/licenses/is-buffer/LICENSE +21 -0
- package/licenses/ktx-parse/LICENSE +21 -0
- package/licenses/lightningcss/LICENSE +373 -0
- package/licenses/lightningcss-win32-x64-msvc/LICENSE +373 -0
- package/licenses/meshoptimizer/LICENSE.md +21 -0
- package/licenses/nanoid/LICENSE +20 -0
- package/licenses/ndarray/LICENSE +22 -0
- package/licenses/ndarray-lanczos/LICENSE +21 -0
- package/licenses/ndarray-ops/LICENSE +22 -0
- package/licenses/ndarray-pixels/LICENSE +21 -0
- package/licenses/ngraph.path/LICENSE +21 -0
- package/licenses/picocolors/LICENSE +15 -0
- package/licenses/picomatch/LICENSE +21 -0
- package/licenses/playwright/LICENSE +202 -0
- package/licenses/playwright/NOTICE +5 -0
- package/licenses/playwright-core/LICENSE +202 -0
- package/licenses/playwright-core/NOTICE +5 -0
- package/licenses/postcss/LICENSE +20 -0
- package/licenses/prettier/LICENSE +7 -0
- package/licenses/property-graph/LICENSE +21 -0
- package/licenses/rolldown/LICENSE +25 -0
- package/licenses/semver/LICENSE +15 -0
- package/licenses/sharp/LICENSE +191 -0
- package/licenses/source-map-js/LICENSE +28 -0
- package/licenses/three/LICENSE +21 -0
- package/licenses/tinyglobby/LICENSE +21 -0
- package/licenses/tsx/LICENSE +21 -0
- package/licenses/typescript/LICENSE.txt +55 -0
- package/licenses/undici-types/LICENSE +21 -0
- package/licenses/uniq/LICENSE +22 -0
- package/licenses/vite/LICENSE.md +2084 -0
- package/package.json +49 -4
- package/web/index.html +34 -0
|
@@ -0,0 +1,1194 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { Color } from "three";
|
|
5
|
+
import { inspectAsset, AssetInspectionError, } from "../assets/index.js";
|
|
6
|
+
import { SDK_VERSION } from "../sdk/index.js";
|
|
7
|
+
import { resolveRenderSettings } from "../runtime/render-settings.js";
|
|
8
|
+
import { compileGraphDeclaration } from "./graph.js";
|
|
9
|
+
import { isGraphType, graphValue } from "../sdk/graph-values.js";
|
|
10
|
+
import { SYSTEM_PHASES } from "./types.js";
|
|
11
|
+
export const hash = (data) => createHash("sha256").update(data).digest("hex");
|
|
12
|
+
export async function normalize(game, extractor, projectRoot) {
|
|
13
|
+
const diagnostics = [];
|
|
14
|
+
const assetCopies = [];
|
|
15
|
+
const source = (o, key) => extractor.source(o, key);
|
|
16
|
+
const error = (code, message, owner, key, suggestion = "Correct the declaration and rebuild.") => diagnostics.push({
|
|
17
|
+
code,
|
|
18
|
+
severity: "error",
|
|
19
|
+
message,
|
|
20
|
+
suggestion,
|
|
21
|
+
source: source(owner, key),
|
|
22
|
+
});
|
|
23
|
+
const obj = (value, owner, name) => {
|
|
24
|
+
if (value && typeof value === "object" && !Array.isArray(value))
|
|
25
|
+
return value;
|
|
26
|
+
error("TC_SCHEMA_TYPE", name + " must be an object.", owner, name);
|
|
27
|
+
return {};
|
|
28
|
+
};
|
|
29
|
+
const list = (value, owner, name) => {
|
|
30
|
+
if (value === undefined)
|
|
31
|
+
return [];
|
|
32
|
+
if (Array.isArray(value))
|
|
33
|
+
return value;
|
|
34
|
+
error("TC_SCHEMA_TYPE", name + " must be an array.", owner, name);
|
|
35
|
+
return [];
|
|
36
|
+
};
|
|
37
|
+
const text = (value, owner, key, fallback = "") => {
|
|
38
|
+
if (value === undefined)
|
|
39
|
+
return fallback;
|
|
40
|
+
if (typeof value === "string")
|
|
41
|
+
return value;
|
|
42
|
+
error("TC_SCHEMA_TYPE", key + " must be a string.", owner, key);
|
|
43
|
+
return fallback;
|
|
44
|
+
};
|
|
45
|
+
const num = (value, owner, key, fallback, min = -Infinity, max = Infinity) => {
|
|
46
|
+
if (value === undefined)
|
|
47
|
+
return fallback;
|
|
48
|
+
if (typeof value === "number" &&
|
|
49
|
+
Number.isFinite(value) &&
|
|
50
|
+
value >= min &&
|
|
51
|
+
value <= max)
|
|
52
|
+
return value;
|
|
53
|
+
error("TC_SCHEMA_RANGE", key + " must be finite in [" + min + ", " + max + "].", owner, key);
|
|
54
|
+
return fallback;
|
|
55
|
+
};
|
|
56
|
+
const vector = (value, owner, key, fallback) => {
|
|
57
|
+
if (value === undefined)
|
|
58
|
+
return fallback;
|
|
59
|
+
if (Array.isArray(value) &&
|
|
60
|
+
value.length === 3 &&
|
|
61
|
+
value.every((x) => typeof x === "number" && Number.isFinite(x)))
|
|
62
|
+
return value;
|
|
63
|
+
error("TC_SCHEMA_TYPE", key + " must be a finite three-component vector.", owner, key);
|
|
64
|
+
return fallback;
|
|
65
|
+
};
|
|
66
|
+
const id = (o) => {
|
|
67
|
+
const value = text(o.id, o, "id");
|
|
68
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_.-]*$/.test(value))
|
|
69
|
+
error("TC_SCHEMA_ID", "IDs must begin with a letter and contain only letters, digits, dot, dash or underscore.", o, "id");
|
|
70
|
+
return value;
|
|
71
|
+
};
|
|
72
|
+
const ref = (value, owner, key) => typeof value === "string"
|
|
73
|
+
? value
|
|
74
|
+
: text(obj(value, owner, key).id, owner, key);
|
|
75
|
+
const located = (o) => source(o) ?? source(game);
|
|
76
|
+
const defs = new Map();
|
|
77
|
+
for (const def of extractor.definitions) {
|
|
78
|
+
if (def.kind === "game")
|
|
79
|
+
continue;
|
|
80
|
+
const kind = String(def.kind);
|
|
81
|
+
const key = id(def);
|
|
82
|
+
const registry = defs.get(kind) ?? new Map();
|
|
83
|
+
if (registry.has(key))
|
|
84
|
+
error("TC_DUPLICATE_ID", "Duplicate " + kind + " ID: " + key, def, "id");
|
|
85
|
+
registry.set(key, def);
|
|
86
|
+
defs.set(kind, registry);
|
|
87
|
+
}
|
|
88
|
+
const registry = (kind) => defs.get(kind) ?? new Map();
|
|
89
|
+
const requireRef = (kind, name, owner, key) => {
|
|
90
|
+
if (!registry(kind).has(name))
|
|
91
|
+
error("TC_RESOURCE_MISSING", "Missing " + kind + " reference: " + name, owner, key, "Declare and reference a matching define" +
|
|
92
|
+
kind[0].toUpperCase() +
|
|
93
|
+
kind.slice(1) +
|
|
94
|
+
" definition.");
|
|
95
|
+
};
|
|
96
|
+
const components = [];
|
|
97
|
+
for (const [name, def] of registry("component")) {
|
|
98
|
+
const schema = obj(def.schema, def, "schema");
|
|
99
|
+
const clean = {};
|
|
100
|
+
for (const [field, raw] of Object.entries(schema)) {
|
|
101
|
+
const f = obj(raw, schema, field);
|
|
102
|
+
const t = f.type;
|
|
103
|
+
const valid = t === "number"
|
|
104
|
+
? typeof f.default === "number" && Number.isFinite(f.default)
|
|
105
|
+
: t === "string"
|
|
106
|
+
? typeof f.default === "string"
|
|
107
|
+
: t === "boolean"
|
|
108
|
+
? typeof f.default === "boolean"
|
|
109
|
+
: t === "vec3"
|
|
110
|
+
? Array.isArray(f.default) &&
|
|
111
|
+
f.default.length === 3 &&
|
|
112
|
+
f.default.every((v) => typeof v === "number" && Number.isFinite(v))
|
|
113
|
+
: false;
|
|
114
|
+
if (!valid)
|
|
115
|
+
error("TC_COMPONENT_FIELD", "Invalid component field type/default: " + field, f, "default");
|
|
116
|
+
clean[field] = { type: t, default: f.default };
|
|
117
|
+
}
|
|
118
|
+
components.push({
|
|
119
|
+
id: name,
|
|
120
|
+
schema: clean,
|
|
121
|
+
source: located(def),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const componentLookup = new Map(components.map((c) => [c.id, c]));
|
|
125
|
+
const computeBuffers = [];
|
|
126
|
+
const initialBuffers = new Map();
|
|
127
|
+
for (const raw of list(game.computeBuffers, game, "computeBuffers")) {
|
|
128
|
+
const definition = obj(raw, game, "computeBuffers");
|
|
129
|
+
const name = id(definition);
|
|
130
|
+
for (const key of Object.keys(definition))
|
|
131
|
+
if (!["id", "type", "count", "data"].includes(key))
|
|
132
|
+
error("TC_COMPUTE_BUFFER", "Unknown initial buffer field " + key, definition, key);
|
|
133
|
+
if (initialBuffers.has(name))
|
|
134
|
+
error("TC_COMPUTE_BUFFER_NAME", "Duplicate initial buffer ID: " + name, definition, "id");
|
|
135
|
+
if (!isGraphType(definition.type))
|
|
136
|
+
error("TC_GRAPH_TYPE", "Initial buffer type must be a graph value type.", definition, "type");
|
|
137
|
+
const type = isGraphType(definition.type) ? definition.type : "float";
|
|
138
|
+
const count = num(definition.count, definition, "count", 1, 1, 0xffffffff);
|
|
139
|
+
if (!Number.isSafeInteger(count))
|
|
140
|
+
error("TC_COMPUTE_COUNT", "Initial buffer count must be a positive integer.", definition, "count");
|
|
141
|
+
const buffer = { id: name, type, count };
|
|
142
|
+
if (definition.data !== undefined) {
|
|
143
|
+
const data = list(definition.data, definition, "data");
|
|
144
|
+
if (data.length !== count)
|
|
145
|
+
error("TC_COMPUTE_COUNT", "Initial data length differs from buffer count.", definition, "data");
|
|
146
|
+
buffer.data = data.map((value, index) => {
|
|
147
|
+
const checked = graphValue(type, value);
|
|
148
|
+
if (checked === null)
|
|
149
|
+
error("TC_COMPUTE_VALUE", "Invalid initial buffer value at index " + index, definition, "data");
|
|
150
|
+
return checked ?? (type === "bool" ? false : 0);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
computeBuffers.push(buffer);
|
|
154
|
+
initialBuffers.set(name, buffer);
|
|
155
|
+
}
|
|
156
|
+
const computeTextures = [];
|
|
157
|
+
const initialTextures = new Map();
|
|
158
|
+
for (const raw of list(game.computeTextures, game, "computeTextures")) {
|
|
159
|
+
const definition = obj(raw, game, "computeTextures"), name = id(definition);
|
|
160
|
+
const assetInput = Object.hasOwn(definition, "asset");
|
|
161
|
+
const fields = assetInput ? ["id", "asset"] : ["id", "width", "height", "data"];
|
|
162
|
+
for (const key of Object.keys(definition))
|
|
163
|
+
if (!fields.includes(key))
|
|
164
|
+
error("TC_COMPUTE_TEXTURE", "Unknown or incompatible initial texture field " + key, definition, key);
|
|
165
|
+
if (initialTextures.has(name) || initialBuffers.has(name))
|
|
166
|
+
error("TC_COMPUTE_TEXTURE_NAME", "Compute resource IDs must be unique across buffers and textures.", definition, "id");
|
|
167
|
+
if (assetInput) {
|
|
168
|
+
const asset = ref(definition.asset, definition, "asset");
|
|
169
|
+
requireRef("asset", asset, definition, "asset");
|
|
170
|
+
if (registry("asset").has(asset) && registry("asset").get(asset)?.type !== "texture")
|
|
171
|
+
error("TC_ASSET_TYPE", "Initial compute textures must import declared texture assets.", definition, "asset");
|
|
172
|
+
const texture = { id: name, asset };
|
|
173
|
+
computeTextures.push(texture);
|
|
174
|
+
initialTextures.set(name, texture);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const width = num(definition.width, definition, "width", 1, 1, 2 ** 24), height = num(definition.height, definition, "height", 1, 1, 2 ** 24);
|
|
178
|
+
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width * height > 2 ** 24)
|
|
179
|
+
error("TC_COMPUTE_TEXTURE_SIZE", "Texture dimensions must be positive integers with at most 2^24 texels.", definition, "width");
|
|
180
|
+
const texture = { id: name, width, height };
|
|
181
|
+
if (definition.data !== undefined) {
|
|
182
|
+
const data = list(definition.data, definition, "data");
|
|
183
|
+
if (data.length !== width * height)
|
|
184
|
+
error("TC_COMPUTE_TEXTURE_SIZE", "Texture data must match width times height.", definition, "data");
|
|
185
|
+
texture.data = data.map((value, index) => {
|
|
186
|
+
const checked = graphValue("vec4", value);
|
|
187
|
+
if (checked === null)
|
|
188
|
+
error("TC_COMPUTE_VALUE", "Expected a finite f32 Vec4 texel at index " + index, definition, "data");
|
|
189
|
+
return (checked ?? [0, 0, 0, 0]);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
computeTextures.push(texture);
|
|
193
|
+
initialTextures.set(name, texture);
|
|
194
|
+
}
|
|
195
|
+
const graph = (raw, owner, expected, domain = "material", buffers, sampledTextures, textureSlots) => {
|
|
196
|
+
const g = obj(raw, owner, "graph");
|
|
197
|
+
const result = compileGraphDeclaration(g, expected, {
|
|
198
|
+
owner,
|
|
199
|
+
domain,
|
|
200
|
+
buffers,
|
|
201
|
+
textureSlots,
|
|
202
|
+
inputs: domain === "compute" ? ["index", "pixelCoord", "outputSize", "uv"] : domain === "vertex" ? ["positionLocal", "normalLocal", "vertexIndex", "instanceIndex", "uv", "uv1"] : ["uv", "uv1", "positionWorld", "normalWorld", "viewDirectionWorld", "vertexColor"],
|
|
203
|
+
textures: sampledTextures ?? new Set(assets.filter(asset => asset.type === "texture").map(asset => asset.id)),
|
|
204
|
+
});
|
|
205
|
+
for (const issue of result.issues)
|
|
206
|
+
error(issue.code, issue.message, issue.owner, issue.field, issue.suggestion);
|
|
207
|
+
return result.graph;
|
|
208
|
+
};
|
|
209
|
+
const assets = [];
|
|
210
|
+
for (const [name, def] of registry("asset")) {
|
|
211
|
+
const sourcePath = text(def.path, def, "path");
|
|
212
|
+
const absolute = path.resolve(projectRoot, sourcePath);
|
|
213
|
+
const rel = path.relative(projectRoot, absolute);
|
|
214
|
+
if (!sourcePath || rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
215
|
+
error("TC_ASSET_PATH", "Asset path must point to a file inside the project.", def, "path");
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!["texture", "binary", "model", "audio"].includes(String(def.type))) {
|
|
219
|
+
error("TC_ASSET_TYPE", "Unsupported asset type.", def, "type");
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const content = await fs.readFile(absolute);
|
|
224
|
+
const metadata = await inspectAsset(absolute, def.type);
|
|
225
|
+
if (metadata.sha256 !== hash(content)) {
|
|
226
|
+
error("TC_ASSET_CHANGED", "Asset changed during inspection: " + sourcePath, def, "path", "Wait for the asset producer to finish writing and rebuild.");
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const output = "assets/" + name + "-" + path.basename(absolute);
|
|
230
|
+
assets.push({
|
|
231
|
+
id: name,
|
|
232
|
+
type: def.type,
|
|
233
|
+
path: output,
|
|
234
|
+
sourcePath: rel.replaceAll("\\", "/"),
|
|
235
|
+
hash: hash(content),
|
|
236
|
+
metadata,
|
|
237
|
+
sourceInfo: {
|
|
238
|
+
...(typeof def.source === "string" ? { source: def.source } : {}),
|
|
239
|
+
...(typeof def.license === "string" ? { license: def.license } : {}),
|
|
240
|
+
...(typeof def.attribution === "string"
|
|
241
|
+
? { attribution: def.attribution }
|
|
242
|
+
: {}),
|
|
243
|
+
},
|
|
244
|
+
source: located(def),
|
|
245
|
+
...(def.type === "texture" && ["srgb", "linear"].includes(String(def.colorSpace)) ? { colorSpace: def.colorSpace } : {}),
|
|
246
|
+
});
|
|
247
|
+
if (def.colorSpace !== undefined && (def.type !== "texture" || !["srgb", "linear"].includes(String(def.colorSpace))))
|
|
248
|
+
error("TC_ASSET_COLOR_SPACE", "colorSpace is srgb or linear and applies only to texture assets.", def, "colorSpace");
|
|
249
|
+
for (const field of ["source", "license", "attribution"])
|
|
250
|
+
if (def[field] !== undefined && typeof def[field] !== "string")
|
|
251
|
+
error("TC_ASSET_PROVENANCE", "Asset " + field + " must be a string.", def, field);
|
|
252
|
+
if (typeof def.license !== "string" || !def.license.trim())
|
|
253
|
+
diagnostics.push({
|
|
254
|
+
code: "TC_ASSET_LICENSE_UNDECLARED",
|
|
255
|
+
severity: "warning",
|
|
256
|
+
message: "No asset license was declared for " +
|
|
257
|
+
name +
|
|
258
|
+
"; ownership and usage rights were not inferred.",
|
|
259
|
+
suggestion: "Declare the actual license and required attribution in defineAsset metadata after checking the asset's terms.",
|
|
260
|
+
source: source(def, "license") ?? located(def),
|
|
261
|
+
});
|
|
262
|
+
assetCopies.push({
|
|
263
|
+
source: absolute,
|
|
264
|
+
output,
|
|
265
|
+
expectedHash: hash(content),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
catch (cause) {
|
|
269
|
+
if (cause instanceof AssetInspectionError) {
|
|
270
|
+
error(cause.code, cause.message.replaceAll(absolute, sourcePath), def, "path", cause.suggestion);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
error("TC_ASSET_MISSING", "Asset file not found or unreadable: " + sourcePath, def, "path", "Supply the file at the declared project-relative path.");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const renderBuffers = (def, label) => {
|
|
277
|
+
const buffers = [];
|
|
278
|
+
const bufferTypes = Object.create(null);
|
|
279
|
+
if (def.buffers !== undefined) {
|
|
280
|
+
for (const raw of list(def.buffers, def, "buffers")) {
|
|
281
|
+
const binding = obj(raw, def, "buffers"), name = text(binding.name, binding, "name");
|
|
282
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_.-]*$/.test(name) || Object.hasOwn(bufferTypes, name))
|
|
283
|
+
error("TC_RENDER_BUFFER_SLOT", label + " buffer slot names must be valid and unique.", binding, "name");
|
|
284
|
+
for (const key of Object.keys(binding))
|
|
285
|
+
if (!["name", "type", "buffer", "count"].includes(key))
|
|
286
|
+
error("TC_RENDER_BUFFER_SLOT", "Unknown " + label.toLowerCase() + " buffer slot field " + key, binding, key);
|
|
287
|
+
if (!isGraphType(binding.type))
|
|
288
|
+
error("TC_GRAPH_TYPE", label + " buffer slot type must be a graph value type.", binding, "type");
|
|
289
|
+
const type = isGraphType(binding.type) ? binding.type : "float";
|
|
290
|
+
bufferTypes[name] = type;
|
|
291
|
+
const slot = { name, type };
|
|
292
|
+
if (binding.count !== undefined) {
|
|
293
|
+
slot.count = num(binding.count, binding, "count", 1, 1, 0xffffffff);
|
|
294
|
+
if (!Number.isSafeInteger(slot.count))
|
|
295
|
+
error("TC_RENDER_BUFFER_COUNT", "Slot count must be an integer.", binding, "count");
|
|
296
|
+
}
|
|
297
|
+
if (binding.buffer !== undefined) {
|
|
298
|
+
slot.buffer = text(binding.buffer, binding, "buffer");
|
|
299
|
+
const initial = initialBuffers.get(slot.buffer);
|
|
300
|
+
if (!initial)
|
|
301
|
+
error("TC_RENDER_BUFFER_MISSING", "Initial " + label.toLowerCase() + " binding must reference a declared computeBuffers ID.", binding, "buffer");
|
|
302
|
+
else if (initial.type !== type || (slot.count !== undefined && initial.count !== slot.count))
|
|
303
|
+
error("TC_RENDER_BUFFER_TYPE", "Initial " + label.toLowerCase() + " slot type/count differs from the buffer declaration.", binding, "buffer");
|
|
304
|
+
}
|
|
305
|
+
buffers.push(slot);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return { buffers, bufferTypes };
|
|
309
|
+
};
|
|
310
|
+
const renderTextures = (def, label) => {
|
|
311
|
+
const textures = [];
|
|
312
|
+
const textureSlots = Object.create(null);
|
|
313
|
+
for (const raw of list(def.textures, def, "textures")) {
|
|
314
|
+
const binding = obj(raw, def, "textures"), name = text(binding.name, binding, "name");
|
|
315
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_.-]*$/.test(name) || Object.hasOwn(textureSlots, name))
|
|
316
|
+
error("TC_RENDER_TEXTURE_SLOT", label + " texture slot names must be valid and unique.", binding, "name");
|
|
317
|
+
if (registry("asset").has(name))
|
|
318
|
+
error("TC_RENDER_TEXTURE_SLOT", "Texture slot name conflicts with static asset ID: " + name, binding, "name");
|
|
319
|
+
for (const key of Object.keys(binding))
|
|
320
|
+
if (!["name", "filter", "wrapU", "wrapV", "texture", "width", "height"].includes(key))
|
|
321
|
+
error("TC_RENDER_TEXTURE_SLOT", "Unknown " + label.toLowerCase() + " texture slot field " + key, binding, key);
|
|
322
|
+
const filter = binding.filter === undefined ? "linear" : binding.filter, wrapU = binding.wrapU === undefined ? "clamp" : binding.wrapU, wrapV = binding.wrapV === undefined ? "clamp" : binding.wrapV;
|
|
323
|
+
if (filter !== "nearest" && filter !== "linear")
|
|
324
|
+
error("TC_RENDER_TEXTURE_SAMPLER", "Texture filter must be nearest or linear.", binding, "filter");
|
|
325
|
+
for (const [field, value] of [["wrapU", wrapU], ["wrapV", wrapV]])
|
|
326
|
+
if (!["clamp", "repeat", "mirror"].includes(String(value)))
|
|
327
|
+
error("TC_RENDER_TEXTURE_SAMPLER", "Texture addressing must be clamp, repeat or mirror.", binding, field);
|
|
328
|
+
const slot = {
|
|
329
|
+
name, filter: filter === "nearest" ? "nearest" : "linear",
|
|
330
|
+
wrapU: wrapU === "repeat" || wrapU === "mirror" ? wrapU : "clamp",
|
|
331
|
+
wrapV: wrapV === "repeat" || wrapV === "mirror" ? wrapV : "clamp",
|
|
332
|
+
};
|
|
333
|
+
textureSlots[name] = { ...slot };
|
|
334
|
+
for (const field of ["width", "height"])
|
|
335
|
+
if (binding[field] !== undefined) {
|
|
336
|
+
const value = binding[field];
|
|
337
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > 2 ** 24)
|
|
338
|
+
error("TC_RENDER_TEXTURE_SIZE", "Texture slot dimensions must be positive integers with at most 2^24 texels.", binding, field);
|
|
339
|
+
else
|
|
340
|
+
slot[field] = value;
|
|
341
|
+
}
|
|
342
|
+
if (slot.width !== undefined && slot.height !== undefined && slot.width * slot.height > 2 ** 24)
|
|
343
|
+
error("TC_RENDER_TEXTURE_SIZE", "Texture slot dimensions exceed 2^24 texels.", binding, "width");
|
|
344
|
+
if (binding.texture !== undefined) {
|
|
345
|
+
slot.texture = text(binding.texture, binding, "texture");
|
|
346
|
+
const initial = initialTextures.get(slot.texture);
|
|
347
|
+
if (!initial)
|
|
348
|
+
error("TC_RENDER_TEXTURE_MISSING", "Initial " + label.toLowerCase() + " binding must reference a declared computeTextures ID.", binding, "texture");
|
|
349
|
+
else {
|
|
350
|
+
const size = typeof initial.asset === "string" ? assets.find(asset => asset.id === initial.asset)?.metadata : initial;
|
|
351
|
+
for (const field of ["width", "height"])
|
|
352
|
+
if (slot[field] !== undefined && typeof size?.[field] === "number" && slot[field] !== size[field])
|
|
353
|
+
error("TC_RENDER_TEXTURE_SIZE", "Initial " + label.toLowerCase() + " slot dimensions differ from the texture declaration.", binding, field);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
textures.push(slot);
|
|
357
|
+
}
|
|
358
|
+
return { textures, textureSlots };
|
|
359
|
+
};
|
|
360
|
+
const materials = [];
|
|
361
|
+
for (const [name, def] of registry("material")) {
|
|
362
|
+
const material = {
|
|
363
|
+
id: name,
|
|
364
|
+
color: text(def.color, def, "color", "#ffffff"),
|
|
365
|
+
roughness: num(def.roughness, def, "roughness", 0.5, 0, 1),
|
|
366
|
+
metalness: num(def.metalness, def, "metalness", 0, 0, 1),
|
|
367
|
+
source: located(def),
|
|
368
|
+
};
|
|
369
|
+
if (def.emissive !== undefined)
|
|
370
|
+
material.emissive = text(def.emissive, def, "emissive");
|
|
371
|
+
if (def.emissiveIntensity !== undefined)
|
|
372
|
+
material.emissiveIntensity = num(def.emissiveIntensity, def, "emissiveIntensity", 1, 0);
|
|
373
|
+
if (def.opacity !== undefined)
|
|
374
|
+
material.opacity = num(def.opacity, def, "opacity", 1, 0, 1);
|
|
375
|
+
if (def.alphaTest !== undefined)
|
|
376
|
+
material.alphaTest = num(def.alphaTest, def, "alphaTest", 0, 0, 1);
|
|
377
|
+
if (def.side !== undefined) {
|
|
378
|
+
if (!["front", "back", "double"].includes(String(def.side)))
|
|
379
|
+
error("TC_MATERIAL_SIDE", "Unknown material side.", def, "side");
|
|
380
|
+
else
|
|
381
|
+
material.side = def.side;
|
|
382
|
+
}
|
|
383
|
+
if (def.transmission !== undefined)
|
|
384
|
+
material.transmission = num(def.transmission, def, "transmission", 0, 0, 1);
|
|
385
|
+
if (def.ior !== undefined)
|
|
386
|
+
material.ior = num(def.ior, def, "ior", 1.5, Number.EPSILON);
|
|
387
|
+
if (def.specularIntensity !== undefined)
|
|
388
|
+
material.specularIntensity = num(def.specularIntensity, def, "specularIntensity", 1, 0, 1);
|
|
389
|
+
if (def.specularColor !== undefined) {
|
|
390
|
+
const color = text(def.specularColor, def, "specularColor"), normalized = color.toLowerCase();
|
|
391
|
+
if (!/^#(?:[a-f0-9]{3}|[a-f0-9]{6})$/i.test(color) && !Object.hasOwn(Color.NAMES, normalized))
|
|
392
|
+
error("TC_MATERIAL_SPECULAR_COLOR", "Specular color must be an sRGB hex or named color.", def, "specularColor");
|
|
393
|
+
material.specularColor = color;
|
|
394
|
+
}
|
|
395
|
+
if (def.attenuationColor !== undefined)
|
|
396
|
+
material.attenuationColor = text(def.attenuationColor, def, "attenuationColor");
|
|
397
|
+
if (def.attenuationDistance !== undefined)
|
|
398
|
+
material.attenuationDistance = num(def.attenuationDistance, def, "attenuationDistance", 1, Number.EPSILON);
|
|
399
|
+
if ((material.transmission ?? 0) > 0 && material.metalness !== 0)
|
|
400
|
+
error("TC_MATERIAL_TRANSMISSION", "A closed dielectric volume cannot also be metallic.", def, "metalness", "Use metalness: 0 for dielectric transmission.");
|
|
401
|
+
if ((material.transmission ?? 0) > 0 && ((material.specularIntensity ?? 1) !== 1 || (material.specularColor !== undefined && !["#fff", "#ffffff", "white"].includes(material.specularColor.toLowerCase()))))
|
|
402
|
+
error("TC_MATERIAL_SPECULAR_TRANSMISSION", "Non-default specular color/intensity on transmissive media is not supported.", def, "transmission", "Use default white specular with transmission, or set transmission to zero.");
|
|
403
|
+
if (!(material.transmission > 0) &&
|
|
404
|
+
(def.attenuationColor !== undefined ||
|
|
405
|
+
def.attenuationDistance !== undefined))
|
|
406
|
+
error("TC_MATERIAL_TRANSMISSION", "Absorption requires positive transmission.", def, "transmission");
|
|
407
|
+
if (def.texture !== undefined) {
|
|
408
|
+
material.texture = ref(def.texture, def, "texture");
|
|
409
|
+
requireRef("asset", material.texture, def, "texture");
|
|
410
|
+
if (registry("asset").get(material.texture)?.type !== "texture")
|
|
411
|
+
error("TC_ASSET_TYPE", "Material textures must reference texture assets.", def, "texture");
|
|
412
|
+
}
|
|
413
|
+
const slots = renderBuffers(def, "Material");
|
|
414
|
+
const sampled = renderTextures(def, "Material");
|
|
415
|
+
if (def.textures !== undefined)
|
|
416
|
+
material.textures = sampled.textures;
|
|
417
|
+
if (def.buffers !== undefined)
|
|
418
|
+
material.buffers = slots.buffers;
|
|
419
|
+
if (def.graph !== undefined)
|
|
420
|
+
material.graph = graph(def.graph, def, "vec3", "material", slots.bufferTypes, undefined, sampled.textureSlots);
|
|
421
|
+
if (def.graphs !== undefined) {
|
|
422
|
+
const channels = obj(def.graphs, def, "graphs");
|
|
423
|
+
const types = { baseColor: "vec3", emissive: "vec3", roughness: "float", metalness: "float", opacity: "float", normalWorld: "vec3" };
|
|
424
|
+
const graphs = {};
|
|
425
|
+
for (const [channel, value] of Object.entries(channels)) {
|
|
426
|
+
if (!Object.hasOwn(types, channel)) {
|
|
427
|
+
error("TC_GRAPH_CHANNEL", "Unknown material graph channel " + channel, channels, channel);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
const key = channel;
|
|
431
|
+
graphs[key] = graph(value, channels, types[key], "material", slots.bufferTypes, undefined, sampled.textureSlots);
|
|
432
|
+
if ((key === "opacity" || key === "emissive") && graphs[key]?.requiredInputs?.includes("viewDirectionWorld"))
|
|
433
|
+
error("TC_GRAPH_CONTEXT", "Coverage and emitted radiance are direction independent in this material model.", channels, channel);
|
|
434
|
+
}
|
|
435
|
+
if (material.graph && graphs.baseColor)
|
|
436
|
+
error("TC_GRAPH_CHANNEL", "Use either legacy graph or graphs.baseColor, not both.", channels, "baseColor");
|
|
437
|
+
if ((material.transmission ?? 0) > 0 && graphs.metalness)
|
|
438
|
+
error("TC_MATERIAL_TRANSMISSION", "A closed dielectric graph cannot change metalness in this material model.", channels, "metalness");
|
|
439
|
+
material.graphs = graphs;
|
|
440
|
+
}
|
|
441
|
+
if (def.vertex !== undefined) {
|
|
442
|
+
const channels = obj(def.vertex, def, "vertex");
|
|
443
|
+
const vertex = {};
|
|
444
|
+
if (!Object.keys(channels).length)
|
|
445
|
+
error("TC_VERTEX_GRAPH", "Declare at least a position or normal vertex graph.", def, "vertex");
|
|
446
|
+
for (const [channel, value] of Object.entries(channels)) {
|
|
447
|
+
if (channel !== "position" && channel !== "normal") {
|
|
448
|
+
error("TC_VERTEX_GRAPH", "Unknown vertex graph channel " + channel, channels, channel);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
vertex[channel] = graph(value, channels, "vec3", "vertex", slots.bufferTypes, undefined, sampled.textureSlots);
|
|
452
|
+
}
|
|
453
|
+
material.vertex = vertex;
|
|
454
|
+
}
|
|
455
|
+
const parameters = new Map();
|
|
456
|
+
for (const g of [material.graph, ...Object.values(material.graphs ?? {}), ...Object.values(material.vertex ?? {})])
|
|
457
|
+
for (const parameter of g?.parameters ?? []) {
|
|
458
|
+
const old = parameters.get(parameter.name);
|
|
459
|
+
if (old && JSON.stringify(old) !== JSON.stringify(parameter))
|
|
460
|
+
error("TC_GRAPH_PARAMETER", "Material graphs disagree about parameter " + parameter.name, def, "graphs");
|
|
461
|
+
else
|
|
462
|
+
parameters.set(parameter.name, parameter);
|
|
463
|
+
}
|
|
464
|
+
if (parameters.size)
|
|
465
|
+
material.parameters = [...parameters.values()];
|
|
466
|
+
materials.push(material);
|
|
467
|
+
}
|
|
468
|
+
const computes = [];
|
|
469
|
+
const postProcesses = [];
|
|
470
|
+
const declaredPost = list(game.postProcesses, game, "postProcesses").map(raw => obj(raw, game, "postProcesses"));
|
|
471
|
+
const postIDs = new Set();
|
|
472
|
+
for (const definition of declaredPost) {
|
|
473
|
+
if (definition.kind !== "postprocess")
|
|
474
|
+
error("TC_POSTPROCESS_KIND", "Use definePostProcess declarations in postProcesses.", definition, "kind");
|
|
475
|
+
const name = id(definition);
|
|
476
|
+
if (postIDs.has(name) || name === "scene" || name.startsWith("scene."))
|
|
477
|
+
error("TC_POSTPROCESS_ID", "Postprocess IDs must be unique and cannot use the reserved scene namespace.", definition, "id");
|
|
478
|
+
postIDs.add(name);
|
|
479
|
+
}
|
|
480
|
+
const frameChannels = new Set(["scene.color", "scene.normal", "scene.depth"]);
|
|
481
|
+
for (const definition of declaredPost) {
|
|
482
|
+
const name = id(definition);
|
|
483
|
+
for (const key of Object.keys(definition))
|
|
484
|
+
if (!["id", "kind", "graph", "enabled", "buffers", "textures"].includes(key))
|
|
485
|
+
error("TC_POSTPROCESS_FIELD", "Unknown postprocess field " + key, definition, key);
|
|
486
|
+
const slots = renderBuffers(definition, "Postprocess");
|
|
487
|
+
const sampled = renderTextures(definition, "Postprocess");
|
|
488
|
+
const result = compileGraphDeclaration(definition.graph, "vec4", {
|
|
489
|
+
owner: definition, domain: "postprocess", inputs: ["uv", "screenSize", "texelSize"],
|
|
490
|
+
textures: new Set(assets.filter(asset => asset.type === "texture").map(asset => asset.id)),
|
|
491
|
+
textureSlots: sampled.textureSlots,
|
|
492
|
+
buffers: slots.bufferTypes, frameChannels, historyChannels: postIDs,
|
|
493
|
+
});
|
|
494
|
+
for (const issue of result.issues)
|
|
495
|
+
error(issue.code, issue.message, issue.owner, issue.field, issue.suggestion);
|
|
496
|
+
if (definition.enabled !== undefined && typeof definition.enabled !== "boolean")
|
|
497
|
+
error("TC_POSTPROCESS_SETTINGS", "enabled must be boolean.", definition, "enabled");
|
|
498
|
+
postProcesses.push({ id: name, enabled: definition.enabled !== false, graph: result.graph, ...(definition.buffers !== undefined ? { buffers: slots.buffers } : {}), ...(definition.textures !== undefined ? { textures: sampled.textures } : {}), source: located(definition) });
|
|
499
|
+
frameChannels.add(name);
|
|
500
|
+
}
|
|
501
|
+
for (const [name, def] of registry("compute")) {
|
|
502
|
+
const buffers = [];
|
|
503
|
+
const textures = [], sampledTextures = new Set();
|
|
504
|
+
for (const raw of list(def.textures, def, "textures")) {
|
|
505
|
+
const binding = obj(raw, def, "textures"), bindingName = text(binding.name, binding, "name");
|
|
506
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_.-]*$/.test(bindingName) || sampledTextures.has(bindingName))
|
|
507
|
+
error("TC_COMPUTE_TEXTURE_BINDING", "Texture slot names must be valid and unique.", binding, "name");
|
|
508
|
+
for (const key of Object.keys(binding))
|
|
509
|
+
if (!["name", "filter", "wrapU", "wrapV"].includes(key))
|
|
510
|
+
error("TC_COMPUTE_TEXTURE_BINDING", "Unknown texture slot field " + key, binding, key);
|
|
511
|
+
const filter = binding.filter ?? "linear", wrapU = binding.wrapU ?? "clamp", wrapV = binding.wrapV ?? "clamp";
|
|
512
|
+
if (filter !== "nearest" && filter !== "linear")
|
|
513
|
+
error("TC_COMPUTE_TEXTURE_SAMPLER", "Texture filter must be nearest or linear.", binding, "filter");
|
|
514
|
+
for (const [field, value] of [["wrapU", wrapU], ["wrapV", wrapV]])
|
|
515
|
+
if (!["clamp", "repeat", "mirror"].includes(String(value)))
|
|
516
|
+
error("TC_COMPUTE_TEXTURE_SAMPLER", "Texture addressing must be clamp, repeat or mirror.", binding, field);
|
|
517
|
+
textures.push({ name: bindingName, filter: filter === "nearest" ? "nearest" : "linear", wrapU: wrapU === "repeat" || wrapU === "mirror" ? wrapU : "clamp", wrapV: wrapV === "repeat" || wrapV === "mirror" ? wrapV : "clamp" });
|
|
518
|
+
sampledTextures.add(bindingName);
|
|
519
|
+
}
|
|
520
|
+
const bufferTypes = Object.create(null);
|
|
521
|
+
if (def.buffers !== undefined) {
|
|
522
|
+
for (const raw of list(def.buffers, def, "buffers")) {
|
|
523
|
+
const binding = obj(raw, def, "buffers");
|
|
524
|
+
const bindingName = text(binding.name, binding, "name");
|
|
525
|
+
for (const key of Object.keys(binding))
|
|
526
|
+
if (!["name", "type"].includes(key))
|
|
527
|
+
error("TC_COMPUTE_BINDING", "Unknown compute buffer declaration field " + key, binding, key);
|
|
528
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_.-]*$/.test(bindingName) || Object.hasOwn(bufferTypes, bindingName))
|
|
529
|
+
error("TC_COMPUTE_BINDING", "Compute buffer names must be valid and unique.", binding, "name");
|
|
530
|
+
if (!isGraphType(binding.type))
|
|
531
|
+
error("TC_GRAPH_TYPE", "Compute buffer type must be a declared graph value type.", binding, "type");
|
|
532
|
+
const type = isGraphType(binding.type) ? binding.type : "float";
|
|
533
|
+
bufferTypes[bindingName] = type;
|
|
534
|
+
buffers.push({ name: bindingName, type });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
const outputType = isGraphType(def.outputType) ? def.outputType : "float";
|
|
538
|
+
if (!isGraphType(def.outputType))
|
|
539
|
+
error("TC_GRAPH_TYPE", "Compute outputType must be a declared graph value type.", def, "outputType");
|
|
540
|
+
const count = num(def.count, def, "count", 1, 1);
|
|
541
|
+
const workgroupSize = num(def.workgroupSize, def, "workgroupSize", 64, 1, 256);
|
|
542
|
+
if (!Number.isInteger(count) || !Number.isInteger(workgroupSize))
|
|
543
|
+
error("TC_SCHEMA_RANGE", "Compute count and workgroupSize must be integers.", def, "count");
|
|
544
|
+
computes.push({
|
|
545
|
+
id: name,
|
|
546
|
+
count,
|
|
547
|
+
workgroupSize,
|
|
548
|
+
outputType,
|
|
549
|
+
graph: graph(def.graph, def, outputType, "compute", bufferTypes, sampledTextures),
|
|
550
|
+
...(buffers.length ? { buffers } : {}),
|
|
551
|
+
...(textures.length ? { textures } : {}),
|
|
552
|
+
source: located(def),
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
const resolvedPrefabs = new Map();
|
|
556
|
+
const activePrefabs = new Set();
|
|
557
|
+
const transform = (raw, owner, base) => {
|
|
558
|
+
const t = raw === undefined ? {} : obj(raw, owner, "transform");
|
|
559
|
+
return {
|
|
560
|
+
position: vector(t.position, t, "position", base?.position ?? [0, 0, 0]),
|
|
561
|
+
rotation: vector(t.rotation, t, "rotation", base?.rotation ?? [0, 0, 0]),
|
|
562
|
+
scale: vector(t.scale, t, "scale", base?.scale ?? [1, 1, 1]),
|
|
563
|
+
};
|
|
564
|
+
};
|
|
565
|
+
const entity = (def, base) => {
|
|
566
|
+
const result = {
|
|
567
|
+
...(base ? structuredClone(base) : {}),
|
|
568
|
+
id: id(def),
|
|
569
|
+
transform: transform(def.transform, def, base?.transform),
|
|
570
|
+
components: structuredClone(base?.components ?? {}),
|
|
571
|
+
};
|
|
572
|
+
delete result.prefab;
|
|
573
|
+
delete result.parent;
|
|
574
|
+
if (def.parent !== undefined)
|
|
575
|
+
result.parent = text(def.parent, def, "parent");
|
|
576
|
+
if (def.prefab !== undefined)
|
|
577
|
+
result.prefab = ref(def.prefab, def, "prefab");
|
|
578
|
+
if (def.mesh !== undefined) {
|
|
579
|
+
const m = obj(def.mesh, def, "mesh");
|
|
580
|
+
const geometry = obj(m.geometry, m, "geometry");
|
|
581
|
+
const material = ref(m.material, m, "material");
|
|
582
|
+
requireRef("material", material, m, "material");
|
|
583
|
+
if (!["box", "sphere", "plane"].includes(String(geometry.kind)))
|
|
584
|
+
error("TC_GEOMETRY_KIND", "M0 geometry must be box, sphere, or plane.", geometry, "kind");
|
|
585
|
+
if (geometry.kind === "box") {
|
|
586
|
+
const size = vector(geometry.size, geometry, "size", [1, 1, 1]);
|
|
587
|
+
if (size.some((x) => x <= 0))
|
|
588
|
+
error("TC_SCHEMA_RANGE", "Box sizes must be positive.", geometry, "size");
|
|
589
|
+
}
|
|
590
|
+
if (geometry.kind === "sphere") {
|
|
591
|
+
num(geometry.radius, geometry, "radius", 1, Number.EPSILON);
|
|
592
|
+
num(geometry.segments, geometry, "segments", 32, 3, 256);
|
|
593
|
+
}
|
|
594
|
+
if (geometry.kind === "plane" &&
|
|
595
|
+
geometry.size !== undefined &&
|
|
596
|
+
(!Array.isArray(geometry.size) ||
|
|
597
|
+
geometry.size.length !== 2 ||
|
|
598
|
+
geometry.size.some((v) => typeof v !== "number" || !Number.isFinite(v) || v <= 0)))
|
|
599
|
+
error("TC_SCHEMA_RANGE", "Plane size requires two positive numbers.", geometry, "size");
|
|
600
|
+
result.mesh = {
|
|
601
|
+
geometry: geometry,
|
|
602
|
+
material,
|
|
603
|
+
castShadow: m.castShadow !== false,
|
|
604
|
+
receiveShadow: m.receiveShadow !== false,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
if (def.light !== undefined) {
|
|
608
|
+
const l = obj(def.light, def, "light");
|
|
609
|
+
if (!["directional", "point"].includes(String(l.kind)))
|
|
610
|
+
error("TC_LIGHT_KIND", "M0 light must be directional or point.", l, "kind");
|
|
611
|
+
result.light = {
|
|
612
|
+
kind: l.kind,
|
|
613
|
+
color: text(l.color, l, "color", "#ffffff"),
|
|
614
|
+
intensity: num(l.intensity, l, "intensity", 1, 0),
|
|
615
|
+
castShadow: l.castShadow !== false,
|
|
616
|
+
...(l.target !== undefined
|
|
617
|
+
? { target: vector(l.target, l, "target", [0, 0, 0]) }
|
|
618
|
+
: {}),
|
|
619
|
+
};
|
|
620
|
+
if (l.shadow !== undefined) {
|
|
621
|
+
const shadow = obj(l.shadow, l, "shadow");
|
|
622
|
+
const mapSize = num(shadow.mapSize, shadow, "mapSize", 2048, 128, 8192);
|
|
623
|
+
if (!Number.isInteger(mapSize) || !Number.isInteger(Math.log2(mapSize)))
|
|
624
|
+
error("TC_LIGHT_SHADOW", "Shadow mapSize must be a power of two.", shadow, "mapSize");
|
|
625
|
+
const near = num(shadow.near, shadow, "near", 0.1, Number.EPSILON);
|
|
626
|
+
result.light.shadow = {
|
|
627
|
+
mapSize,
|
|
628
|
+
extent: num(shadow.extent, shadow, "extent", 20, Number.EPSILON),
|
|
629
|
+
bias: num(shadow.bias, shadow, "bias", -0.0001),
|
|
630
|
+
normalBias: num(shadow.normalBias, shadow, "normalBias", 0.04, 0),
|
|
631
|
+
near,
|
|
632
|
+
far: num(shadow.far, shadow, "far", 100, near + Number.EPSILON),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (def.model !== undefined) {
|
|
637
|
+
const model = obj(def.model, def, "model");
|
|
638
|
+
const asset = ref(model.asset, model, "asset");
|
|
639
|
+
requireRef("asset", asset, model, "asset");
|
|
640
|
+
if (registry("asset").get(asset)?.type !== "model")
|
|
641
|
+
error("TC_ASSET_TYPE", "Entity models must reference model assets.", model, "asset");
|
|
642
|
+
for (const field of ["castShadow", "receiveShadow"])
|
|
643
|
+
if (model[field] !== undefined && typeof model[field] !== "boolean")
|
|
644
|
+
error("TC_MODEL_VALUE", field + " must be a boolean.", model, field);
|
|
645
|
+
if (model.material !== undefined && model.materialSlots !== undefined)
|
|
646
|
+
error("TC_MODEL_MATERIAL_CONFLICT", "Use either material or materialSlots, not both.", model, "materialSlots");
|
|
647
|
+
let materialSlots;
|
|
648
|
+
if (model.materialSlots !== undefined) {
|
|
649
|
+
const slots = obj(model.materialSlots, model, "materialSlots");
|
|
650
|
+
const entries = Object.entries(slots).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
651
|
+
if (!entries.length)
|
|
652
|
+
error("TC_MODEL_MATERIAL_SLOTS", "materialSlots must not be empty.", model, "materialSlots");
|
|
653
|
+
materialSlots = Object.fromEntries(entries.map(([slot, value]) => {
|
|
654
|
+
if (!slot.trim())
|
|
655
|
+
error("TC_MODEL_MATERIAL_SLOTS", "Material slot names must not be empty.", model, "materialSlots");
|
|
656
|
+
const name = ref(value, slots, slot);
|
|
657
|
+
requireRef("material", name, slots, slot);
|
|
658
|
+
return [slot, name];
|
|
659
|
+
}));
|
|
660
|
+
}
|
|
661
|
+
result.model = {
|
|
662
|
+
castShadow: model.castShadow !== false,
|
|
663
|
+
receiveShadow: model.receiveShadow !== false,
|
|
664
|
+
asset,
|
|
665
|
+
scale: num(model.scale, model, "scale", 1, Number.EPSILON),
|
|
666
|
+
...(model.material !== undefined ? { material: (() => { const name = ref(model.material, model, "material"); requireRef("material", name, model, "material"); return name; })() } : {}),
|
|
667
|
+
...(materialSlots !== undefined ? { materialSlots } : {}),
|
|
668
|
+
...(model.offset !== undefined
|
|
669
|
+
? { offset: vector(model.offset, model, "offset", [0, 0, 0]) }
|
|
670
|
+
: {}),
|
|
671
|
+
...(model.rotation !== undefined
|
|
672
|
+
? { rotation: vector(model.rotation, model, "rotation", [0, 0, 0]) }
|
|
673
|
+
: {}),
|
|
674
|
+
};
|
|
675
|
+
if (model.animation !== undefined) {
|
|
676
|
+
const animation = obj(model.animation, model, "animation");
|
|
677
|
+
if (animation.loop !== undefined && typeof animation.loop !== "boolean")
|
|
678
|
+
error("TC_MODEL_ANIMATION", "Animation loop must be a boolean.", animation, "loop");
|
|
679
|
+
const clips = assets.find((entry) => entry.id === asset)?.metadata
|
|
680
|
+
?.animations;
|
|
681
|
+
if (clips && !clips.some((clip) => clip.name === animation.clip))
|
|
682
|
+
error("TC_MODEL_CLIP", "Model " +
|
|
683
|
+
asset +
|
|
684
|
+
" has no animation clip " +
|
|
685
|
+
String(animation.clip) +
|
|
686
|
+
". Available: " +
|
|
687
|
+
clips.map((clip) => clip.name).join(", "), animation, "clip", "Choose a clip from the asset metadata or re-export the animation.");
|
|
688
|
+
result.model.animation = {
|
|
689
|
+
clip: text(animation.clip, animation, "clip"),
|
|
690
|
+
speed: num(animation.speed, animation, "speed", 1, Number.EPSILON),
|
|
691
|
+
loop: animation.loop === undefined ? true : animation.loop === true,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (def.rigidBody !== undefined) {
|
|
696
|
+
const body = obj(def.rigidBody, def, "rigidBody");
|
|
697
|
+
const collider = obj(body.collider, body, "collider");
|
|
698
|
+
const shape = collider.shape;
|
|
699
|
+
if (!["box", "sphere", "capsule"].includes(String(shape)))
|
|
700
|
+
error("TC_PHYSICS_COLLIDER", "Collider shape must be box, sphere, or capsule.", collider, "shape");
|
|
701
|
+
const resolved = {
|
|
702
|
+
type: (body.type ?? "fixed"),
|
|
703
|
+
collider: {
|
|
704
|
+
shape: (shape ?? "box"),
|
|
705
|
+
...(shape === "box"
|
|
706
|
+
? { size: vector(collider.size, collider, "size", [1, 1, 1]) }
|
|
707
|
+
: {}),
|
|
708
|
+
...(shape === "sphere"
|
|
709
|
+
? {
|
|
710
|
+
radius: num(collider.radius, collider, "radius", 0.5, Number.EPSILON),
|
|
711
|
+
}
|
|
712
|
+
: {}),
|
|
713
|
+
...(shape === "capsule"
|
|
714
|
+
? {
|
|
715
|
+
radius: num(collider.radius, collider, "radius", 0.5, Number.EPSILON),
|
|
716
|
+
halfHeight: num(collider.halfHeight, collider, "halfHeight", 0.5, Number.EPSILON),
|
|
717
|
+
}
|
|
718
|
+
: {}),
|
|
719
|
+
...(collider.offset !== undefined
|
|
720
|
+
? { offset: vector(collider.offset, collider, "offset", [0, 0, 0]) }
|
|
721
|
+
: {}),
|
|
722
|
+
},
|
|
723
|
+
mass: num(body.mass, body, "mass", 1, Number.EPSILON),
|
|
724
|
+
friction: num(body.friction, body, "friction", 0.5, 0, 1),
|
|
725
|
+
restitution: num(body.restitution, body, "restitution", 0, 0, 1),
|
|
726
|
+
trigger: body.trigger === true,
|
|
727
|
+
};
|
|
728
|
+
if (!["fixed", "dynamic", "kinematic"].includes(String(body.type ?? "fixed")))
|
|
729
|
+
error("TC_PHYSICS_BODY", "Rigid body type must be fixed, dynamic, or kinematic.", body, "type");
|
|
730
|
+
if (body.trigger !== undefined && typeof body.trigger !== "boolean")
|
|
731
|
+
error("TC_PHYSICS_BODY", "Rigid body trigger must be a boolean.", body, "trigger");
|
|
732
|
+
if (resolved.collider.shape === "box" &&
|
|
733
|
+
resolved.collider.size?.some((x) => x <= 0))
|
|
734
|
+
error("TC_PHYSICS_COLLIDER", "Box collider dimensions must be positive.", collider, "size");
|
|
735
|
+
result.rigidBody = resolved;
|
|
736
|
+
}
|
|
737
|
+
if (def.character !== undefined) {
|
|
738
|
+
const character = obj(def.character, def, "character");
|
|
739
|
+
result.character = {
|
|
740
|
+
radius: num(character.radius, character, "radius", 0.35, Number.EPSILON),
|
|
741
|
+
height: num(character.height, character, "height", 1.8, Number.EPSILON),
|
|
742
|
+
speed: num(character.speed, character, "speed", 4, 0),
|
|
743
|
+
jumpSpeed: num(character.jumpSpeed, character, "jumpSpeed", 5, 0),
|
|
744
|
+
gravity: num(character.gravity, character, "gravity", 9.81, 0),
|
|
745
|
+
};
|
|
746
|
+
if (result.character.height < result.character.radius * 2)
|
|
747
|
+
error("TC_CHARACTER_CAPSULE", "Character height must be at least its capsule diameter (2 * radius).", character, "height");
|
|
748
|
+
}
|
|
749
|
+
for (const raw of list(def.components, def, "components")) {
|
|
750
|
+
const c = obj(raw, def, "components");
|
|
751
|
+
const name = ref(c.component, c, "component");
|
|
752
|
+
requireRef("component", name, c, "component");
|
|
753
|
+
const schema = componentLookup.get(name)?.schema;
|
|
754
|
+
const supplied = c.value === undefined ? {} : obj(c.value, c, "value");
|
|
755
|
+
const data = { ...result.components[name] };
|
|
756
|
+
if (schema) {
|
|
757
|
+
for (const [field, fieldDef] of Object.entries(schema)) {
|
|
758
|
+
const value = Object.hasOwn(supplied, field)
|
|
759
|
+
? supplied[field]
|
|
760
|
+
: Object.hasOwn(data, field)
|
|
761
|
+
? data[field]
|
|
762
|
+
: structuredClone(fieldDef.default);
|
|
763
|
+
const valid = fieldDef.type === "vec3"
|
|
764
|
+
? Array.isArray(value) &&
|
|
765
|
+
value.length === 3 &&
|
|
766
|
+
value.every((x) => typeof x === "number" && Number.isFinite(x))
|
|
767
|
+
: typeof value === fieldDef.type &&
|
|
768
|
+
(fieldDef.type !== "number" || Number.isFinite(value));
|
|
769
|
+
if (!valid)
|
|
770
|
+
error("TC_COMPONENT_VALUE", "Invalid value for " + name + "." + field, supplied, field);
|
|
771
|
+
data[field] = value;
|
|
772
|
+
}
|
|
773
|
+
for (const field of Object.keys(supplied))
|
|
774
|
+
if (!(field in schema))
|
|
775
|
+
error("TC_COMPONENT_VALUE", "Unknown component field: " + name + "." + field, supplied, field);
|
|
776
|
+
}
|
|
777
|
+
result.components[name] = data;
|
|
778
|
+
}
|
|
779
|
+
return result;
|
|
780
|
+
};
|
|
781
|
+
const prefab = (name, owner) => {
|
|
782
|
+
if (resolvedPrefabs.has(name))
|
|
783
|
+
return resolvedPrefabs.get(name);
|
|
784
|
+
const def = registry("prefab").get(name);
|
|
785
|
+
if (!def) {
|
|
786
|
+
requireRef("prefab", name, owner, "prefab");
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (activePrefabs.has(name)) {
|
|
790
|
+
error("TC_PREFAB_CYCLE", "Prefab inheritance cycle: " + name, def, "extends");
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
activePrefabs.add(name);
|
|
794
|
+
const base = def.extends !== undefined
|
|
795
|
+
? prefab(ref(def.extends, def, "extends"), def)
|
|
796
|
+
: undefined;
|
|
797
|
+
const result = entity(def, base);
|
|
798
|
+
activePrefabs.delete(name);
|
|
799
|
+
resolvedPrefabs.set(name, result);
|
|
800
|
+
return result;
|
|
801
|
+
};
|
|
802
|
+
for (const [name, def] of registry("prefab"))
|
|
803
|
+
prefab(name, def);
|
|
804
|
+
const resolveScene = (rawScene, owner, key) => {
|
|
805
|
+
const scene = obj(rawScene, owner, key);
|
|
806
|
+
if (scene.kind !== "scene")
|
|
807
|
+
error("TC_SCENE_KIND", "Game scenes must reference defineScene.", owner, key);
|
|
808
|
+
const entities = [];
|
|
809
|
+
const entityLookup = new Map();
|
|
810
|
+
for (const raw of list(scene.entities, scene, "entities")) {
|
|
811
|
+
const def = obj(raw, scene, "entities");
|
|
812
|
+
const base = def.prefab !== undefined
|
|
813
|
+
? prefab(ref(def.prefab, def, "prefab"), def)
|
|
814
|
+
: undefined;
|
|
815
|
+
const result = entity(def, base);
|
|
816
|
+
if (entityLookup.has(result.id))
|
|
817
|
+
error("TC_DUPLICATE_ID", "Duplicate entity ID: " + result.id, def, "id");
|
|
818
|
+
entityLookup.set(result.id, result);
|
|
819
|
+
entities.push(result);
|
|
820
|
+
}
|
|
821
|
+
const visited = new Set();
|
|
822
|
+
const activeParents = new Set();
|
|
823
|
+
const checkParent = (e) => {
|
|
824
|
+
if (visited.has(e.id))
|
|
825
|
+
return;
|
|
826
|
+
if (activeParents.has(e.id)) {
|
|
827
|
+
error("TC_SCENE_CYCLE", "Entity parent cycle: " + e.id, scene, "entities");
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
activeParents.add(e.id);
|
|
831
|
+
if (e.parent) {
|
|
832
|
+
const parent = entityLookup.get(e.parent);
|
|
833
|
+
if (!parent)
|
|
834
|
+
error("TC_RESOURCE_MISSING", "Missing parent entity: " + e.parent, scene, "entities");
|
|
835
|
+
else
|
|
836
|
+
checkParent(parent);
|
|
837
|
+
}
|
|
838
|
+
activeParents.delete(e.id);
|
|
839
|
+
visited.add(e.id);
|
|
840
|
+
};
|
|
841
|
+
for (const e of entities)
|
|
842
|
+
checkParent(e);
|
|
843
|
+
const camera = scene.camera === undefined ? {} : obj(scene.camera, scene, "camera");
|
|
844
|
+
const environment = scene.environment === undefined
|
|
845
|
+
? {}
|
|
846
|
+
: obj(scene.environment, scene, "environment");
|
|
847
|
+
if (environment.lighting !== undefined &&
|
|
848
|
+
!["none", "studio"].includes(String(environment.lighting)))
|
|
849
|
+
error("TC_ENVIRONMENT_LIGHTING", "Supported environment lighting presets are none and studio.", environment, "lighting");
|
|
850
|
+
const near = num(camera.near, camera, "near", 0.1, Number.EPSILON);
|
|
851
|
+
const far = num(camera.far, camera, "far", 1000, near + Number.EPSILON);
|
|
852
|
+
let follow;
|
|
853
|
+
if (scene.follow !== undefined) {
|
|
854
|
+
const f = obj(scene.follow, scene, "follow");
|
|
855
|
+
const target = text(f.entity, f, "entity");
|
|
856
|
+
if (!entityLookup.has(target))
|
|
857
|
+
error("TC_CAMERA_FOLLOW_ENTITY", "Camera follow target must exist in the same scene: " + target, f, "entity");
|
|
858
|
+
follow = {
|
|
859
|
+
entity: target,
|
|
860
|
+
distance: num(f.distance, f, "distance", 5, Number.EPSILON),
|
|
861
|
+
height: num(f.height, f, "height", 2, 0),
|
|
862
|
+
lookHeight: num(f.lookHeight, f, "lookHeight", 1, 0),
|
|
863
|
+
yaw: num(f.yaw, f, "yaw", 0),
|
|
864
|
+
smoothing: num(f.smoothing, f, "smoothing", 0.1, 0, 1),
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
return {
|
|
868
|
+
id: id(scene),
|
|
869
|
+
camera: {
|
|
870
|
+
position: vector(camera.position, camera, "position", [5, 4, 7]),
|
|
871
|
+
target: vector(camera.target, camera, "target", [0, 0, 0]),
|
|
872
|
+
fov: num(camera.fov, camera, "fov", 50, 1, 179),
|
|
873
|
+
near,
|
|
874
|
+
far,
|
|
875
|
+
},
|
|
876
|
+
environment: {
|
|
877
|
+
...(environment.lighting !== undefined
|
|
878
|
+
? { lighting: environment.lighting }
|
|
879
|
+
: {}),
|
|
880
|
+
background: text(environment.background, environment, "background", "#151c2b"),
|
|
881
|
+
ambientIntensity: num(environment.ambientIntensity, environment, "ambientIntensity", 0.4, 0),
|
|
882
|
+
},
|
|
883
|
+
...(follow ? { follow } : {}),
|
|
884
|
+
gravity: vector(scene.gravity, scene, "gravity", [0, -9.81, 0]),
|
|
885
|
+
entities,
|
|
886
|
+
};
|
|
887
|
+
};
|
|
888
|
+
const primaryScene = resolveScene(game.scene, game, "scene");
|
|
889
|
+
const additionalScenes = list(game.scenes, game, "scenes").map((value) => resolveScene(value, game, "scenes"));
|
|
890
|
+
const allScenes = [primaryScene, ...additionalScenes];
|
|
891
|
+
const sceneIds = new Set();
|
|
892
|
+
for (const scene of allScenes) {
|
|
893
|
+
if (sceneIds.has(scene.id))
|
|
894
|
+
error("TC_DUPLICATE_ID", "Duplicate scene ID: " + scene.id, game, "scenes");
|
|
895
|
+
sceneIds.add(scene.id);
|
|
896
|
+
}
|
|
897
|
+
const systems = [];
|
|
898
|
+
const systemOwners = [];
|
|
899
|
+
const systemIds = new Set();
|
|
900
|
+
for (const raw of list(game.systems, game, "systems")) {
|
|
901
|
+
const s = obj(raw, game, "systems");
|
|
902
|
+
systemOwners.push(s);
|
|
903
|
+
const name = id(s);
|
|
904
|
+
for (const key of Object.keys(s))
|
|
905
|
+
if (![
|
|
906
|
+
"kind",
|
|
907
|
+
"id",
|
|
908
|
+
"phase",
|
|
909
|
+
"before",
|
|
910
|
+
"after",
|
|
911
|
+
"reads",
|
|
912
|
+
"writes",
|
|
913
|
+
"structural",
|
|
914
|
+
"run",
|
|
915
|
+
].includes(key))
|
|
916
|
+
error("TC_SYSTEM_METADATA", "Unknown system property: " + key, s, key);
|
|
917
|
+
if (s.kind !== "system" ||
|
|
918
|
+
!s.run ||
|
|
919
|
+
typeof s.run !== "object" ||
|
|
920
|
+
!s.run.__runtime)
|
|
921
|
+
error("TC_SYSTEM_RUN", "Systems must use defineSystem with a run function.", s, "run");
|
|
922
|
+
if (s.phase !== undefined &&
|
|
923
|
+
!SYSTEM_PHASES.includes(s.phase))
|
|
924
|
+
error("TC_SYSTEM_PHASE", "Unknown system phase; expected " + SYSTEM_PHASES.join(", ") + ".", s, "phase");
|
|
925
|
+
if (systemIds.has(name))
|
|
926
|
+
error("TC_DUPLICATE_ID", "Duplicate system ID: " + name, s, "id");
|
|
927
|
+
systemIds.add(name);
|
|
928
|
+
const metadata = {
|
|
929
|
+
id: name,
|
|
930
|
+
phase: (s.phase ?? "update"),
|
|
931
|
+
source: located(s),
|
|
932
|
+
};
|
|
933
|
+
for (const key of ["before", "after", "reads", "writes"]) {
|
|
934
|
+
if (s[key] === undefined)
|
|
935
|
+
continue;
|
|
936
|
+
const values = list(s[key], s, key);
|
|
937
|
+
const names = [];
|
|
938
|
+
for (const value of values) {
|
|
939
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
940
|
+
error("TC_SYSTEM_METADATA", key + " must contain non-empty IDs.", s, key);
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
if (names.includes(value))
|
|
944
|
+
error("TC_SYSTEM_METADATA", "Duplicate " + key + " ID: " + value, s, key);
|
|
945
|
+
if ((key === "reads" || key === "writes") &&
|
|
946
|
+
value !== "Transform" &&
|
|
947
|
+
!registry("component").has(value))
|
|
948
|
+
error("TC_SYSTEM_ACCESS", "Unknown component access: " + value, s, key);
|
|
949
|
+
names.push(value);
|
|
950
|
+
}
|
|
951
|
+
metadata[key] = names;
|
|
952
|
+
}
|
|
953
|
+
if (s.structural !== undefined) {
|
|
954
|
+
if (typeof s.structural !== "boolean")
|
|
955
|
+
error("TC_SYSTEM_METADATA", "structural must be a boolean.", s, "structural");
|
|
956
|
+
else
|
|
957
|
+
metadata.structural = s.structural;
|
|
958
|
+
}
|
|
959
|
+
systems.push(metadata);
|
|
960
|
+
}
|
|
961
|
+
// Keep declaration order in IR; dependencies describe the runtime scheduler's order.
|
|
962
|
+
const bySystemId = new Map(systems.map((s, i) => [s.id, { system: s, owner: systemOwners[i] }]));
|
|
963
|
+
const edges = new Map(systems.map((s) => [s.id, new Set()]));
|
|
964
|
+
for (let i = 0; i < systems.length; i++) {
|
|
965
|
+
const s = systems[i], owner = systemOwners[i];
|
|
966
|
+
for (const key of ["before", "after"]) {
|
|
967
|
+
for (const target of s[key] ?? []) {
|
|
968
|
+
const other = bySystemId.get(target)?.system;
|
|
969
|
+
if (!other) {
|
|
970
|
+
error("TC_SYSTEM_DEPENDENCY", "Unknown system dependency: " + target, owner, key);
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
const from = key === "before" ? s : other;
|
|
974
|
+
const to = key === "before" ? other : s;
|
|
975
|
+
if (SYSTEM_PHASES.indexOf(from.phase) > SYSTEM_PHASES.indexOf(to.phase))
|
|
976
|
+
error("TC_SYSTEM_PHASE_ORDER", "Dependency reverses phase order: " + from.id + " -> " + to.id, owner, key);
|
|
977
|
+
edges.get(from.id).add(to.id);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
const done = new Set(), visiting = new Set();
|
|
982
|
+
const visitSystem = (name) => {
|
|
983
|
+
if (visiting.has(name)) {
|
|
984
|
+
const owner = bySystemId.get(name).owner;
|
|
985
|
+
error("TC_SYSTEM_CYCLE", "System dependency cycle at " + name, owner, owner.after !== undefined
|
|
986
|
+
? "after"
|
|
987
|
+
: owner.before !== undefined
|
|
988
|
+
? "before"
|
|
989
|
+
: "id");
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
if (done.has(name))
|
|
993
|
+
return;
|
|
994
|
+
visiting.add(name);
|
|
995
|
+
for (const next of edges.get(name) ?? [])
|
|
996
|
+
visitSystem(next);
|
|
997
|
+
visiting.delete(name);
|
|
998
|
+
done.add(name);
|
|
999
|
+
};
|
|
1000
|
+
for (const s of systems)
|
|
1001
|
+
visitSystem(s.id);
|
|
1002
|
+
const input = {};
|
|
1003
|
+
if (game.input !== undefined) {
|
|
1004
|
+
const bindings = obj(game.input, game, "input");
|
|
1005
|
+
for (const [action, raw] of Object.entries(bindings)) {
|
|
1006
|
+
if (!action.trim() ||
|
|
1007
|
+
action.length > 64 ||
|
|
1008
|
+
/[\x00-\x1f\x7f]/.test(action))
|
|
1009
|
+
error("TC_INPUT_ACTION", "Input action names must be 1-64 printable characters.", bindings, action);
|
|
1010
|
+
const values = list(raw, bindings, action);
|
|
1011
|
+
if (!values.length || values.length > 16)
|
|
1012
|
+
error("TC_INPUT_BINDINGS", "Each action must have between 1 and 16 key bindings.", bindings, action);
|
|
1013
|
+
const keys = [];
|
|
1014
|
+
for (const key of values) {
|
|
1015
|
+
if (typeof key !== "string" ||
|
|
1016
|
+
!key.length ||
|
|
1017
|
+
key.length > 64 ||
|
|
1018
|
+
/[\x00-\x1f\x7f]/.test(key)) {
|
|
1019
|
+
error("TC_INPUT_BINDINGS", "Key names must be 1-64 printable characters.", bindings, action);
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
if (keys.includes(key))
|
|
1023
|
+
error("TC_INPUT_BINDINGS", "Duplicate key binding: " + key, bindings, action);
|
|
1024
|
+
keys.push(key);
|
|
1025
|
+
}
|
|
1026
|
+
input[action] = keys;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
const ui = [];
|
|
1030
|
+
const uiIds = new Set();
|
|
1031
|
+
for (const raw of list(game.ui, game, "ui")) {
|
|
1032
|
+
const element = obj(raw, game, "ui");
|
|
1033
|
+
const name = id(element);
|
|
1034
|
+
if (uiIds.has(name))
|
|
1035
|
+
error("TC_DUPLICATE_ID", "Duplicate UI element ID: " + name, element, "id");
|
|
1036
|
+
uiIds.add(name);
|
|
1037
|
+
const kind = element.kind;
|
|
1038
|
+
if (!["text", "bar", "button", "input"].includes(String(kind)))
|
|
1039
|
+
error("TC_UI_KIND", "UI kind must be text, bar, button or input.", element, "kind");
|
|
1040
|
+
const color = text(element.color, element, "color", "#ffffff");
|
|
1041
|
+
const namedColors = new Set([
|
|
1042
|
+
"black",
|
|
1043
|
+
"white",
|
|
1044
|
+
"red",
|
|
1045
|
+
"green",
|
|
1046
|
+
"blue",
|
|
1047
|
+
"yellow",
|
|
1048
|
+
"orange",
|
|
1049
|
+
"purple",
|
|
1050
|
+
"pink",
|
|
1051
|
+
"gray",
|
|
1052
|
+
"grey",
|
|
1053
|
+
"cyan",
|
|
1054
|
+
"magenta",
|
|
1055
|
+
"lime",
|
|
1056
|
+
"navy",
|
|
1057
|
+
"teal",
|
|
1058
|
+
"silver",
|
|
1059
|
+
"maroon",
|
|
1060
|
+
"olive",
|
|
1061
|
+
"aqua",
|
|
1062
|
+
"fuchsia",
|
|
1063
|
+
"rebeccapurple",
|
|
1064
|
+
"transparent",
|
|
1065
|
+
]);
|
|
1066
|
+
if (!/^(#(?:[\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})|(?:rgb|rgba|hsl|hsla)\([\d.% ,+-]+\))$/i.test(color) &&
|
|
1067
|
+
!namedColors.has(color.toLowerCase()))
|
|
1068
|
+
error("TC_UI_COLOR", "UI color must be a CSS hex color or named color.", element, "color");
|
|
1069
|
+
const anchor = element.anchor ?? "top-left";
|
|
1070
|
+
if (![
|
|
1071
|
+
"top-left",
|
|
1072
|
+
"top-right",
|
|
1073
|
+
"bottom-left",
|
|
1074
|
+
"bottom-right",
|
|
1075
|
+
"center",
|
|
1076
|
+
].includes(String(anchor)))
|
|
1077
|
+
error("TC_UI_ANCHOR", "Unknown UI anchor.", element, "anchor");
|
|
1078
|
+
if (element.visible !== undefined && typeof element.visible !== "boolean")
|
|
1079
|
+
error("TC_UI_VALUE", "UI visible must be a boolean.", element, "visible");
|
|
1080
|
+
ui.push({
|
|
1081
|
+
id: name,
|
|
1082
|
+
kind: (kind ?? "text"),
|
|
1083
|
+
text: text(element.text, element, "text", ""),
|
|
1084
|
+
value: num(element.value, element, "value", 0, 0, 1),
|
|
1085
|
+
visible: element.visible !== false,
|
|
1086
|
+
anchor: anchor,
|
|
1087
|
+
x: num(element.x, element, "x", 0),
|
|
1088
|
+
y: num(element.y, element, "y", 0),
|
|
1089
|
+
width: num(element.width, element, "width", 160, Number.EPSILON),
|
|
1090
|
+
color,
|
|
1091
|
+
...(element.world !== undefined
|
|
1092
|
+
? {
|
|
1093
|
+
world: (() => {
|
|
1094
|
+
const world = obj(element.world, element, "world"), entity = text(world.entity, world, "entity");
|
|
1095
|
+
if (!allScenes.some((scene) => scene.entities.some((item) => item.id === entity)))
|
|
1096
|
+
error("TC_UI_WORLD_ENTITY", "World UI entity must be declared in at least one scene.", world, "entity");
|
|
1097
|
+
if (world.occlude !== undefined &&
|
|
1098
|
+
typeof world.occlude !== "boolean")
|
|
1099
|
+
error("TC_UI_WORLD_VALUE", "occlude must be a boolean.", world, "occlude");
|
|
1100
|
+
return {
|
|
1101
|
+
entity,
|
|
1102
|
+
offset: vector(world.offset, world, "offset", [0, 0, 0]),
|
|
1103
|
+
occlude: world.occlude !== false,
|
|
1104
|
+
};
|
|
1105
|
+
})(),
|
|
1106
|
+
}
|
|
1107
|
+
: {}),
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
const audioBuses = [];
|
|
1111
|
+
const busIds = new Set();
|
|
1112
|
+
for (const raw of list(game.audioBuses, game, "audioBuses")) {
|
|
1113
|
+
const bus = obj(raw, game, "audioBuses");
|
|
1114
|
+
const name = id(bus);
|
|
1115
|
+
if (busIds.has(name))
|
|
1116
|
+
error("TC_DUPLICATE_ID", "Duplicate audio bus ID: " + name, bus, "id");
|
|
1117
|
+
busIds.add(name);
|
|
1118
|
+
audioBuses.push({ id: name, volume: num(bus.volume, bus, "volume", 1, 0, 1) });
|
|
1119
|
+
}
|
|
1120
|
+
const sounds = [];
|
|
1121
|
+
const soundIds = new Set();
|
|
1122
|
+
for (const raw of list(game.sounds, game, "sounds")) {
|
|
1123
|
+
const sound = obj(raw, game, "sounds");
|
|
1124
|
+
const name = id(sound);
|
|
1125
|
+
if (soundIds.has(name))
|
|
1126
|
+
error("TC_DUPLICATE_ID", "Duplicate sound ID: " + name, sound, "id");
|
|
1127
|
+
soundIds.add(name);
|
|
1128
|
+
const asset = ref(sound.asset, sound, "asset");
|
|
1129
|
+
requireRef("asset", asset, sound, "asset");
|
|
1130
|
+
if (registry("asset").get(asset)?.type !== "audio")
|
|
1131
|
+
error("TC_ASSET_TYPE", "Sound definitions must reference audio assets.", sound, "asset");
|
|
1132
|
+
if (sound.loop !== undefined && typeof sound.loop !== "boolean")
|
|
1133
|
+
error("TC_SOUND_VALUE", "Sound loop must be a boolean.", sound, "loop");
|
|
1134
|
+
if (sound.bus !== undefined && (typeof sound.bus !== "string" || !busIds.has(sound.bus)))
|
|
1135
|
+
error("TC_AUDIO_BUS", "Sound bus must reference a declared audio bus.", sound, "bus");
|
|
1136
|
+
sounds.push({
|
|
1137
|
+
id: name,
|
|
1138
|
+
asset,
|
|
1139
|
+
volume: num(sound.volume, sound, "volume", 1, 0, 1),
|
|
1140
|
+
loop: sound.loop === true,
|
|
1141
|
+
...(sound.bus !== undefined ? { bus: sound.bus } : {}),
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
const ir = {
|
|
1145
|
+
format: "pelican.scene",
|
|
1146
|
+
version: 1,
|
|
1147
|
+
engineVersion: SDK_VERSION,
|
|
1148
|
+
gameId: id(game),
|
|
1149
|
+
...(game.render !== undefined
|
|
1150
|
+
? {
|
|
1151
|
+
render: (() => {
|
|
1152
|
+
try {
|
|
1153
|
+
return resolveRenderSettings(game.render);
|
|
1154
|
+
}
|
|
1155
|
+
catch (cause) {
|
|
1156
|
+
error("TC_RENDER_SETTINGS", String(cause), game, "render");
|
|
1157
|
+
return resolveRenderSettings({});
|
|
1158
|
+
}
|
|
1159
|
+
})(),
|
|
1160
|
+
}
|
|
1161
|
+
: {}),
|
|
1162
|
+
scene: primaryScene,
|
|
1163
|
+
...(additionalScenes.length ? { scenes: additionalScenes } : {}),
|
|
1164
|
+
...(game.input !== undefined ? { input } : {}),
|
|
1165
|
+
...(game.ui !== undefined ? { ui } : {}),
|
|
1166
|
+
...(game.sounds !== undefined ? { sounds } : {}),
|
|
1167
|
+
...(game.audioBuses !== undefined ? { audioBuses } : {}),
|
|
1168
|
+
components,
|
|
1169
|
+
prefabs: [...resolvedPrefabs.values()],
|
|
1170
|
+
materials,
|
|
1171
|
+
computes,
|
|
1172
|
+
...(computeBuffers.length ? { computeBuffers } : {}),
|
|
1173
|
+
...(computeTextures.length ? { computeTextures } : {}),
|
|
1174
|
+
...(postProcesses.length ? { postProcesses } : {}),
|
|
1175
|
+
systems,
|
|
1176
|
+
assets,
|
|
1177
|
+
assetNotices: assets.map((asset) => ({
|
|
1178
|
+
id: asset.id,
|
|
1179
|
+
path: asset.path,
|
|
1180
|
+
license: asset.sourceInfo?.license ?? null,
|
|
1181
|
+
...(asset.sourceInfo?.attribution
|
|
1182
|
+
? { attribution: asset.sourceInfo.attribution }
|
|
1183
|
+
: {}),
|
|
1184
|
+
...(asset.sourceInfo?.source ? { source: asset.sourceInfo.source } : {}),
|
|
1185
|
+
})),
|
|
1186
|
+
};
|
|
1187
|
+
return {
|
|
1188
|
+
ir: diagnostics.some((diagnostic) => diagnostic.severity === "error")
|
|
1189
|
+
? undefined
|
|
1190
|
+
: ir,
|
|
1191
|
+
diagnostics,
|
|
1192
|
+
assetCopies,
|
|
1193
|
+
};
|
|
1194
|
+
}
|