@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.
Files changed (58) hide show
  1. package/README.md +77 -0
  2. package/authoring.d.ts +30 -0
  3. package/index.d.ts +221 -0
  4. package/native/component_runtime.cpp +341 -0
  5. package/native/component_runtime.hpp +112 -0
  6. package/native/deterministic_math.cpp +594 -0
  7. package/native/deterministic_math.hpp +21 -0
  8. package/native/kinetix-f64-native-profile.cpp +406 -0
  9. package/native/kinetix-f64-native-profile.hpp +5 -0
  10. package/native/kinetix-fixed1000-native-profile.cpp +777 -0
  11. package/native/kinetix-fixed1000-native-profile.hpp +58 -0
  12. package/native/kinetix-fixed1000-physics.cpp +887 -0
  13. package/native/kinetix-fixed1000-physics.hpp +28 -0
  14. package/native/kinetix-installed-f64-renderer.cpp +344 -0
  15. package/native/kinetix-installed-f64-renderer.hpp +35 -0
  16. package/native/kinetix-installed-f64-runtime.cpp +1085 -0
  17. package/native/kinetix-installed-f64-runtime.hpp +141 -0
  18. package/native/kinetix-installed-fixed1000-data.hpp +77 -0
  19. package/native/kinetix-native-main.cpp +37 -0
  20. package/native/kinetix-native-runtime.cpp +20 -0
  21. package/native/kinetix-native-runtime.hpp +25 -0
  22. package/native/kinetix-render-projection.cpp +20 -0
  23. package/native/kinetix-render-projection.hpp +14 -0
  24. package/package.json +65 -0
  25. package/runtime.d.ts +76 -0
  26. package/scripts/build-native.mjs +67 -0
  27. package/scripts/emit-product-digests.mjs +33 -0
  28. package/scripts/preflight.mjs +76 -0
  29. package/src/index.mjs +57 -0
  30. package/src/kinetix-authoring.mjs +69 -0
  31. package/src/kinetix-baker.mjs +587 -0
  32. package/src/kinetix-deterministic-math.mjs +1044 -0
  33. package/src/kinetix-fixed1000-runtime-adapter.mjs +33 -0
  34. package/src/kinetix-fixed1000-runtime.mjs +954 -0
  35. package/src/kinetix-installed-f64-reference.mjs +157 -0
  36. package/src/kinetix-installed-f64-render-frames.mjs +53 -0
  37. package/src/kinetix-installed-f64-renderer.mjs +240 -0
  38. package/src/kinetix-installed-f64-runtime-adapter.mjs +68 -0
  39. package/src/kinetix-installed-f64-runtime.mjs +607 -0
  40. package/src/kinetix-installed-mechanics.mjs +377 -0
  41. package/src/kinetix-installed-systems.mjs +181 -0
  42. package/src/kinetix-native-product.mjs +72 -0
  43. package/src/kinetix-product-contract.mjs +121 -0
  44. package/src/kinetix-project-compiler.mjs +1017 -0
  45. package/src/kinetix-render-projection.mjs +28 -0
  46. package/src/kinetix-runtime-adapter-utils.mjs +168 -0
  47. package/src/kinetix-runtime-contract.mjs +54 -0
  48. package/src/kinetix-session-config.mjs +170 -0
  49. package/src/kinetix-source-snapshot.mjs +24 -0
  50. package/src/kinetix-survival-runtime-adapter.mjs +305 -0
  51. package/src/kinetix-survival-runtime.generated.mjs +1 -0
  52. package/src/kinetix-world-kernel.mjs +580 -0
  53. package/src/kinetix-world-runtime.mjs +171 -0
  54. package/src/runtime.mjs +14 -0
  55. package/src/shared/f64-bits.mjs +14 -0
  56. package/src/shared/kinetix-envelope-v1.mjs +589 -0
  57. package/src/shared/render-records-v1.mjs +168 -0
  58. package/src/shared/sha256.mjs +73 -0
@@ -0,0 +1,580 @@
1
+ import { sha256Hex } from './shared/sha256.mjs';
2
+
3
+ const phases = Object.freeze([
4
+ 'initialization',
5
+ 'fixed-simulation',
6
+ 'post-simulation',
7
+ 'presentation-projection',
8
+ 'presentation',
9
+ ]);
10
+
11
+ function fail(code) {
12
+ throw new Error(code);
13
+ }
14
+
15
+ function isPlainObject(value) {
16
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
17
+ }
18
+
19
+ function canonicalize(value) {
20
+ if (Array.isArray(value)) {
21
+ return value.map(canonicalize);
22
+ }
23
+ if (isPlainObject(value)) {
24
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]));
25
+ }
26
+ return value;
27
+ }
28
+
29
+ function stableBytes(value) {
30
+ return new TextEncoder().encode(JSON.stringify(canonicalize(value)));
31
+ }
32
+
33
+ function digest(value) {
34
+ return sha256Hex(stableBytes(value));
35
+ }
36
+
37
+ function handleFor(worldId, index, generation) {
38
+ return { worldId, index, generation };
39
+ }
40
+
41
+ function compareSortKey(left, right) {
42
+ if (typeof left === 'number' && typeof right === 'number') {
43
+ return left - right;
44
+ }
45
+ return String(left).localeCompare(String(right));
46
+ }
47
+
48
+ function hydrateComponentSources(products) {
49
+ const direct = new Map();
50
+ const prefabs = new Map();
51
+ for (const payloadName of ['simulation', 'presentation', 'binding']) {
52
+ for (const record of products[payloadName].records) {
53
+ if (record.kind !== 'component') {
54
+ continue;
55
+ }
56
+ if (record.source.kind === 'entity') {
57
+ if (!direct.has(record.source.id)) {
58
+ direct.set(record.source.id, []);
59
+ }
60
+ direct.get(record.source.id).push(structuredClone(record.component));
61
+ } else if (record.source.kind === 'prefab-local-entity') {
62
+ const key = `${record.source.prefabId}:${record.source.id}`;
63
+ if (!prefabs.has(key)) {
64
+ prefabs.set(key, []);
65
+ }
66
+ prefabs.get(key).push(structuredClone(record.component));
67
+ }
68
+ }
69
+ }
70
+ return { direct, prefabs };
71
+ }
72
+
73
+ function createInitialState(worldId, products, componentSources) {
74
+ const slots = products.manifest.directEntities.map((entity) => ({
75
+ generation: 1,
76
+ active: true,
77
+ enabled: entity.enabled,
78
+ sourceId: entity.sourceId,
79
+ profile: entity.profile,
80
+ archetypeId: entity.archetypeId,
81
+ components: structuredClone(componentSources.direct.get(entity.sourceId) ?? []),
82
+ closureId: null,
83
+ localEntityId: null,
84
+ identity: { kind: 'direct', sourceId: entity.sourceId },
85
+ }));
86
+ return {
87
+ worldId,
88
+ tick: 0,
89
+ lastInputFrame: null,
90
+ slots,
91
+ freeIndices: [],
92
+ closures: new Map(),
93
+ nextSpawnId: 1,
94
+ };
95
+ }
96
+
97
+ function activeCount(state) {
98
+ return state.slots.reduce((count, slot) => count + (slot?.active ? 1 : 0), 0);
99
+ }
100
+
101
+ function validateHandle(state, handle) {
102
+ if (!isPlainObject(handle) || !Number.isSafeInteger(handle.index) || !Number.isSafeInteger(handle.generation)) {
103
+ fail('KINETIX_INVALID_HANDLE');
104
+ }
105
+ if (handle.worldId !== state.worldId) {
106
+ fail('KINETIX_WRONG_WORLD');
107
+ }
108
+ const slot = state.slots[handle.index];
109
+ if (!slot) {
110
+ fail('KINETIX_INVALID_HANDLE');
111
+ }
112
+ if (!slot.active || slot.generation !== handle.generation) {
113
+ fail('KINETIX_STALE_HANDLE');
114
+ }
115
+ return slot;
116
+ }
117
+
118
+ function allocateIndex(state) {
119
+ if (state.freeIndices.length > 0) {
120
+ return state.freeIndices.shift();
121
+ }
122
+ const index = state.slots.length;
123
+ state.slots.push({ generation: 1, active: false });
124
+ return index;
125
+ }
126
+
127
+ function authoritativeSnapshot(state) {
128
+ return {
129
+ worldId: state.worldId,
130
+ tick: state.tick,
131
+ lastInputFrame: structuredClone(state.lastInputFrame),
132
+ nextSpawnId: state.nextSpawnId,
133
+ freeIndices: [...state.freeIndices],
134
+ slots: state.slots.map((slot) => structuredClone(slot)),
135
+ closures: [...state.closures.entries()].map(([id, closure]) => [id, structuredClone(closure)]),
136
+ };
137
+ }
138
+
139
+ function authoritativeView(state) {
140
+ const snapshot = authoritativeSnapshot(state);
141
+ snapshot.slots = snapshot.slots.map((slot) => ({
142
+ ...slot,
143
+ components: Array.isArray(slot.components)
144
+ ? slot.components.filter((component) => component.domain === 'simulation')
145
+ : slot.components,
146
+ }));
147
+ return snapshot;
148
+ }
149
+
150
+ function stateFromSnapshot(snapshot, componentSources) {
151
+ const closures = new Map(snapshot.closures.map(([id, closure]) => [id, structuredClone(closure)]));
152
+ const slots = snapshot.slots.map((slot) => {
153
+ const restored = structuredClone(slot);
154
+ if (!restored?.active) return restored;
155
+ const closure = restored.closureId === null ? null : closures.get(restored.closureId);
156
+ const authored = restored.sourceId !== null
157
+ ? componentSources.direct.get(restored.sourceId) ?? []
158
+ : componentSources.prefabs.get(`${closure?.prefabId}:${restored.localEntityId}`) ?? [];
159
+ restored.components.push(...authored.filter((component) => component.domain !== 'simulation').map((component) => structuredClone(component)));
160
+ return restored;
161
+ });
162
+ return {
163
+ worldId: snapshot.worldId,
164
+ tick: snapshot.tick,
165
+ lastInputFrame: structuredClone(snapshot.lastInputFrame),
166
+ nextSpawnId: snapshot.nextSpawnId,
167
+ freeIndices: [...snapshot.freeIndices],
168
+ slots,
169
+ closures,
170
+ };
171
+ }
172
+
173
+ export function createKinetixWorld(options) {
174
+ if (!isPlainObject(options) || typeof options.worldId !== 'string' || !isPlainObject(options.products)
175
+ || !(options.installedProfiles instanceof Map)) {
176
+ fail('KINETIX_WORLD_INVALID');
177
+ }
178
+ for (const key of ['maxCommandsPerTick', 'maxLiveRuntimeEntities', 'budgets', 'envelope']) {
179
+ if (Object.hasOwn(options, key)) {
180
+ fail('KINETIX_BUDGET_OVERRIDE');
181
+ }
182
+ }
183
+ const { worldId, products, installedProfiles } = options;
184
+ const initialization = options.initialization === undefined ? {} : structuredClone(options.initialization);
185
+ if (!isPlainObject(initialization)) fail('KINETIX_INITIALIZATION_INVALID');
186
+ const manifest = products.manifest;
187
+ if (!isPlainObject(manifest) || !Array.isArray(manifest.directEntities) || !Array.isArray(manifest.prefabs)) {
188
+ fail('KINETIX_PRODUCTS_INVALID');
189
+ }
190
+ if (!Array.isArray(manifest.systems) || manifest.systems.some((system) => (
191
+ !Number.isSafeInteger(system.phaseIndex)
192
+ || system.phaseIndex !== phases.indexOf(system.phase)
193
+ || !Number.isSafeInteger(system.order)
194
+ ))) {
195
+ fail('KINETIX_SYSTEM_ORDER_INVALID');
196
+ }
197
+ for (const payloadEntry of manifest.payloads) {
198
+ if (!products[payloadEntry.name] || digest(products[payloadEntry.name]) !== payloadEntry.digest
199
+ || stableBytes(products[payloadEntry.name]).length !== payloadEntry.byteLength) {
200
+ fail('KINETIX_PAYLOAD_DIGEST_MISMATCH');
201
+ }
202
+ }
203
+
204
+ const componentSources = hydrateComponentSources(products);
205
+ let state = createInitialState(worldId, products, componentSources);
206
+ const prefabById = new Map(manifest.prefabs.map((prefab) => [prefab.id, prefab]));
207
+ const systemById = new Map(manifest.systems.map((system) => [system.id, system]));
208
+ const directBySource = new Map(manifest.directEntities.map((entity) => [entity.sourceId, entity.index]));
209
+ const queues = new Map(phases.map((phase) => [phase, []]));
210
+ const queueSequences = new Map(phases.map((phase) => [phase, 0]));
211
+ const closedPhases = new Set();
212
+ const resolvedSpawns = new Map();
213
+ let nextTokenId = 1;
214
+ let queuedCommandCount = 0;
215
+ let iterating = false;
216
+
217
+ function clearTransientState() {
218
+ for (const phase of phases) {
219
+ queues.set(phase, []);
220
+ queueSequences.set(phase, 0);
221
+ }
222
+ closedPhases.clear();
223
+ resolvedSpawns.clear();
224
+ queuedCommandCount = 0;
225
+ }
226
+
227
+ function enqueue(system, command) {
228
+ if (!isPlainObject(command) || !system.structuralCommands.includes(command.kind)) {
229
+ fail('KINETIX_COMMAND_NOT_DECLARED');
230
+ }
231
+ if (!phases.includes(command.phase)) {
232
+ fail('KINETIX_PHASE_INVALID');
233
+ }
234
+ if (closedPhases.has(command.phase)) {
235
+ fail('KINETIX_PHASE_CLOSED');
236
+ }
237
+ if (queuedCommandCount >= manifest.budgets.maxCommandsPerTick) {
238
+ fail('KINETIX_COMMAND_BOUND_EXCEEDED');
239
+ }
240
+ if (typeof command.sortKey !== 'number' && typeof command.sortKey !== 'string') {
241
+ fail('KINETIX_COMMAND_INVALID');
242
+ }
243
+ const sequence = queueSequences.get(command.phase);
244
+ queueSequences.set(command.phase, sequence + 1);
245
+ const token = command.kind === 'spawnPrefab' ? { worldId, tokenId: nextTokenId } : null;
246
+ nextTokenId += command.kind === 'spawnPrefab' ? 1 : 0;
247
+ queues.get(command.phase).push({
248
+ ...structuredClone(command),
249
+ recordingPhaseIndex: system.phaseIndex,
250
+ systemOrder: system.order,
251
+ sequence,
252
+ token,
253
+ });
254
+ queuedCommandCount += 1;
255
+ return token;
256
+ }
257
+
258
+ function applySpawn(shadow, command, stagedSpawns) {
259
+ const prefab = prefabById.get(command.prefabId);
260
+ if (!prefab) {
261
+ fail('KINETIX_PREFAB_NOT_FOUND');
262
+ }
263
+ const liveForPrefab = [...shadow.closures.values()].filter((closure) => closure.prefabId === prefab.id).length;
264
+ if (liveForPrefab >= prefab.poolBudget
265
+ || activeCount(shadow) + prefab.entities.length > manifest.budgets.maxLiveRuntimeEntities) {
266
+ fail('KINETIX_BUDGET_EXCEEDED');
267
+ }
268
+ const spawnId = shadow.nextSpawnId;
269
+ shadow.nextSpawnId += 1;
270
+ const closureId = `${prefab.id}:${spawnId}`;
271
+ const handles = new Map();
272
+ const indices = [];
273
+ for (const localEntity of prefab.entities) {
274
+ const index = allocateIndex(shadow);
275
+ const prior = shadow.slots[index];
276
+ const generation = prior.generation ?? 1;
277
+ shadow.slots[index] = {
278
+ generation,
279
+ active: true,
280
+ enabled: localEntity.enabled,
281
+ sourceId: null,
282
+ profile: prefab.profile,
283
+ archetypeId: localEntity.archetypeId,
284
+ components: structuredClone(componentSources.prefabs.get(`${prefab.id}:${localEntity.localEntityId}`) ?? []),
285
+ closureId,
286
+ localEntityId: localEntity.localEntityId,
287
+ identity: {
288
+ kind: 'prefab',
289
+ prefabId: prefab.id,
290
+ monotonicSpawnId: spawnId,
291
+ localEntityId: localEntity.localEntityId,
292
+ },
293
+ };
294
+ indices.push(index);
295
+ handles.set(localEntity.localEntityId, handleFor(worldId, index, generation));
296
+ }
297
+ const rootHandle = handles.get(prefab.rootLocalEntityId);
298
+ shadow.closures.set(closureId, {
299
+ prefabId: prefab.id,
300
+ monotonicSpawnId: spawnId,
301
+ rootIndex: rootHandle.index,
302
+ indices,
303
+ });
304
+ stagedSpawns.set(command.token.tokenId, handles);
305
+ }
306
+
307
+ function applyDespawn(shadow, command, despawnedClosures, despawnedHandles) {
308
+ if (isPlainObject(command.handle) && Object.hasOwn(command.handle, 'tokenId')) {
309
+ fail('KINETIX_UNSPAWNED_TARGET');
310
+ }
311
+ const handleKey = `${command.handle?.worldId}:${command.handle?.index}:${command.handle?.generation}`;
312
+ if (despawnedHandles.has(handleKey)) {
313
+ fail('KINETIX_COMMAND_CONFLICT');
314
+ }
315
+ const slot = validateHandle(shadow, command.handle);
316
+ if (slot.closureId === null) {
317
+ fail('KINETIX_NOT_PREFAB_INSTANCE');
318
+ }
319
+ const closure = shadow.closures.get(slot.closureId);
320
+ if (closure.rootIndex !== command.handle.index) {
321
+ fail('KINETIX_NOT_CLOSURE_ROOT');
322
+ }
323
+ if (despawnedClosures.has(slot.closureId)) {
324
+ fail('KINETIX_COMMAND_CONFLICT');
325
+ }
326
+ despawnedClosures.add(slot.closureId);
327
+ closure.indices.forEach((index) => {
328
+ const member = shadow.slots[index];
329
+ despawnedHandles.add(`${worldId}:${index}:${member.generation}`);
330
+ });
331
+ for (const index of closure.indices) {
332
+ const member = shadow.slots[index];
333
+ member.active = false;
334
+ member.generation += 1;
335
+ shadow.freeIndices.push(index);
336
+ }
337
+ shadow.freeIndices.sort((left, right) => left - right);
338
+ shadow.closures.delete(slot.closureId);
339
+ }
340
+
341
+ function applySetEnabled(shadow, command, enabledWrites, despawnedClosures, despawnedHandles) {
342
+ if (isPlainObject(command.handle) && Object.hasOwn(command.handle, 'tokenId')) {
343
+ fail('KINETIX_UNSPAWNED_TARGET');
344
+ }
345
+ const handleKey = `${command.handle?.worldId}:${command.handle?.index}:${command.handle?.generation}`;
346
+ if (despawnedHandles.has(handleKey)) {
347
+ fail('KINETIX_COMMAND_CONFLICT');
348
+ }
349
+ const slot = validateHandle(shadow, command.handle);
350
+ if (typeof command.enabled !== 'boolean') {
351
+ fail('KINETIX_COMMAND_INVALID');
352
+ }
353
+ if (slot.closureId !== null && despawnedClosures.has(slot.closureId)) {
354
+ fail('KINETIX_COMMAND_CONFLICT');
355
+ }
356
+ const targetKey = slot.closureId ?? `direct:${command.handle.index}`;
357
+ if (enabledWrites.has(targetKey) && enabledWrites.get(targetKey) !== command.enabled) {
358
+ fail('KINETIX_COMMAND_CONFLICT');
359
+ }
360
+ enabledWrites.set(targetKey, command.enabled);
361
+ const indices = slot.closureId === null ? [command.handle.index] : shadow.closures.get(slot.closureId).indices;
362
+ indices.forEach((index) => { shadow.slots[index].enabled = command.enabled; });
363
+ }
364
+
365
+ function playback(phase) {
366
+ if (iterating) {
367
+ fail('KINETIX_MUTATION_DURING_QUERY');
368
+ }
369
+ if (!phases.includes(phase)) {
370
+ fail('KINETIX_PHASE_INVALID');
371
+ }
372
+ if (closedPhases.has(phase)) {
373
+ fail('KINETIX_PHASE_CLOSED');
374
+ }
375
+ const batch = queues.get(phase).sort((left, right) => (
376
+ left.recordingPhaseIndex - right.recordingPhaseIndex
377
+ || left.systemOrder - right.systemOrder
378
+ || compareSortKey(left.sortKey, right.sortKey)
379
+ || left.sequence - right.sequence
380
+ ));
381
+ const shadow = structuredClone(state);
382
+ const stagedSpawns = new Map();
383
+ const despawnedClosures = new Set();
384
+ const despawnedHandles = new Set();
385
+ const enabledWrites = new Map();
386
+ try {
387
+ for (const command of batch) {
388
+ if (command.kind === 'spawnPrefab') {
389
+ applySpawn(shadow, command, stagedSpawns);
390
+ } else if (command.kind === 'despawnPrefab') {
391
+ applyDespawn(shadow, command, despawnedClosures, despawnedHandles);
392
+ } else if (command.kind === 'setEnabled') {
393
+ applySetEnabled(shadow, command, enabledWrites, despawnedClosures, despawnedHandles);
394
+ }
395
+ }
396
+ } catch (error) {
397
+ queuedCommandCount -= batch.length;
398
+ queues.set(phase, []);
399
+ throw error;
400
+ }
401
+ state = shadow;
402
+ stagedSpawns.forEach((handles, tokenId) => resolvedSpawns.set(tokenId, handles));
403
+ queuedCommandCount -= batch.length;
404
+ queues.set(phase, []);
405
+ closedPhases.add(phase);
406
+ }
407
+
408
+ function recorderForTest(systemId) {
409
+ const system = systemById.get(systemId);
410
+ if (!system) {
411
+ fail('KINETIX_SYSTEM_NOT_INSTALLED');
412
+ }
413
+ return Object.freeze({ enqueue: (command) => enqueue(system, command) });
414
+ }
415
+
416
+ function directHandle(sourceId) {
417
+ const index = directBySource.get(sourceId);
418
+ if (index === undefined) {
419
+ fail('KINETIX_SOURCE_NOT_FOUND');
420
+ }
421
+ const slot = state.slots[index];
422
+ return handleFor(worldId, index, slot.generation);
423
+ }
424
+
425
+ function read(handle) {
426
+ const slot = validateHandle(state, handle);
427
+ return structuredClone({
428
+ enabled: slot.enabled,
429
+ identity: slot.identity,
430
+ components: slot.components,
431
+ archetypeId: slot.archetypeId,
432
+ });
433
+ }
434
+
435
+ function resolveSpawn(token, localEntityId) {
436
+ if (!isPlainObject(token) || token.worldId !== worldId) {
437
+ fail('KINETIX_UNSPAWNED_TARGET');
438
+ }
439
+ const handles = resolvedSpawns.get(token.tokenId);
440
+ if (!handles || !handles.has(localEntityId)) {
441
+ fail('KINETIX_UNSPAWNED_TARGET');
442
+ }
443
+ return structuredClone(handles.get(localEntityId));
444
+ }
445
+
446
+ function query(querySpec) {
447
+ const all = querySpec?.all ?? [];
448
+ const none = querySpec?.none ?? [];
449
+ return (function* iterate() {
450
+ if (iterating) {
451
+ fail('KINETIX_QUERY_REENTRANT');
452
+ }
453
+ iterating = true;
454
+ try {
455
+ for (let index = 0; index < state.slots.length; index += 1) {
456
+ const slot = state.slots[index];
457
+ if (!slot?.active || !slot.enabled) {
458
+ continue;
459
+ }
460
+ const types = new Set(slot.components.map((component) => component.type));
461
+ if (all.every((type) => types.has(type)) && none.every((type) => !types.has(type))) {
462
+ yield handleFor(worldId, index, slot.generation);
463
+ }
464
+ }
465
+ } finally {
466
+ iterating = false;
467
+ }
468
+ }());
469
+ }
470
+
471
+ function simulationComponent(sourceId, componentType) {
472
+ const index = directBySource.get(sourceId);
473
+ const slot = index === undefined ? null : state.slots[index];
474
+ const component = slot?.components.find((candidate) => (
475
+ candidate.domain === 'simulation' && candidate.type === componentType
476
+ ));
477
+ if (component === undefined) fail('KINETIX_SIMULATION_COMPONENT_NOT_FOUND');
478
+ return component;
479
+ }
480
+
481
+ const simulationAccess = Object.freeze({
482
+ read: (sourceId, componentType) => structuredClone(
483
+ simulationComponent(sourceId, componentType).properties,
484
+ ),
485
+ write: (sourceId, componentType, properties) => {
486
+ if (!isPlainObject(properties)) fail('KINETIX_SIMULATION_COMPONENT_INVALID');
487
+ simulationComponent(sourceId, componentType).properties = structuredClone(properties);
488
+ },
489
+ });
490
+
491
+ function deriveTransforms() {
492
+ const projected = new Map();
493
+ const worldValues = new Map();
494
+ function deriveIndex(index, parentIndex) {
495
+ const slot = state.slots[index];
496
+ if (!slot?.active) {
497
+ return;
498
+ }
499
+ const profile = installedProfiles.get(slot.profile);
500
+ const semantics = profile?.transformSemantics;
501
+ if (!semantics) {
502
+ return;
503
+ }
504
+ const local = slot.components.find((component) => component.type === semantics.localType)?.properties;
505
+ if (local === undefined) {
506
+ return;
507
+ }
508
+ const parentWorld = parentIndex === null ? null : worldValues.get(parentIndex) ?? null;
509
+ const worldValue = semantics.composeWorld({ parentWorld: structuredClone(parentWorld), local: structuredClone(local) });
510
+ worldValues.set(index, structuredClone(worldValue));
511
+ projected.set(handleFor(worldId, index, slot.generation), structuredClone(worldValue));
512
+ }
513
+ const parentByChild = new Map(manifest.parentEdges.map((edge) => [edge.childIndex, edge.parentIndex]));
514
+ manifest.directEntities
515
+ .filter((entity) => !parentByChild.has(entity.index))
516
+ .forEach((entity) => deriveIndex(entity.index, null));
517
+ manifest.parentEdges.forEach((edge) => deriveIndex(edge.childIndex, edge.parentIndex));
518
+ for (const closure of state.closures.values()) {
519
+ const prefab = prefabById.get(closure.prefabId);
520
+ const localToRuntime = new Map(prefab.entities.map((entity, index) => [index, closure.indices[index]]));
521
+ const prefabParentByChild = new Map(prefab.parentEdges.map((edge) => [edge.childLocalIndex, edge.parentLocalIndex]));
522
+ prefab.entities
523
+ .filter((entity) => !prefabParentByChild.has(entity.localIndex))
524
+ .forEach((entity) => deriveIndex(localToRuntime.get(entity.localIndex), null));
525
+ prefab.parentEdges.forEach((edge) => {
526
+ deriveIndex(localToRuntime.get(edge.childLocalIndex), localToRuntime.get(edge.parentLocalIndex));
527
+ });
528
+ }
529
+ return projected;
530
+ }
531
+
532
+ function step(inputFrame) {
533
+ if (!isPlainObject(inputFrame)) {
534
+ fail('KINETIX_INPUT_FRAME_INVALID');
535
+ }
536
+ clearTransientState();
537
+ for (const phase of phases.slice(0, 3)) {
538
+ for (const system of manifest.systems.filter((entry) => entry.phase === phase)) {
539
+ const profile = installedProfiles.get(system.profileId);
540
+ profile.executeSystem?.({
541
+ systemId: system.id,
542
+ inputFrame: structuredClone(inputFrame),
543
+ recorder: Object.freeze({ enqueue: (command) => enqueue(system, command) }),
544
+ simulation: simulationAccess,
545
+ initialization: structuredClone(initialization),
546
+ });
547
+ }
548
+ playback(phase);
549
+ }
550
+ state.tick += 1;
551
+ state.lastInputFrame = structuredClone(inputFrame);
552
+ return stableBytes(authoritativeView(state));
553
+ }
554
+
555
+ return Object.freeze({
556
+ directHandle,
557
+ read,
558
+ resolveSpawn,
559
+ recorderForTest,
560
+ playback,
561
+ beginTick: clearTransientState,
562
+ query,
563
+ deriveTransforms,
564
+ step,
565
+ snapshot: () => authoritativeView(state),
566
+ restore: (snapshot) => {
567
+ if (!isPlainObject(snapshot) || snapshot.worldId !== worldId
568
+ || !Number.isSafeInteger(snapshot.tick) || snapshot.tick < 0
569
+ || !Number.isSafeInteger(snapshot.nextSpawnId) || snapshot.nextSpawnId < 1
570
+ || !Array.isArray(snapshot.slots) || !Array.isArray(snapshot.closures)
571
+ || !Array.isArray(snapshot.freeIndices)) {
572
+ fail('KINETIX_SNAPSHOT_INVALID');
573
+ }
574
+ state = stateFromSnapshot(structuredClone(snapshot), componentSources);
575
+ clearTransientState();
576
+ },
577
+ authoritativeBytes: () => stableBytes(authoritativeView(state)),
578
+ canonicalStateBytes: () => stableBytes(authoritativeView(state)),
579
+ });
580
+ }