@typeonce/ritmo 0.7.0 → 0.8.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/dist/Command.d.ts +102 -17
- package/dist/Command.d.ts.map +1 -1
- package/dist/Command.js +10 -8
- package/dist/Command.js.map +1 -1
- package/dist/Condition.d.ts +12 -1
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js.map +1 -1
- package/dist/Debug.d.ts +63 -7
- package/dist/Debug.d.ts.map +1 -1
- package/dist/Descriptor.d.ts +36 -3
- package/dist/Descriptor.d.ts.map +1 -1
- package/dist/Descriptor.js +11 -3
- package/dist/Descriptor.js.map +1 -1
- package/dist/Entity.d.ts +12 -4
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js.map +1 -1
- package/dist/EntityScope.d.ts +20 -1
- package/dist/EntityScope.d.ts.map +1 -1
- package/dist/EntityScope.js +8 -2
- package/dist/EntityScope.js.map +1 -1
- package/dist/Machine.d.ts +55 -33
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +6 -2
- package/dist/Machine.js.map +1 -1
- package/dist/Query.d.ts +48 -4
- package/dist/Query.d.ts.map +1 -1
- package/dist/Query.js +32 -4
- package/dist/Query.js.map +1 -1
- package/dist/Runtime.d.ts +3 -0
- package/dist/Runtime.d.ts.map +1 -1
- package/dist/Runtime.js +330 -24
- package/dist/Runtime.js.map +1 -1
- package/dist/Schema.d.ts +141 -25
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +29 -3
- package/dist/Schema.js.map +1 -1
- package/dist/Snapshot.d.ts +14 -0
- package/dist/Snapshot.d.ts.map +1 -1
- package/dist/Snapshot.js.map +1 -1
- package/dist/System.d.ts +64 -7
- package/dist/System.d.ts.map +1 -1
- package/dist/System.js +32 -0
- package/dist/System.js.map +1 -1
- package/dist/internal/debug.d.ts +7 -0
- package/dist/internal/debug.d.ts.map +1 -1
- package/dist/internal/debug.js +97 -6
- package/dist/internal/debug.js.map +1 -1
- package/dist/internal/equal.d.ts +8 -0
- package/dist/internal/equal.d.ts.map +1 -0
- package/dist/internal/equal.js +38 -0
- package/dist/internal/equal.js.map +1 -0
- package/dist/internal/fragments.d.ts.map +1 -1
- package/dist/internal/fragments.js +11 -1
- package/dist/internal/fragments.js.map +1 -1
- package/dist/internal/game.d.ts.map +1 -1
- package/dist/internal/game.js +33 -5
- package/dist/internal/game.js.map +1 -1
- package/dist/internal/order.d.ts.map +1 -1
- package/dist/internal/order.js +4 -2
- package/dist/internal/order.js.map +1 -1
- package/dist/internal/queries.d.ts.map +1 -1
- package/dist/internal/queries.js +30 -8
- package/dist/internal/queries.js.map +1 -1
- package/dist/internal/requirements.d.ts +26 -0
- package/dist/internal/requirements.d.ts.map +1 -0
- package/dist/internal/requirements.js +46 -0
- package/dist/internal/requirements.js.map +1 -0
- package/package.json +1 -1
- package/src/Command.ts +149 -29
- package/src/Condition.ts +17 -1
- package/src/Debug.ts +64 -7
- package/src/Descriptor.ts +55 -10
- package/src/Entity.ts +13 -4
- package/src/EntityScope.ts +21 -2
- package/src/Machine.ts +68 -34
- package/src/Query.ts +71 -7
- package/src/Runtime.ts +336 -22
- package/src/Schema.ts +185 -28
- package/src/Snapshot.ts +4 -0
- package/src/System.ts +75 -6
- package/src/internal/debug.ts +104 -8
- package/src/internal/equal.ts +31 -0
- package/src/internal/fragments.ts +12 -2
- package/src/internal/game.ts +51 -5
- package/src/internal/order.ts +8 -4
- package/src/internal/queries.ts +34 -10
- package/src/internal/requirements.ts +71 -0
package/dist/Runtime.js
CHANGED
|
@@ -59,8 +59,10 @@ import * as HostClockModule from "./HostClock.js";
|
|
|
59
59
|
import * as Command from "./Command.js";
|
|
60
60
|
import * as DescriptorModule from "./Descriptor.js";
|
|
61
61
|
import * as Cells from "./internal/cells.js";
|
|
62
|
+
import { sameValue } from "./internal/equal.js";
|
|
62
63
|
import * as DebugInternal from "./internal/debug.js";
|
|
63
64
|
import { makeQueryEngine } from "./internal/queries.js";
|
|
65
|
+
import { requirementIndex } from "./internal/requirements.js";
|
|
64
66
|
import * as Streams from "./internal/streams.js";
|
|
65
67
|
import { ABSENT, makeWorld } from "./internal/world.js";
|
|
66
68
|
import * as Query from "./Query.js";
|
|
@@ -240,8 +242,52 @@ const makeValidatedRuntime = (options) => {
|
|
|
240
242
|
const providedMachines = (options.machines ?? machines());
|
|
241
243
|
const machineEntries = (options.machines ?? machines())[runtimeMachinesEntries];
|
|
242
244
|
const machineDefinitionOrder = new Map(options.machineDefinitions.map((machine, index) => [machine.key, index]));
|
|
245
|
+
/** Machines the runtime was given an initial value for. */
|
|
246
|
+
const providedMachineKeys = new Set();
|
|
243
247
|
for (const provision of machineEntries) {
|
|
244
248
|
currentMachines.set(provision.machine.key, provision.initial);
|
|
249
|
+
providedMachineKeys.add(provision.machine.key);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Sub-states by parent machine key, in definition order, and the value
|
|
253
|
+
* each enters whenever its parent enters the state it exists in (its
|
|
254
|
+
* runtime-provided value). A sub-state has no current value while its
|
|
255
|
+
* parent is elsewhere, and never one when the runtime does not provide it.
|
|
256
|
+
*/
|
|
257
|
+
const subStates = new Map();
|
|
258
|
+
const subStateEntry = new Map();
|
|
259
|
+
for (const machine of options.machineDefinitions) {
|
|
260
|
+
if (machine.within === undefined)
|
|
261
|
+
continue;
|
|
262
|
+
const siblings = subStates.get(machine.within.machine.key);
|
|
263
|
+
if (siblings === undefined)
|
|
264
|
+
subStates.set(machine.within.machine.key, [machine]);
|
|
265
|
+
else
|
|
266
|
+
siblings.push(machine);
|
|
267
|
+
if (!currentMachines.has(machine.key))
|
|
268
|
+
continue;
|
|
269
|
+
subStateEntry.set(machine.key, currentMachines.get(machine.key));
|
|
270
|
+
}
|
|
271
|
+
// Definition order puts a parent before its sub-states, so a sub-state of
|
|
272
|
+
// an inactive sub-state is cleared too.
|
|
273
|
+
for (const machine of options.machineDefinitions) {
|
|
274
|
+
if (machine.within !== undefined && currentMachines.get(machine.within.machine.key) !== machine.within.state) {
|
|
275
|
+
currentMachines.delete(machine.key);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Scope despawn triggers by machine key, in scope definition order
|
|
280
|
+
* (`Game.EntityScope(name, { despawnOnExit, despawnOnEnter })`).
|
|
281
|
+
*/
|
|
282
|
+
const scopeTriggers = new Map();
|
|
283
|
+
for (const scope of options.scopeDefinitions) {
|
|
284
|
+
for (const trigger of scope.despawnOn) {
|
|
285
|
+
const triggers = scopeTriggers.get(trigger.machine.key);
|
|
286
|
+
if (triggers === undefined)
|
|
287
|
+
scopeTriggers.set(trigger.machine.key, [{ trigger, scope }]);
|
|
288
|
+
else
|
|
289
|
+
triggers.push({ trigger, scope });
|
|
290
|
+
}
|
|
245
291
|
}
|
|
246
292
|
const appendRelationFailure = (relation, operation, sourceId, targetId, error) => {
|
|
247
293
|
if (tracing) {
|
|
@@ -283,15 +329,57 @@ const makeValidatedRuntime = (options) => {
|
|
|
283
329
|
}
|
|
284
330
|
systems.add(commandOrigin);
|
|
285
331
|
};
|
|
332
|
+
/**
|
|
333
|
+
* `Schema.requires`, checked when commands are applied. Types reject a
|
|
334
|
+
* breaking command when they can see it; this catches the rest: a query
|
|
335
|
+
* proof gone stale within one flush (another system's command applied in
|
|
336
|
+
* between), erased types, and raw ids. A breaking command is skipped
|
|
337
|
+
* whole, traced, and reported by the `requirement-violated` lint.
|
|
338
|
+
*/
|
|
339
|
+
const requirements = requirementIndex(options.schema);
|
|
340
|
+
/** With `debug`, skipped commands by system, command, and requirement, for the lint. */
|
|
341
|
+
const requirementViolations = new Map();
|
|
342
|
+
const violateRequirement = (command, entity, [component, missing]) => {
|
|
343
|
+
if (tracing)
|
|
344
|
+
recordEffect({ kind: "requirementViolated", entity, command, component, missing });
|
|
345
|
+
if (debugEnabled) {
|
|
346
|
+
const key = `${commandOrigin}/${command}/${component}/${missing}`;
|
|
347
|
+
const seen = requirementViolations.get(key);
|
|
348
|
+
requirementViolations.set(key, { system: commandOrigin, command, component, missing, entity, count: (seen?.count ?? 0) + 1 });
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
const holds = (id) => {
|
|
352
|
+
const record = world.records.get(id);
|
|
353
|
+
return (descriptor) => record !== undefined && world.has(record, world.ordinalOf(descriptor));
|
|
354
|
+
};
|
|
355
|
+
/** The first requirement `components` break, as `[requirer, missing]`. */
|
|
356
|
+
const missingIn = (components, alsoHeld) => {
|
|
357
|
+
for (const [descriptor] of components) {
|
|
358
|
+
for (const required of requirements.requires.get(descriptor.name) ?? []) {
|
|
359
|
+
if (!alsoHeld(required) && !components.some(([staged]) => staged.name === required.name)) {
|
|
360
|
+
return [descriptor.name, required.name];
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return undefined;
|
|
365
|
+
};
|
|
286
366
|
/**
|
|
287
367
|
* World adapter used by deferred commands.
|
|
288
368
|
*/
|
|
289
369
|
const commandWorld = {
|
|
290
370
|
spawnEntity(id, components) {
|
|
371
|
+
if (!requirements.empty) {
|
|
372
|
+
const missing = missingIn(components, () => false);
|
|
373
|
+
if (missing !== undefined) {
|
|
374
|
+
violateRequirement("spawn", id, missing);
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
291
378
|
if (debugEnabled)
|
|
292
379
|
for (const [descriptor] of components)
|
|
293
380
|
noteCommand("spawns", descriptor.name);
|
|
294
381
|
world.spawnEntity(id, components);
|
|
382
|
+
return true;
|
|
295
383
|
},
|
|
296
384
|
destroyEntity(id) {
|
|
297
385
|
if (debugEnabled) {
|
|
@@ -311,15 +399,37 @@ const makeValidatedRuntime = (options) => {
|
|
|
311
399
|
noteCommand("despawnsScope", scope.name);
|
|
312
400
|
world.destroyEntityScope(scope.key);
|
|
313
401
|
},
|
|
314
|
-
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
402
|
+
removeComponents(id, descriptors) {
|
|
403
|
+
if (!requirements.empty && world.records.has(id)) {
|
|
404
|
+
const held = holds(id);
|
|
405
|
+
for (const descriptor of descriptors) {
|
|
406
|
+
for (const requirer of requirements.requiredBy.get(descriptor.name) ?? []) {
|
|
407
|
+
if (held(requirer) && !descriptors.some((removed) => removed.name === requirer.name)) {
|
|
408
|
+
violateRequirement("remove", id, [requirer.name, descriptor.name]);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
for (const descriptor of descriptors) {
|
|
415
|
+
if (debugEnabled)
|
|
416
|
+
noteCommand("removes", descriptor.name);
|
|
417
|
+
world.removeComponent(id, descriptor);
|
|
418
|
+
}
|
|
318
419
|
},
|
|
319
|
-
|
|
320
|
-
if (
|
|
321
|
-
|
|
322
|
-
|
|
420
|
+
insertComponents(id, entries) {
|
|
421
|
+
if (!requirements.empty && world.records.has(id)) {
|
|
422
|
+
const missing = missingIn(entries, holds(id));
|
|
423
|
+
if (missing !== undefined) {
|
|
424
|
+
violateRequirement("insert", id, missing);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
for (const [descriptor, value] of entries) {
|
|
429
|
+
if (debugEnabled)
|
|
430
|
+
noteCommand("inserts", descriptor.name);
|
|
431
|
+
world.writeComponent(id, descriptor, value);
|
|
432
|
+
}
|
|
323
433
|
},
|
|
324
434
|
tryRelate(id, relation, target) {
|
|
325
435
|
if (debugEnabled) {
|
|
@@ -494,6 +604,14 @@ const makeValidatedRuntime = (options) => {
|
|
|
494
604
|
store.set(key, value);
|
|
495
605
|
};
|
|
496
606
|
const writeResource = journalStoreWrite(resources, resourceOriginals);
|
|
607
|
+
/**
|
|
608
|
+
* Resource change ticks for `changedResource` readers. Only resources some
|
|
609
|
+
* system reads with `changedResource` are compared at commit; a reader's
|
|
610
|
+
* first run reports a change anyway, so writes before it registers are not
|
|
611
|
+
* needed.
|
|
612
|
+
*/
|
|
613
|
+
const watchedResources = new Set();
|
|
614
|
+
const resourceChangedAt = new Map();
|
|
497
615
|
const journalMachine = (key) => {
|
|
498
616
|
if (!machineOriginals.has(key)) {
|
|
499
617
|
machineOriginals.set(key, pendingMachines.get(key) ?? absentValue);
|
|
@@ -504,6 +622,15 @@ const makeValidatedRuntime = (options) => {
|
|
|
504
622
|
};
|
|
505
623
|
const commitSystemTransaction = () => {
|
|
506
624
|
world.commitTransaction();
|
|
625
|
+
if (watchedResources.size > 0) {
|
|
626
|
+
for (const [key, before] of resourceOriginals) {
|
|
627
|
+
if (!watchedResources.has(key))
|
|
628
|
+
continue;
|
|
629
|
+
if (before === absentValue || !sameValue(before, resources.get(key))) {
|
|
630
|
+
resourceChangedAt.set(key, world.currentTick());
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
507
634
|
if (emittedEvents.size > 0) {
|
|
508
635
|
const published = world.advanceTick();
|
|
509
636
|
for (const [key, values] of emittedEvents) {
|
|
@@ -540,6 +667,16 @@ const makeValidatedRuntime = (options) => {
|
|
|
540
667
|
emittedEvents.clear();
|
|
541
668
|
};
|
|
542
669
|
const makeResourceWriteView = (descriptor, store, write) => Cells.storeWrite(store, descriptor.key, write, DescriptorModule.constructorOf(descriptor));
|
|
670
|
+
const makeResourceChangedView = (descriptor, reader) => {
|
|
671
|
+
const key = descriptor.key;
|
|
672
|
+
watchedResources.add(key);
|
|
673
|
+
const read = Cells.storeRead(resources, key);
|
|
674
|
+
return {
|
|
675
|
+
get: () => read.get(),
|
|
676
|
+
// `since` is the previous completed run; 0 before the first one.
|
|
677
|
+
changed: () => reader.since === 0 || (resourceChangedAt.get(key) ?? 0) > reader.since
|
|
678
|
+
};
|
|
679
|
+
};
|
|
543
680
|
const makeEventReadView = (descriptorKey, reader) => ({
|
|
544
681
|
all() {
|
|
545
682
|
return events.since(descriptorKey, reader.streamSince);
|
|
@@ -598,15 +735,28 @@ const makeValidatedRuntime = (options) => {
|
|
|
598
735
|
get: () => currentMachines.get(stateMachine.key),
|
|
599
736
|
is: (value) => currentMachines.get(stateMachine.key) === value
|
|
600
737
|
});
|
|
738
|
+
/** Queues a next value; a sub-state whose parent is in another state queues nothing and fails. */
|
|
739
|
+
const queueMachine = (stateMachine, value, skipIfSame) => {
|
|
740
|
+
const within = stateMachine.within;
|
|
741
|
+
if (within !== undefined && !currentMachines.has(stateMachine.key)) {
|
|
742
|
+
return Result.failure({
|
|
743
|
+
_tag: "SubStateInactive",
|
|
744
|
+
machine: stateMachine.name,
|
|
745
|
+
parent: within.machine.name,
|
|
746
|
+
requires: within.state
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
journalMachine(stateMachine.key);
|
|
750
|
+
pendingMachines.set(stateMachine.key, { value, skipIfSame });
|
|
751
|
+
return (within === undefined ? undefined : succeeded);
|
|
752
|
+
};
|
|
601
753
|
const makeNextMachineWriteView = (stateMachine) => ({
|
|
602
754
|
getPending: () => pendingMachines.get(stateMachine.key)?.value,
|
|
603
755
|
set(value) {
|
|
604
|
-
|
|
605
|
-
pendingMachines.set(stateMachine.key, { value, skipIfSame: false });
|
|
756
|
+
return queueMachine(stateMachine, value, false);
|
|
606
757
|
},
|
|
607
758
|
setIfChanged(value) {
|
|
608
|
-
|
|
609
|
-
pendingMachines.set(stateMachine.key, { value, skipIfSame: true });
|
|
759
|
+
return queueMachine(stateMachine, value, true);
|
|
610
760
|
},
|
|
611
761
|
reset() {
|
|
612
762
|
journalMachine(stateMachine.key);
|
|
@@ -660,7 +810,9 @@ const makeValidatedRuntime = (options) => {
|
|
|
660
810
|
lookup,
|
|
661
811
|
resources: mapRecord(spec.resources, (access) => access.mode === "read"
|
|
662
812
|
? Cells.storeRead(resources, access.descriptor.key)
|
|
663
|
-
:
|
|
813
|
+
: access.mode === "changed"
|
|
814
|
+
? makeResourceChangedView(access.descriptor, reader)
|
|
815
|
+
: makeResourceWriteView(access.descriptor, resources, writeResource)),
|
|
664
816
|
events: mapRecord(spec.events, (access) => access.mode === "read"
|
|
665
817
|
? makeEventReadView(access.descriptor.key, reader)
|
|
666
818
|
: makeEventWriteView(access.descriptor.key)),
|
|
@@ -971,6 +1123,39 @@ const makeValidatedRuntime = (options) => {
|
|
|
971
1123
|
activeTransitions.delete(schedule.transition.machine.key);
|
|
972
1124
|
}
|
|
973
1125
|
};
|
|
1126
|
+
/**
|
|
1127
|
+
* Despawns the scopes whose trigger matches a committed transition: leaving
|
|
1128
|
+
* `snapshot.from` or entering `snapshot.to`. It runs right after the
|
|
1129
|
+
* commit, so `onExit` systems still saw the members and entities spawned by
|
|
1130
|
+
* `onEnter` survive. Members are the entities in the scope at that moment:
|
|
1131
|
+
* commands queued by `onExit` systems and not yet applied are not.
|
|
1132
|
+
*/
|
|
1133
|
+
const despawnTriggeredScopes = (machineKey, snapshot) => {
|
|
1134
|
+
const triggers = scopeTriggers.get(machineKey);
|
|
1135
|
+
if (triggers === undefined)
|
|
1136
|
+
return;
|
|
1137
|
+
const applied = [];
|
|
1138
|
+
for (const { trigger, scope } of triggers) {
|
|
1139
|
+
const matches = trigger.phase === "exit" ? trigger.state === snapshot.from : trigger.state === snapshot.to;
|
|
1140
|
+
if (!matches)
|
|
1141
|
+
continue;
|
|
1142
|
+
const origin = `${trigger.phase === "exit" ? "onExit" : "onEnter"}(${trigger.machine.name}=${String(trigger.state)})`;
|
|
1143
|
+
const effects = [];
|
|
1144
|
+
commandEffects = tracing ? effects : undefined;
|
|
1145
|
+
commandOrigin = origin;
|
|
1146
|
+
try {
|
|
1147
|
+
commandWorld.destroyEntityScope(scope);
|
|
1148
|
+
}
|
|
1149
|
+
finally {
|
|
1150
|
+
commandEffects = undefined;
|
|
1151
|
+
}
|
|
1152
|
+
if (tracing)
|
|
1153
|
+
applied.push({ tag: `despawnScope ${scope.name}`, system: origin, effects });
|
|
1154
|
+
}
|
|
1155
|
+
if (applied.length > 0) {
|
|
1156
|
+
emit({ type: "deferred", frame: frameCount, tick: world.currentTick(), schedule: currentSchedule(), marker: "applyStateTransitions", commands: applied });
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
974
1159
|
/**
|
|
975
1160
|
* Applies queued commands, then queued machine transitions, running the
|
|
976
1161
|
* matching exit, transition, and enter schedules from the bundle.
|
|
@@ -983,6 +1168,10 @@ const makeValidatedRuntime = (options) => {
|
|
|
983
1168
|
- (machineDefinitionOrder.get(rightKey) ?? Number.MAX_SAFE_INTEGER));
|
|
984
1169
|
const schedules = bundle?.entries ?? [];
|
|
985
1170
|
for (const [machineKey, pending] of scheduledTransitions) {
|
|
1171
|
+
if (pendingMachines.get(machineKey) !== pending) {
|
|
1172
|
+
// Discarded earlier in this marker: its parent left the state it exists in.
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
986
1175
|
pendingMachines.delete(machineKey);
|
|
987
1176
|
const current = currentMachines.get(machineKey);
|
|
988
1177
|
if (current === undefined) {
|
|
@@ -993,11 +1182,24 @@ const makeValidatedRuntime = (options) => {
|
|
|
993
1182
|
traceTransition(machineKey, { from: current, to: current }, "unchanged");
|
|
994
1183
|
continue;
|
|
995
1184
|
}
|
|
996
|
-
previousMachines.set(machineKey, current);
|
|
997
1185
|
const snapshot = {
|
|
998
1186
|
from: current,
|
|
999
1187
|
to: pending.value
|
|
1000
1188
|
};
|
|
1189
|
+
// Sub-states that exist in the state being left exit first, deepest
|
|
1190
|
+
// first: their exit schedules run before the parent's, while they
|
|
1191
|
+
// still have their values, and they lose them at the parent's commit.
|
|
1192
|
+
const leaving = activeSubStatesIn(machineKey, snapshot.from);
|
|
1193
|
+
for (const sub of leaving) {
|
|
1194
|
+
const value = currentMachines.get(sub.key);
|
|
1195
|
+
const result = runPhase(sub.key, "exit", value, { from: value, to: undefined }, schedules);
|
|
1196
|
+
if (!result.ok) {
|
|
1197
|
+
pendingMachines.set(machineKey, pending);
|
|
1198
|
+
if (tracing)
|
|
1199
|
+
traceTransition(machineKey, snapshot, "failed");
|
|
1200
|
+
return result;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1001
1203
|
for (const schedule of schedules) {
|
|
1002
1204
|
const transition = schedule.transition;
|
|
1003
1205
|
if (transition.phase === "exit" && transition.machine.key === machineKey && transition.state === snapshot.from) {
|
|
@@ -1026,11 +1228,9 @@ const makeValidatedRuntime = (options) => {
|
|
|
1026
1228
|
}
|
|
1027
1229
|
}
|
|
1028
1230
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
if (tracing)
|
|
1033
|
-
traceTransition(machineKey, snapshot, "applied");
|
|
1231
|
+
for (const sub of leaving)
|
|
1232
|
+
commitMachine(sub.key, currentMachines.get(sub.key), undefined);
|
|
1233
|
+
commitMachine(machineKey, current, pending.value);
|
|
1034
1234
|
for (const schedule of schedules) {
|
|
1035
1235
|
const transition = schedule.transition;
|
|
1036
1236
|
if (transition.phase === "enter" && transition.machine.key === machineKey && transition.state === snapshot.to) {
|
|
@@ -1042,6 +1242,79 @@ const makeValidatedRuntime = (options) => {
|
|
|
1042
1242
|
}
|
|
1043
1243
|
}
|
|
1044
1244
|
}
|
|
1245
|
+
// Sub-states that exist in the entered state enter their value, parents first.
|
|
1246
|
+
const entered = enterSubStates(machineKey, snapshot.to, schedules);
|
|
1247
|
+
if (!entered.ok)
|
|
1248
|
+
return entered;
|
|
1249
|
+
}
|
|
1250
|
+
return succeeded;
|
|
1251
|
+
};
|
|
1252
|
+
/**
|
|
1253
|
+
* Commits one machine's value (`undefined` for a sub-state that stops
|
|
1254
|
+
* existing): records the change and its transition event, traces it, and
|
|
1255
|
+
* despawns the scopes it triggers.
|
|
1256
|
+
*/
|
|
1257
|
+
const commitMachine = (machineKey, from, to) => {
|
|
1258
|
+
const snapshot = { from, to };
|
|
1259
|
+
if (from !== undefined)
|
|
1260
|
+
previousMachines.set(machineKey, from);
|
|
1261
|
+
if (to === undefined) {
|
|
1262
|
+
currentMachines.delete(machineKey);
|
|
1263
|
+
pendingMachines.delete(machineKey);
|
|
1264
|
+
}
|
|
1265
|
+
else {
|
|
1266
|
+
currentMachines.set(machineKey, to);
|
|
1267
|
+
}
|
|
1268
|
+
changedMachines.add(machineKey);
|
|
1269
|
+
transitionEvents.append(machineKey, world.advanceTick(), [snapshot]);
|
|
1270
|
+
if (tracing)
|
|
1271
|
+
traceTransition(machineKey, snapshot, "applied");
|
|
1272
|
+
despawnTriggeredScopes(machineKey, snapshot);
|
|
1273
|
+
};
|
|
1274
|
+
/** Runs every schedule of `phase` for `machineKey` in `state`, in bundle order. */
|
|
1275
|
+
const runPhase = (machineKey, phase, state, snapshot, schedules) => {
|
|
1276
|
+
for (const schedule of schedules) {
|
|
1277
|
+
const transition = schedule.transition;
|
|
1278
|
+
if (transition.phase === phase && transition.machine.key === machineKey && transition.state === state) {
|
|
1279
|
+
const result = runTransitionSchedule(schedule, snapshot);
|
|
1280
|
+
if (!result.ok)
|
|
1281
|
+
return result;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return succeeded;
|
|
1285
|
+
};
|
|
1286
|
+
/** The sub-states with a value that exist in `state` of `parentKey`, and theirs, deepest first. */
|
|
1287
|
+
const activeSubStatesIn = (parentKey, state) => {
|
|
1288
|
+
const result = [];
|
|
1289
|
+
for (const sub of subStates.get(parentKey) ?? []) {
|
|
1290
|
+
const value = currentMachines.get(sub.key);
|
|
1291
|
+
if (sub.within.state !== state || value === undefined)
|
|
1292
|
+
continue;
|
|
1293
|
+
result.push(...activeSubStatesIn(sub.key, value), sub);
|
|
1294
|
+
}
|
|
1295
|
+
return result;
|
|
1296
|
+
};
|
|
1297
|
+
/**
|
|
1298
|
+
* Gives the sub-states of `parentKey` that exist in `state` their entry
|
|
1299
|
+
* value, then runs their `onEnter` schedules and enters their own
|
|
1300
|
+
* sub-states.
|
|
1301
|
+
*/
|
|
1302
|
+
const enterSubStates = (parentKey, state, schedules) => {
|
|
1303
|
+
for (const sub of subStates.get(parentKey) ?? []) {
|
|
1304
|
+
if (sub.within.state !== state || currentMachines.has(sub.key) || !subStateEntry.has(sub.key))
|
|
1305
|
+
continue;
|
|
1306
|
+
const value = subStateEntry.get(sub.key);
|
|
1307
|
+
commitMachine(sub.key, undefined, value);
|
|
1308
|
+
const snapshot = { from: undefined, to: value };
|
|
1309
|
+
const result = runPhase(sub.key, "enter", value, snapshot, schedules);
|
|
1310
|
+
if (!result.ok) {
|
|
1311
|
+
if (tracing)
|
|
1312
|
+
traceTransition(sub.key, snapshot, "enterFailed");
|
|
1313
|
+
return result;
|
|
1314
|
+
}
|
|
1315
|
+
const nested = enterSubStates(sub.key, value, schedules);
|
|
1316
|
+
if (!nested.ok)
|
|
1317
|
+
return nested;
|
|
1045
1318
|
}
|
|
1046
1319
|
return succeeded;
|
|
1047
1320
|
};
|
|
@@ -1213,7 +1486,8 @@ const makeValidatedRuntime = (options) => {
|
|
|
1213
1486
|
case "resource":
|
|
1214
1487
|
return resources.has(requirement.key);
|
|
1215
1488
|
case "stateMachine":
|
|
1216
|
-
|
|
1489
|
+
// An inactive sub-state has no value but is provided.
|
|
1490
|
+
return providedMachineKeys.has(requirement.key) || currentMachines.has(requirement.key);
|
|
1217
1491
|
}
|
|
1218
1492
|
};
|
|
1219
1493
|
const tryRunSchedule = (schedule) => {
|
|
@@ -1317,6 +1591,11 @@ const makeValidatedRuntime = (options) => {
|
|
|
1317
1591
|
return Result.failure({ _tag: "InvalidComponent", entityId: entity.id, name, error: value.error });
|
|
1318
1592
|
components.push([descriptor, value.value]);
|
|
1319
1593
|
}
|
|
1594
|
+
// Transient components are never saved, so a requirement on one is not checked here.
|
|
1595
|
+
const missing = missingIn(components, DescriptorModule.isTransient);
|
|
1596
|
+
if (missing !== undefined) {
|
|
1597
|
+
return Result.failure({ _tag: "MissingRequiredComponent", entityId: entity.id, name: missing[1], requiredBy: missing[0] });
|
|
1598
|
+
}
|
|
1320
1599
|
entities.push({ id: entity.id, components });
|
|
1321
1600
|
}
|
|
1322
1601
|
const validatedResources = [];
|
|
@@ -1358,14 +1637,34 @@ const makeValidatedRuntime = (options) => {
|
|
|
1358
1637
|
}
|
|
1359
1638
|
}
|
|
1360
1639
|
}
|
|
1361
|
-
const
|
|
1640
|
+
const restoredMachines = new Map(currentMachines);
|
|
1362
1641
|
for (const [name, value] of Object.entries(saved.machines)) {
|
|
1363
1642
|
const machine = machinesByName.get(name);
|
|
1364
1643
|
if (!machine)
|
|
1365
1644
|
return Result.failure({ _tag: "UnknownMachine", name });
|
|
1366
1645
|
if (!machine.values.includes(value))
|
|
1367
1646
|
return Result.failure({ _tag: "InvalidMachineState", name, value });
|
|
1368
|
-
|
|
1647
|
+
restoredMachines.set(machine.key, value);
|
|
1648
|
+
}
|
|
1649
|
+
// A save has no value for a sub-state whose parent is elsewhere; one that
|
|
1650
|
+
// has a value must have its parent in the state it exists in. A sub-state
|
|
1651
|
+
// missing while its parent is there enters its entry value.
|
|
1652
|
+
for (const machine of options.machineDefinitions) {
|
|
1653
|
+
const within = machine.within;
|
|
1654
|
+
if (within === undefined)
|
|
1655
|
+
continue;
|
|
1656
|
+
const active = restoredMachines.get(within.machine.key) === within.state;
|
|
1657
|
+
if (Object.hasOwn(saved.machines, machine.name)) {
|
|
1658
|
+
if (!active) {
|
|
1659
|
+
return Result.failure({ _tag: "InactiveSubState", name: machine.name, parent: within.machine.name, requires: within.state });
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
else if (active && subStateEntry.has(machine.key)) {
|
|
1663
|
+
restoredMachines.set(machine.key, subStateEntry.get(machine.key));
|
|
1664
|
+
}
|
|
1665
|
+
else {
|
|
1666
|
+
restoredMachines.delete(machine.key);
|
|
1667
|
+
}
|
|
1369
1668
|
}
|
|
1370
1669
|
const scoped = new Set();
|
|
1371
1670
|
const memberships = [];
|
|
@@ -1397,9 +1696,12 @@ const makeValidatedRuntime = (options) => {
|
|
|
1397
1696
|
}
|
|
1398
1697
|
for (const [id, key] of memberships)
|
|
1399
1698
|
world.assignEntityScope(id, key);
|
|
1400
|
-
for (const [key, value] of validatedResources)
|
|
1699
|
+
for (const [key, value] of validatedResources) {
|
|
1401
1700
|
resources.set(key, value);
|
|
1402
|
-
|
|
1701
|
+
resourceChangedAt.set(key, world.currentTick());
|
|
1702
|
+
}
|
|
1703
|
+
currentMachines.clear();
|
|
1704
|
+
for (const [key, value] of restoredMachines)
|
|
1403
1705
|
currentMachines.set(key, value);
|
|
1404
1706
|
// Locals are not saved: each system starts over from its factory.
|
|
1405
1707
|
localSlots.forEach(({ slot }, index) => { slot.context.local = rebuiltLocals[index]; });
|
|
@@ -1546,11 +1848,14 @@ const makeValidatedRuntime = (options) => {
|
|
|
1546
1848
|
schema: options.schema,
|
|
1547
1849
|
machines: [...new Set(machinesByName.values())],
|
|
1548
1850
|
currentMachine: (machine) => currentMachines.get(machine.key),
|
|
1851
|
+
providesMachine: (machine) => providedMachineKeys.has(machine.key),
|
|
1852
|
+
scopes: options.scopeDefinitions,
|
|
1549
1853
|
providedServices: Object.keys(providedServices),
|
|
1550
1854
|
externallyWrittenEvents: new Set([...externallyWrittenEvents, ...hostEvents]),
|
|
1551
1855
|
relationMutations,
|
|
1552
1856
|
commandUses,
|
|
1553
1857
|
ranSystems,
|
|
1858
|
+
requirementViolations: [...requirementViolations.values()],
|
|
1554
1859
|
hasResource: (descriptor) => resources.has(descriptor.key),
|
|
1555
1860
|
schedules: [...scheduleNames].map(([schedule, name]) => [name, schedule])
|
|
1556
1861
|
}),
|
|
@@ -1621,6 +1926,7 @@ export const make = (options) => {
|
|
|
1621
1926
|
resources: validated.value,
|
|
1622
1927
|
machines: options.machines,
|
|
1623
1928
|
machineDefinitions: options.machineDefinitions ?? [],
|
|
1929
|
+
scopeDefinitions: options.scopeDefinitions ?? [],
|
|
1624
1930
|
debug: options.debug
|
|
1625
1931
|
});
|
|
1626
1932
|
return (fallible ? Result.success(runtime) : runtime);
|