@series-inc/rundot-kinetix 0.0.0-bootstrap.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -0
- package/authoring.d.ts +30 -0
- package/index.d.ts +221 -0
- package/native/component_runtime.cpp +341 -0
- package/native/component_runtime.hpp +112 -0
- package/native/deterministic_math.cpp +594 -0
- package/native/deterministic_math.hpp +21 -0
- package/native/kinetix-f64-native-profile.cpp +406 -0
- package/native/kinetix-f64-native-profile.hpp +5 -0
- package/native/kinetix-fixed1000-native-profile.cpp +777 -0
- package/native/kinetix-fixed1000-native-profile.hpp +58 -0
- package/native/kinetix-fixed1000-physics.cpp +887 -0
- package/native/kinetix-fixed1000-physics.hpp +28 -0
- package/native/kinetix-installed-f64-renderer.cpp +344 -0
- package/native/kinetix-installed-f64-renderer.hpp +35 -0
- package/native/kinetix-installed-f64-runtime.cpp +1085 -0
- package/native/kinetix-installed-f64-runtime.hpp +141 -0
- package/native/kinetix-installed-fixed1000-data.hpp +77 -0
- package/native/kinetix-native-main.cpp +37 -0
- package/native/kinetix-native-runtime.cpp +20 -0
- package/native/kinetix-native-runtime.hpp +25 -0
- package/native/kinetix-render-projection.cpp +20 -0
- package/native/kinetix-render-projection.hpp +14 -0
- package/package.json +65 -0
- package/runtime.d.ts +76 -0
- package/scripts/build-native.mjs +67 -0
- package/scripts/emit-product-digests.mjs +33 -0
- package/scripts/preflight.mjs +76 -0
- package/src/index.mjs +57 -0
- package/src/kinetix-authoring.mjs +69 -0
- package/src/kinetix-baker.mjs +587 -0
- package/src/kinetix-deterministic-math.mjs +1044 -0
- package/src/kinetix-fixed1000-runtime-adapter.mjs +33 -0
- package/src/kinetix-fixed1000-runtime.mjs +954 -0
- package/src/kinetix-installed-f64-reference.mjs +157 -0
- package/src/kinetix-installed-f64-render-frames.mjs +53 -0
- package/src/kinetix-installed-f64-renderer.mjs +240 -0
- package/src/kinetix-installed-f64-runtime-adapter.mjs +68 -0
- package/src/kinetix-installed-f64-runtime.mjs +607 -0
- package/src/kinetix-installed-mechanics.mjs +377 -0
- package/src/kinetix-installed-systems.mjs +181 -0
- package/src/kinetix-native-product.mjs +72 -0
- package/src/kinetix-product-contract.mjs +121 -0
- package/src/kinetix-project-compiler.mjs +1017 -0
- package/src/kinetix-render-projection.mjs +28 -0
- package/src/kinetix-runtime-adapter-utils.mjs +168 -0
- package/src/kinetix-runtime-contract.mjs +54 -0
- package/src/kinetix-session-config.mjs +170 -0
- package/src/kinetix-source-snapshot.mjs +24 -0
- package/src/kinetix-survival-runtime-adapter.mjs +305 -0
- package/src/kinetix-survival-runtime.generated.mjs +1 -0
- package/src/kinetix-world-kernel.mjs +580 -0
- package/src/kinetix-world-runtime.mjs +171 -0
- package/src/runtime.mjs +14 -0
- package/src/shared/f64-bits.mjs +14 -0
- package/src/shared/kinetix-envelope-v1.mjs +589 -0
- package/src/shared/render-records-v1.mjs +168 -0
- package/src/shared/sha256.mjs +73 -0
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import {
|
|
2
|
+
kinetixEnvelopeDigest,
|
|
3
|
+
stableKinetixEnvelopeBytes,
|
|
4
|
+
validateKinetixEnvelope,
|
|
5
|
+
} from './shared/kinetix-envelope-v1.mjs';
|
|
6
|
+
import {
|
|
7
|
+
assertKinetixProductContract,
|
|
8
|
+
assertKinetixRuntimeContract,
|
|
9
|
+
deriveKinetixRuntimeIdentity,
|
|
10
|
+
digestKinetixBytes,
|
|
11
|
+
stableKinetixContractBytes,
|
|
12
|
+
} from './kinetix-product-contract.mjs';
|
|
13
|
+
import { kinetixRuntimeContractForProfile } from './kinetix-installed-systems.mjs';
|
|
14
|
+
|
|
15
|
+
export const kinetixBakerVersion = 'rundot.kinetix-baker.v1';
|
|
16
|
+
|
|
17
|
+
export function scanKinetixPayloadTree(value) {
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
let scanned = 0;
|
|
20
|
+
function visit(entry, path) {
|
|
21
|
+
if (entry instanceof Uint8Array) {
|
|
22
|
+
scanned += 1;
|
|
23
|
+
const bytes = Buffer.from(entry);
|
|
24
|
+
if (bytes.subarray(0, 4).equals(Buffer.from([0x00, 0x61, 0x73, 0x6d]))) throw new Error(`KINETIX_PROHIBITED_WASM ${path}`);
|
|
25
|
+
if (bytes.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46]))
|
|
26
|
+
|| bytes.subarray(0, 2).equals(Buffer.from([0x4d, 0x5a]))
|
|
27
|
+
|| ['feedface', 'feedfacf', 'cefaedfe', 'cffaedfe'].includes(bytes.subarray(0, 4).toString('hex'))) {
|
|
28
|
+
throw new Error(`KINETIX_PROHIBITED_EXECUTABLE ${path}`);
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (typeof entry === 'string') {
|
|
33
|
+
if (['eval(', 'new Function', 'gl_Position', 'gl_FragColor', 'kernel void'].some((signature) => entry.includes(signature))) {
|
|
34
|
+
throw new Error(`KINETIX_PROHIBITED_SOURCE ${path}`);
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (entry === null || typeof entry !== 'object') return;
|
|
39
|
+
if (seen.has(entry)) throw new Error(`KINETIX_PAYLOAD_CYCLE ${path}`);
|
|
40
|
+
seen.add(entry);
|
|
41
|
+
if (Array.isArray(entry)) {
|
|
42
|
+
entry.forEach((item, index) => visit(item, `${path}[${index}]`));
|
|
43
|
+
seen.delete(entry);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
for (const [key, item] of Object.entries(entry)) {
|
|
47
|
+
if (/(?:script|shader|bytecode|wasm|executable)/iu.test(key)) throw new Error(`KINETIX_PROHIBITED_FIELD ${path}.${key}`);
|
|
48
|
+
if (typeof item === 'string' && /(?:binary|bytesBase64)$/iu.test(key)) {
|
|
49
|
+
const decoded = Buffer.from(item, 'base64');
|
|
50
|
+
if (decoded.length === 0 || decoded.toString('base64').replace(/=+$/u, '') !== item.replace(/=+$/u, '')) {
|
|
51
|
+
throw new Error(`KINETIX_BINARY_ENCODING_INVALID ${path}.${key}`);
|
|
52
|
+
}
|
|
53
|
+
visit(decoded, `${path}.${key}`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
visit(item, `${path}.${key}`);
|
|
57
|
+
}
|
|
58
|
+
seen.delete(entry);
|
|
59
|
+
}
|
|
60
|
+
visit(value, '$');
|
|
61
|
+
return { scanned };
|
|
62
|
+
}
|
|
63
|
+
export const kinetixPhases = Object.freeze([
|
|
64
|
+
'initialization',
|
|
65
|
+
'fixed-simulation',
|
|
66
|
+
'post-simulation',
|
|
67
|
+
'presentation-projection',
|
|
68
|
+
'presentation',
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const structuralCommandKinds = Object.freeze(['spawnPrefab', 'despawnPrefab', 'setEnabled']);
|
|
72
|
+
const authoritativePhases = new Set(['initialization', 'fixed-simulation', 'post-simulation']);
|
|
73
|
+
const presentationPhases = new Set(['presentation-projection', 'presentation']);
|
|
74
|
+
|
|
75
|
+
function isPlainObject(value) {
|
|
76
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function addDiagnostic(diagnostics, code, path) {
|
|
80
|
+
if (!diagnostics.some((entry) => entry.code === code && entry.path === path)) {
|
|
81
|
+
diagnostics.push({ code, path });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function stableBytes(value) {
|
|
86
|
+
return stableKinetixContractBytes(value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function digestBytes(bytes) {
|
|
90
|
+
return digestKinetixBytes(bytes);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function validateStringArray(value, path, diagnostics) {
|
|
94
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
|
|
95
|
+
addDiagnostic(diagnostics, 'KINETIX_SYSTEM_QUERY_INVALID', path);
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function validateSystem(profile, system, path, diagnostics) {
|
|
102
|
+
if (!isPlainObject(system) || typeof system.id !== 'string' || system.id.length === 0
|
|
103
|
+
|| !Number.isSafeInteger(system.version) || system.version < 1) {
|
|
104
|
+
addDiagnostic(diagnostics, 'KINETIX_SYSTEM_INVALID', path);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!kinetixPhases.includes(system.phase)) {
|
|
108
|
+
addDiagnostic(diagnostics, 'KINETIX_SYSTEM_PHASE_INVALID', `${path}.phase`);
|
|
109
|
+
}
|
|
110
|
+
if (!Number.isSafeInteger(system.order) || system.order < 0) {
|
|
111
|
+
addDiagnostic(diagnostics, 'KINETIX_SYSTEM_ORDER_INVALID', `${path}.order`);
|
|
112
|
+
}
|
|
113
|
+
if (!isPlainObject(system.query)) {
|
|
114
|
+
addDiagnostic(diagnostics, 'KINETIX_SYSTEM_QUERY_INVALID', `${path}.query`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const all = validateStringArray(system.query.all, `${path}.query.all`, diagnostics);
|
|
118
|
+
const none = validateStringArray(system.query.none, `${path}.query.none`, diagnostics);
|
|
119
|
+
if (all.some((type) => none.includes(type))) {
|
|
120
|
+
addDiagnostic(diagnostics, 'KINETIX_SYSTEM_QUERY_CONTRADICTION', `${path}.query`);
|
|
121
|
+
}
|
|
122
|
+
for (const [accessKind, accesses] of [['reads', system.reads], ['writes', system.writes]]) {
|
|
123
|
+
if (!Array.isArray(accesses)) {
|
|
124
|
+
addDiagnostic(diagnostics, 'KINETIX_SYSTEM_ACCESS_INVALID', `${path}.${accessKind}`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
accesses.forEach((access, index) => {
|
|
128
|
+
const accessPath = `${path}.${accessKind}[${index}]`;
|
|
129
|
+
const schema = isPlainObject(access) ? profile.componentSchemas.get(access.type) : null;
|
|
130
|
+
if (!schema || !schema.domains.includes(access.domain) || !all.includes(access.type)) {
|
|
131
|
+
addDiagnostic(diagnostics, 'KINETIX_SYSTEM_ACCESS_INVALID', accessPath);
|
|
132
|
+
}
|
|
133
|
+
if (accessKind === 'reads' && authoritativePhases.has(system.phase) && access?.domain === 'presentation') {
|
|
134
|
+
addDiagnostic(diagnostics, 'KINETIX_SYSTEM_ACCESS_INVALID', accessPath);
|
|
135
|
+
}
|
|
136
|
+
if (accessKind === 'writes' && presentationPhases.has(system.phase) && access?.domain === 'simulation') {
|
|
137
|
+
addDiagnostic(diagnostics, 'KINETIX_SYSTEM_ACCESS_INVALID', accessPath);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (!Array.isArray(system.structuralCommands)
|
|
142
|
+
|| system.structuralCommands.some((kind) => !structuralCommandKinds.includes(kind))) {
|
|
143
|
+
addDiagnostic(diagnostics, 'KINETIX_SYSTEM_COMMAND_INVALID', `${path}.structuralCommands`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function validateInstalledKinetixProfile(profileId, profile) {
|
|
148
|
+
const diagnostics = [];
|
|
149
|
+
const path = `$.installedProfiles.${profileId}`;
|
|
150
|
+
if (!isPlainObject(profile) || typeof profile.abi !== 'string' || profile.abi.length === 0) {
|
|
151
|
+
addDiagnostic(diagnostics, 'KINETIX_PROFILE_INVALID', path);
|
|
152
|
+
return { valid: false, diagnostics };
|
|
153
|
+
}
|
|
154
|
+
if (!(profile.componentSchemas instanceof Map)) {
|
|
155
|
+
addDiagnostic(diagnostics, 'KINETIX_PROFILE_INVALID', `${path}.componentSchemas`);
|
|
156
|
+
return { valid: false, diagnostics };
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
assertKinetixRuntimeContract(profile.runtimeContract);
|
|
160
|
+
} catch {
|
|
161
|
+
addDiagnostic(diagnostics, 'KINETIX_RUNTIME_CONTRACT_INVALID', `${path}.runtimeContract`);
|
|
162
|
+
}
|
|
163
|
+
for (const [type, schema] of profile.componentSchemas) {
|
|
164
|
+
const schemaPath = `${path}.componentSchemas.${type}`;
|
|
165
|
+
if (typeof type !== 'string' || !isPlainObject(schema)
|
|
166
|
+
|| !Number.isSafeInteger(schema.currentSchemaVersion) || schema.currentSchemaVersion < 1
|
|
167
|
+
|| !Array.isArray(schema.domains) || schema.domains.some((domain) => !['simulation', 'presentation', 'binding'].includes(domain))
|
|
168
|
+
|| typeof schema.validate !== 'function') {
|
|
169
|
+
addDiagnostic(diagnostics, 'KINETIX_PROFILE_SCHEMA_INVALID', schemaPath);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (typeof profile.bakeEntity !== 'function' || typeof profile.bakePrefab !== 'function') {
|
|
173
|
+
addDiagnostic(diagnostics, 'KINETIX_PROFILE_INVALID', path);
|
|
174
|
+
}
|
|
175
|
+
if (!Array.isArray(profile.systems)) {
|
|
176
|
+
addDiagnostic(diagnostics, 'KINETIX_PROFILE_INVALID', `${path}.systems`);
|
|
177
|
+
} else {
|
|
178
|
+
const ids = new Set();
|
|
179
|
+
const orders = new Set();
|
|
180
|
+
profile.systems.forEach((system, index) => {
|
|
181
|
+
const systemPath = `${path}.systems[${index}]`;
|
|
182
|
+
validateSystem(profile, system, systemPath, diagnostics);
|
|
183
|
+
if (isPlainObject(system)) {
|
|
184
|
+
if (ids.has(system.id)) {
|
|
185
|
+
addDiagnostic(diagnostics, 'KINETIX_SYSTEM_DUPLICATE', `${systemPath}.id`);
|
|
186
|
+
}
|
|
187
|
+
ids.add(system.id);
|
|
188
|
+
const orderKey = `${system.phase}:${system.order}`;
|
|
189
|
+
if (orders.has(orderKey)) {
|
|
190
|
+
addDiagnostic(diagnostics, 'KINETIX_SYSTEM_ORDER_DUPLICATE', `${systemPath}.order`);
|
|
191
|
+
}
|
|
192
|
+
orders.add(orderKey);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (profile.transformSemantics !== undefined) {
|
|
197
|
+
const transform = profile.transformSemantics;
|
|
198
|
+
if (!isPlainObject(transform) || typeof transform.localType !== 'string'
|
|
199
|
+
|| typeof transform.presentationType !== 'string' || typeof transform.composeWorld !== 'function') {
|
|
200
|
+
addDiagnostic(diagnostics, 'KINETIX_PROFILE_TRANSFORM_INVALID', `${path}.transformSemantics`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { valid: diagnostics.length === 0, diagnostics };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function validateAuthoredComponent(component, path, profile, diagnostics) {
|
|
207
|
+
const schema = profile.componentSchemas.get(component.type);
|
|
208
|
+
if (!schema) {
|
|
209
|
+
addDiagnostic(diagnostics, 'KINETIX_COMPONENT_UNKNOWN', `${path}.type`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (component.schemaVersion !== schema.currentSchemaVersion) {
|
|
213
|
+
addDiagnostic(diagnostics, 'KINETIX_SCHEMA_VERSION_UNSUPPORTED', `${path}.schemaVersion`);
|
|
214
|
+
}
|
|
215
|
+
if (!schema.domains.includes(component.domain)) {
|
|
216
|
+
addDiagnostic(diagnostics, 'KINETIX_COMPONENT_DOMAIN_INVALID', `${path}.domain`);
|
|
217
|
+
}
|
|
218
|
+
const propertyDiagnostics = schema.validate(component.properties, { path });
|
|
219
|
+
if (!Array.isArray(propertyDiagnostics)) {
|
|
220
|
+
addDiagnostic(diagnostics, 'KINETIX_PROFILE_SCHEMA_INVALID', `${path}.properties`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
for (const diagnostic of propertyDiagnostics) {
|
|
224
|
+
if (!isPlainObject(diagnostic) || typeof diagnostic.code !== 'string') {
|
|
225
|
+
addDiagnostic(diagnostics, 'KINETIX_PROFILE_SCHEMA_INVALID', `${path}.properties`);
|
|
226
|
+
} else {
|
|
227
|
+
addDiagnostic(diagnostics, diagnostic.code, diagnostic.path ?? `${path}.properties`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function validateCallbackOutput(output, path, diagnostics) {
|
|
233
|
+
if (!isPlainObject(output)) {
|
|
234
|
+
addDiagnostic(diagnostics, 'KINETIX_PROFILE_OUTPUT_INVALID', path);
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
for (const domain of ['simulation', 'presentation', 'binding']) {
|
|
238
|
+
if (!Array.isArray(output[domain]) || output[domain].some((record) => !isPlainObject(record))) {
|
|
239
|
+
addDiagnostic(diagnostics, 'KINETIX_PROFILE_OUTPUT_INVALID', `${path}.${domain}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
for (const key of Object.keys(output)) {
|
|
243
|
+
if (!['simulation', 'presentation', 'binding'].includes(key)) {
|
|
244
|
+
addDiagnostic(diagnostics, 'KINETIX_PROFILE_OUTPUT_INVALID', `${path}.${key}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return diagnostics.length === 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function componentSignature(components) {
|
|
251
|
+
return components
|
|
252
|
+
.map((component) => [component.type, component.schemaVersion, component.domain])
|
|
253
|
+
.sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function topologicalParentEdges(entities, resolveIndex) {
|
|
257
|
+
const byId = new Map(entities.map((entity, index) => [entity.id, { entity, index }]));
|
|
258
|
+
const visited = new Set();
|
|
259
|
+
const edges = [];
|
|
260
|
+
function visit(id) {
|
|
261
|
+
if (visited.has(id)) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const current = byId.get(id);
|
|
265
|
+
const parentId = current?.entity.parent?.id;
|
|
266
|
+
if (parentId !== undefined) {
|
|
267
|
+
visit(parentId);
|
|
268
|
+
edges.push({ childIndex: resolveIndex(current.index), parentIndex: resolveIndex(byId.get(parentId).index) });
|
|
269
|
+
}
|
|
270
|
+
visited.add(id);
|
|
271
|
+
}
|
|
272
|
+
entities.forEach((entity) => visit(entity.id));
|
|
273
|
+
return edges;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function addComponentRecords(payloads, components, source) {
|
|
277
|
+
components.forEach((component) => {
|
|
278
|
+
payloads[component.domain].records.push({
|
|
279
|
+
kind: 'component',
|
|
280
|
+
source,
|
|
281
|
+
component: structuredClone(component),
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function addCallbackRecords(payloads, output, source) {
|
|
287
|
+
for (const domain of ['simulation', 'presentation', 'binding']) {
|
|
288
|
+
output[domain].forEach((record) => payloads[domain].records.push({ ...structuredClone(record), source }));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function archetypeFor(components, archetypeState) {
|
|
293
|
+
const signature = componentSignature(components);
|
|
294
|
+
const signatureBytes = stableBytes(signature);
|
|
295
|
+
const signatureKey = signatureBytes.toString('utf8');
|
|
296
|
+
if (!archetypeState.bySignature.has(signatureKey)) {
|
|
297
|
+
const id = `archetype-${digestBytes(signatureBytes).slice(0, 16)}`;
|
|
298
|
+
archetypeState.bySignature.set(signatureKey, id);
|
|
299
|
+
archetypeState.entries.push({ id, signature });
|
|
300
|
+
}
|
|
301
|
+
return archetypeState.bySignature.get(signatureKey);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function invalidResult(diagnostics) {
|
|
305
|
+
return { valid: false, diagnostics, products: null, runtimeIdentity: null };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function bakeKinetixEnvelope(envelope, installedProfiles) {
|
|
309
|
+
const envelopeResult = validateKinetixEnvelope(envelope);
|
|
310
|
+
if (!envelopeResult.valid) {
|
|
311
|
+
return invalidResult(envelopeResult.diagnostics);
|
|
312
|
+
}
|
|
313
|
+
const diagnostics = [];
|
|
314
|
+
if (!(installedProfiles instanceof Map)) {
|
|
315
|
+
addDiagnostic(diagnostics, 'KINETIX_PROFILE_INVALID', '$.installedProfiles');
|
|
316
|
+
return invalidResult(diagnostics);
|
|
317
|
+
}
|
|
318
|
+
for (const declaration of envelope.profiles) {
|
|
319
|
+
const profile = installedProfiles.get(declaration.id);
|
|
320
|
+
if (!profile) {
|
|
321
|
+
addDiagnostic(diagnostics, 'KINETIX_PROFILE_NOT_INSTALLED', `$.profiles.${declaration.id}`);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const profileResult = validateInstalledKinetixProfile(declaration.id, profile);
|
|
325
|
+
diagnostics.push(...profileResult.diagnostics);
|
|
326
|
+
if (profile.abi !== declaration.abi) {
|
|
327
|
+
addDiagnostic(diagnostics, 'KINETIX_PROFILE_ABI_MISMATCH', `$.profiles.${declaration.id}.abi`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const systemIds = new Set();
|
|
332
|
+
const systemOrders = new Set();
|
|
333
|
+
for (const declaration of envelope.profiles) {
|
|
334
|
+
const profile = installedProfiles.get(declaration.id);
|
|
335
|
+
for (const [index, system] of (profile?.systems ?? []).entries()) {
|
|
336
|
+
const path = `$.installedProfiles.${declaration.id}.systems[${index}]`;
|
|
337
|
+
if (systemIds.has(system.id)) {
|
|
338
|
+
addDiagnostic(diagnostics, 'KINETIX_SYSTEM_DUPLICATE', `${path}.id`);
|
|
339
|
+
}
|
|
340
|
+
systemIds.add(system.id);
|
|
341
|
+
const orderKey = `${system.phase}:${system.order}`;
|
|
342
|
+
if (systemOrders.has(orderKey)) {
|
|
343
|
+
addDiagnostic(diagnostics, 'KINETIX_SYSTEM_ORDER_DUPLICATE', `${path}.order`);
|
|
344
|
+
}
|
|
345
|
+
systemOrders.add(orderKey);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const authoredOwners = [
|
|
350
|
+
...envelope.entities.map((entity, index) => ({ entity, path: `$.entities[${index}]`, profileId: entity.profile })),
|
|
351
|
+
...envelope.prefabs.flatMap((prefab, prefabIndex) => prefab.entities.map((entity, entityIndex) => ({
|
|
352
|
+
entity,
|
|
353
|
+
path: `$.prefabs[${prefabIndex}].entities[${entityIndex}]`,
|
|
354
|
+
profileId: prefab.profile,
|
|
355
|
+
}))),
|
|
356
|
+
];
|
|
357
|
+
for (const owner of authoredOwners) {
|
|
358
|
+
const profile = installedProfiles.get(owner.profileId);
|
|
359
|
+
if (!profile) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
owner.entity.components.forEach((component, index) => {
|
|
363
|
+
validateAuthoredComponent(component, `${owner.path}.components[${index}]`, profile, diagnostics);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
if (diagnostics.length > 0) {
|
|
367
|
+
return invalidResult(diagnostics);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const payloads = {
|
|
371
|
+
simulation: { records: [], sourceIndex: [] },
|
|
372
|
+
presentation: { records: [], sourceIndex: [] },
|
|
373
|
+
binding: { records: [], sourceIndex: [] },
|
|
374
|
+
};
|
|
375
|
+
const callbackOutputs = [];
|
|
376
|
+
envelope.entities.forEach((entity, index) => {
|
|
377
|
+
const output = installedProfiles.get(entity.profile).bakeEntity({ entity: structuredClone(entity) });
|
|
378
|
+
callbackOutputs.push({ output, path: `$.entities[${index}]`, source: { kind: 'entity', id: entity.id } });
|
|
379
|
+
});
|
|
380
|
+
envelope.prefabs.forEach((prefab, index) => {
|
|
381
|
+
const output = installedProfiles.get(prefab.profile).bakePrefab({ prefab: structuredClone(prefab) });
|
|
382
|
+
callbackOutputs.push({ output, path: `$.prefabs[${index}]`, source: { kind: 'prefab', id: prefab.id } });
|
|
383
|
+
});
|
|
384
|
+
for (const entry of callbackOutputs) {
|
|
385
|
+
validateCallbackOutput(entry.output, `${entry.path}.output`, diagnostics);
|
|
386
|
+
}
|
|
387
|
+
if (diagnostics.length > 0) {
|
|
388
|
+
return invalidResult(diagnostics);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const archetypeState = { bySignature: new Map(), entries: [] };
|
|
392
|
+
const directEntities = envelope.entities.map((entity, index) => {
|
|
393
|
+
const source = { kind: 'entity', id: entity.id };
|
|
394
|
+
addComponentRecords(payloads, entity.components, source);
|
|
395
|
+
addCallbackRecords(payloads, callbackOutputs[index].output, source);
|
|
396
|
+
return {
|
|
397
|
+
index,
|
|
398
|
+
sourceId: entity.id,
|
|
399
|
+
section: entity.section,
|
|
400
|
+
profile: entity.profile,
|
|
401
|
+
archetypeId: archetypeFor(entity.components, archetypeState),
|
|
402
|
+
enabled: entity.enabled ?? true,
|
|
403
|
+
};
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const prefabCallbackOffset = envelope.entities.length;
|
|
407
|
+
const prefabs = envelope.prefabs.map((prefab, prefabIndex) => {
|
|
408
|
+
const source = { kind: 'prefab', id: prefab.id };
|
|
409
|
+
prefab.entities.forEach((entity) => addComponentRecords(payloads, entity.components, {
|
|
410
|
+
kind: 'prefab-local-entity',
|
|
411
|
+
prefabId: prefab.id,
|
|
412
|
+
id: entity.id,
|
|
413
|
+
}));
|
|
414
|
+
addCallbackRecords(payloads, callbackOutputs[prefabCallbackOffset + prefabIndex].output, source);
|
|
415
|
+
return {
|
|
416
|
+
id: prefab.id,
|
|
417
|
+
profile: prefab.profile,
|
|
418
|
+
rootLocalEntityId: prefab.rootLocalEntityId,
|
|
419
|
+
poolBudget: prefab.poolBudget,
|
|
420
|
+
entities: prefab.entities.map((entity, index) => ({
|
|
421
|
+
localEntityId: entity.id,
|
|
422
|
+
localIndex: index,
|
|
423
|
+
archetypeId: archetypeFor(entity.components, archetypeState),
|
|
424
|
+
enabled: entity.enabled ?? true,
|
|
425
|
+
})),
|
|
426
|
+
parentEdges: topologicalParentEdges(prefab.entities, (index) => index)
|
|
427
|
+
.map((edge) => ({ childLocalIndex: edge.childIndex, parentLocalIndex: edge.parentIndex })),
|
|
428
|
+
};
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
for (const domain of ['simulation', 'presentation', 'binding']) {
|
|
432
|
+
payloads[domain].sourceIndex = payloads[domain].records.map((record, index) => ({ index, source: record.source }));
|
|
433
|
+
}
|
|
434
|
+
const budgetByDomain = {
|
|
435
|
+
simulation: envelope.budgets.maxSimulationRecords,
|
|
436
|
+
presentation: envelope.budgets.maxPresentationRecords,
|
|
437
|
+
binding: envelope.budgets.maxBindingRecords,
|
|
438
|
+
};
|
|
439
|
+
for (const domain of ['simulation', 'presentation', 'binding']) {
|
|
440
|
+
if (payloads[domain].records.length > budgetByDomain[domain]) {
|
|
441
|
+
addDiagnostic(diagnostics, 'KINETIX_OUTPUT_BUDGET_EXCEEDED', `$.budgets.max${domain[0].toUpperCase()}${domain.slice(1)}Records`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (diagnostics.length > 0) {
|
|
445
|
+
return invalidResult(diagnostics);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const systems = envelope.profiles
|
|
449
|
+
.flatMap((declaration) => installedProfiles.get(declaration.id).systems.map((system) => ({
|
|
450
|
+
...structuredClone(system),
|
|
451
|
+
profileId: declaration.id,
|
|
452
|
+
phaseIndex: kinetixPhases.indexOf(system.phase),
|
|
453
|
+
})))
|
|
454
|
+
.sort((left, right) => left.phaseIndex - right.phaseIndex || left.order - right.order);
|
|
455
|
+
const sections = [...new Set(envelope.entities.map((entity) => entity.section))]
|
|
456
|
+
.sort((left, right) => left - right)
|
|
457
|
+
.map((section) => ({
|
|
458
|
+
section,
|
|
459
|
+
entityIndices: directEntities.filter((entity) => entity.section === section).map((entity) => entity.index),
|
|
460
|
+
}));
|
|
461
|
+
const parentEdges = topologicalParentEdges(envelope.entities, (index) => index);
|
|
462
|
+
const payloadTable = ['simulation', 'presentation', 'binding'].map((name) => {
|
|
463
|
+
const bytes = stableBytes(payloads[name]);
|
|
464
|
+
return { name, digest: digestBytes(bytes), byteLength: bytes.length };
|
|
465
|
+
});
|
|
466
|
+
const runtime = structuredClone(
|
|
467
|
+
installedProfiles.get(envelope.profiles[0].id).runtimeContract,
|
|
468
|
+
);
|
|
469
|
+
const manifest = {
|
|
470
|
+
sceneId: envelope.sceneId,
|
|
471
|
+
runtime,
|
|
472
|
+
sections,
|
|
473
|
+
directEntities,
|
|
474
|
+
parentEdges,
|
|
475
|
+
prefabs,
|
|
476
|
+
archetypes: archetypeState.entries,
|
|
477
|
+
systems,
|
|
478
|
+
payloads: payloadTable,
|
|
479
|
+
budgets: {
|
|
480
|
+
maxLiveRuntimeEntities: envelope.budgets.maxLiveRuntimeEntities,
|
|
481
|
+
maxCommandsPerTick: envelope.budgets.maxCommandsPerTick,
|
|
482
|
+
prefabs: prefabs.map((prefab) => ({ prefabId: prefab.id, poolBudget: prefab.poolBudget })),
|
|
483
|
+
},
|
|
484
|
+
dependencies: {
|
|
485
|
+
bakerVersion: kinetixBakerVersion,
|
|
486
|
+
profiles: envelope.profiles.map((profile) => ({ ...profile })),
|
|
487
|
+
assets: envelope.assets.map((asset) => ({ id: asset.id, digest: asset.sha256, sourceDigest: asset.sourceSha256 })),
|
|
488
|
+
scenes: envelope.sceneDependencies.map((scene) => ({ id: scene.id, digest: scene.sha256 })),
|
|
489
|
+
},
|
|
490
|
+
sourceIndex: directEntities.map((entity) => ({ index: entity.index, sourceId: entity.sourceId })),
|
|
491
|
+
};
|
|
492
|
+
const products = {
|
|
493
|
+
sourceDigest: kinetixEnvelopeDigest(envelope),
|
|
494
|
+
manifest,
|
|
495
|
+
simulation: payloads.simulation,
|
|
496
|
+
presentation: payloads.presentation,
|
|
497
|
+
binding: payloads.binding,
|
|
498
|
+
};
|
|
499
|
+
assertKinetixProductContract(products);
|
|
500
|
+
return {
|
|
501
|
+
valid: true,
|
|
502
|
+
diagnostics: [],
|
|
503
|
+
products,
|
|
504
|
+
runtimeIdentity: deriveKinetixRuntimeIdentity(products, runtime),
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export function stableKinetixProductBytes(products) {
|
|
509
|
+
if (!isPlainObject(products)) {
|
|
510
|
+
throw new Error('KINETIX_PRODUCTS_INVALID');
|
|
511
|
+
}
|
|
512
|
+
return stableBytes(products);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function stableKinetixSourceBytes(envelope) {
|
|
516
|
+
return stableKinetixEnvelopeBytes(envelope);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function bakeKinetixProjectDefinition(project) {
|
|
520
|
+
const baseRuntime = kinetixRuntimeContractForProfile(project.installedProfile);
|
|
521
|
+
const sessionSchemaDigest = digestKinetixBytes(stableKinetixContractBytes(project.sessionConfigSchema));
|
|
522
|
+
const runtime = {
|
|
523
|
+
...baseRuntime,
|
|
524
|
+
tickRate: project.tickRate,
|
|
525
|
+
inputSchema: structuredClone(project.inputSchema),
|
|
526
|
+
};
|
|
527
|
+
const simulation = {
|
|
528
|
+
format: 'rundot.kinetix-authored-simulation.v1',
|
|
529
|
+
installedProfile: project.installedProfile,
|
|
530
|
+
mechanics: structuredClone(project.mechanics),
|
|
531
|
+
components: structuredClone(project.components),
|
|
532
|
+
systems: structuredClone(project.systems),
|
|
533
|
+
};
|
|
534
|
+
const presentation = {
|
|
535
|
+
format: 'rundot.kinetix-authored-presentation.v1',
|
|
536
|
+
bindings: structuredClone(project.presentationBindings),
|
|
537
|
+
};
|
|
538
|
+
const binding = {
|
|
539
|
+
format: 'rundot.kinetix-authored-binding.v1',
|
|
540
|
+
projectId: project.id,
|
|
541
|
+
};
|
|
542
|
+
const payloads = { simulation, presentation, binding };
|
|
543
|
+
const payloadTable = ['simulation', 'presentation', 'binding'].map((name) => {
|
|
544
|
+
const bytes = stableKinetixContractBytes(payloads[name]);
|
|
545
|
+
return { name, digest: digestKinetixBytes(bytes), byteLength: bytes.length };
|
|
546
|
+
});
|
|
547
|
+
const sourceContract = {
|
|
548
|
+
format: 'rundot.kinetix-authoring-source.v1',
|
|
549
|
+
projectId: project.id,
|
|
550
|
+
installedProfile: project.installedProfile,
|
|
551
|
+
tickRate: project.tickRate,
|
|
552
|
+
inputSchema: project.inputSchema,
|
|
553
|
+
sessionConfigSchemaId: project.sessionConfigSchema.id,
|
|
554
|
+
sessionConfigSchemaDigest: sessionSchemaDigest,
|
|
555
|
+
mechanics: project.mechanics,
|
|
556
|
+
components: project.components,
|
|
557
|
+
systems: project.systems,
|
|
558
|
+
presentationBindings: project.presentationBindings,
|
|
559
|
+
};
|
|
560
|
+
const products = {
|
|
561
|
+
sourceDigest: digestKinetixBytes(stableKinetixContractBytes(sourceContract)),
|
|
562
|
+
manifest: {
|
|
563
|
+
sceneId: project.id,
|
|
564
|
+
runtime,
|
|
565
|
+
sessionConfig: {
|
|
566
|
+
schemaId: project.sessionConfigSchema.id,
|
|
567
|
+
schemaDigest: sessionSchemaDigest,
|
|
568
|
+
maxBytes: project.sessionConfigSchema.maxBytes,
|
|
569
|
+
},
|
|
570
|
+
payloads: payloadTable,
|
|
571
|
+
},
|
|
572
|
+
...payloads,
|
|
573
|
+
};
|
|
574
|
+
assertKinetixProductContract(products);
|
|
575
|
+
const bytes = stableKinetixContractBytes(products);
|
|
576
|
+
return {
|
|
577
|
+
products,
|
|
578
|
+
runtimeIdentity: deriveKinetixRuntimeIdentity(products, runtime),
|
|
579
|
+
contentDigest: digestKinetixBytes(bytes),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export {
|
|
584
|
+
assertKinetixProductContract,
|
|
585
|
+
assertKinetixRuntimeIdentityMatchesProducts,
|
|
586
|
+
deriveKinetixRuntimeIdentity,
|
|
587
|
+
} from './kinetix-product-contract.mjs';
|