@typeonce/ritmo 0.8.0 → 0.9.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 +3 -2
- package/dist/Clock.d.ts +19 -4
- package/dist/Clock.d.ts.map +1 -1
- package/dist/Clock.js +18 -2
- package/dist/Clock.js.map +1 -1
- package/dist/Command.d.ts +101 -45
- package/dist/Command.d.ts.map +1 -1
- package/dist/Command.js +22 -4
- package/dist/Command.js.map +1 -1
- package/dist/Debug.d.ts +66 -8
- package/dist/Debug.d.ts.map +1 -1
- package/dist/Descriptor.d.ts +27 -9
- package/dist/Descriptor.d.ts.map +1 -1
- package/dist/Descriptor.js +13 -9
- package/dist/Descriptor.js.map +1 -1
- package/dist/Entity.d.ts +15 -8
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js.map +1 -1
- package/dist/FixedLoop.d.ts +106 -0
- package/dist/FixedLoop.d.ts.map +1 -0
- package/dist/FixedLoop.js +97 -0
- package/dist/FixedLoop.js.map +1 -0
- package/dist/HostClock.d.ts +1 -1
- package/dist/HostClock.d.ts.map +1 -1
- package/dist/HostClock.js +2 -1
- package/dist/HostClock.js.map +1 -1
- package/dist/Machine.d.ts +4 -1
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +4 -1
- package/dist/Machine.js.map +1 -1
- package/dist/Query.d.ts +52 -10
- package/dist/Query.d.ts.map +1 -1
- package/dist/Query.js +2 -1
- package/dist/Query.js.map +1 -1
- package/dist/Relation.d.ts +5 -14
- package/dist/Relation.d.ts.map +1 -1
- package/dist/Relation.js +0 -32
- package/dist/Relation.js.map +1 -1
- package/dist/Runtime.d.ts +58 -15
- package/dist/Runtime.d.ts.map +1 -1
- package/dist/Runtime.js +288 -70
- package/dist/Runtime.js.map +1 -1
- package/dist/Schedule.d.ts +24 -15
- package/dist/Schedule.d.ts.map +1 -1
- package/dist/Schedule.js +2 -1
- package/dist/Schedule.js.map +1 -1
- package/dist/Schema.d.ts +98 -29
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +4 -4
- package/dist/Schema.js.map +1 -1
- package/dist/Snapshot.d.ts +76 -1
- package/dist/Snapshot.d.ts.map +1 -1
- package/dist/Snapshot.js +42 -1
- package/dist/Snapshot.js.map +1 -1
- package/dist/System.d.ts +68 -24
- package/dist/System.d.ts.map +1 -1
- package/dist/System.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/cells.d.ts +2 -0
- package/dist/internal/cells.d.ts.map +1 -1
- package/dist/internal/cells.js +34 -8
- package/dist/internal/cells.js.map +1 -1
- package/dist/internal/debug.d.ts +3 -0
- package/dist/internal/debug.d.ts.map +1 -1
- package/dist/internal/debug.js +83 -15
- package/dist/internal/debug.js.map +1 -1
- package/dist/internal/fragments.d.ts +2 -6
- package/dist/internal/fragments.d.ts.map +1 -1
- package/dist/internal/fragments.js +25 -2
- package/dist/internal/fragments.js.map +1 -1
- package/dist/internal/game.d.ts +2 -0
- package/dist/internal/game.d.ts.map +1 -1
- package/dist/internal/game.js +16 -3
- package/dist/internal/game.js.map +1 -1
- package/dist/internal/queries.d.ts.map +1 -1
- package/dist/internal/queries.js +27 -18
- package/dist/internal/queries.js.map +1 -1
- package/dist/internal/relations.d.ts +10 -0
- package/dist/internal/relations.d.ts.map +1 -0
- package/dist/internal/relations.js +32 -0
- package/dist/internal/relations.js.map +1 -0
- package/dist/internal/streams.d.ts +2 -0
- package/dist/internal/streams.d.ts.map +1 -1
- package/dist/internal/streams.js +10 -0
- package/dist/internal/streams.js.map +1 -1
- package/dist/internal/world.d.ts +7 -1
- package/dist/internal/world.d.ts.map +1 -1
- package/dist/internal/world.js +53 -3
- package/dist/internal/world.js.map +1 -1
- package/package.json +1 -1
- package/src/Clock.ts +29 -4
- package/src/Command.ts +161 -67
- package/src/Debug.ts +61 -9
- package/src/Descriptor.ts +43 -17
- package/src/Entity.ts +16 -9
- package/src/FixedLoop.ts +218 -0
- package/src/HostClock.ts +2 -1
- package/src/Machine.ts +4 -1
- package/src/Query.ts +47 -10
- package/src/Relation.ts +5 -67
- package/src/Runtime.ts +345 -85
- package/src/Schedule.ts +31 -19
- package/src/Schema.ts +118 -35
- package/src/Snapshot.ts +90 -2
- package/src/System.ts +76 -23
- package/src/index.ts +4 -0
- package/src/internal/cells.ts +42 -10
- package/src/internal/debug.ts +84 -15
- package/src/internal/fragments.ts +23 -3
- package/src/internal/game.ts +19 -3
- package/src/internal/queries.ts +28 -2
- package/src/internal/relations.ts +52 -0
- package/src/internal/streams.ts +11 -0
- package/src/internal/world.ts +60 -4
package/dist/Runtime.js
CHANGED
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
* Game.Runtime.service(Random, { next: Math.random })
|
|
24
24
|
* )
|
|
25
25
|
*
|
|
26
|
-
* // Viewport is a constructed resource, so
|
|
26
|
+
* // make always returns a Result. Viewport is a constructed resource, so
|
|
27
|
+
* // validating it can fail and the Result must be checked.
|
|
27
28
|
* const runtime = Game.Runtime.make({
|
|
28
29
|
* services,
|
|
29
30
|
* resources: {
|
|
@@ -149,7 +150,7 @@ const collectConstructedRegistryValues = (registry, provided) => {
|
|
|
149
150
|
* resources: {
|
|
150
151
|
* score: 0
|
|
151
152
|
* }
|
|
152
|
-
* })
|
|
153
|
+
* }).value
|
|
153
154
|
* ```
|
|
154
155
|
*/
|
|
155
156
|
/**
|
|
@@ -159,6 +160,25 @@ const collectConstructedRegistryValues = (registry, provided) => {
|
|
|
159
160
|
* `lagged() === true` (or a `missed` read in the debug trace).
|
|
160
161
|
*/
|
|
161
162
|
export const streamCapacity = 65_536;
|
|
163
|
+
/**
|
|
164
|
+
* Brings a parsed save to `migrations.latest`: runs every step after its
|
|
165
|
+
* game version, checking the structure after each.
|
|
166
|
+
*/
|
|
167
|
+
const migrateSave = (save, saves) => {
|
|
168
|
+
const latest = saves?.latest ?? 0;
|
|
169
|
+
if (save.gameVersion > latest)
|
|
170
|
+
return Result.failure({ _tag: "NewerGameVersion", gameVersion: save.gameVersion, latest });
|
|
171
|
+
let current = save;
|
|
172
|
+
for (const step of saves?.steps ?? []) {
|
|
173
|
+
if (step.to <= current.gameVersion)
|
|
174
|
+
continue;
|
|
175
|
+
const next = Snapshot.parse({ ...step.migrate(current), gameVersion: step.to });
|
|
176
|
+
if (!next.ok)
|
|
177
|
+
return next;
|
|
178
|
+
current = next.value;
|
|
179
|
+
}
|
|
180
|
+
return Result.success(current);
|
|
181
|
+
};
|
|
162
182
|
const makeValidatedRuntime = (options) => {
|
|
163
183
|
const world = makeWorld(options.schema, streamCapacity);
|
|
164
184
|
const queries = makeQueryEngine(world);
|
|
@@ -171,7 +191,10 @@ const makeValidatedRuntime = (options) => {
|
|
|
171
191
|
* Machine-keyed committed, pending, and previous values.
|
|
172
192
|
*/
|
|
173
193
|
const currentMachines = new Map();
|
|
194
|
+
/** Queued next values; with `debug`, `by` is the system that queued it, for the `next-state-replaced` lint. */
|
|
174
195
|
const pendingMachines = new Map();
|
|
196
|
+
/** Requests dropped because their sub-state stopped existing: skipped by the marker that queued them. */
|
|
197
|
+
const discardedPending = new WeakSet();
|
|
175
198
|
const previousMachines = new Map();
|
|
176
199
|
/**
|
|
177
200
|
* Machine keys changed by transitions applied during the current schedule run.
|
|
@@ -313,6 +336,10 @@ const makeValidatedRuntime = (options) => {
|
|
|
313
336
|
*/
|
|
314
337
|
const commandUses = new Map();
|
|
315
338
|
const ranSystems = new Set();
|
|
339
|
+
/** With `debug`, the system running now: who queued a next state. */
|
|
340
|
+
let runningSystem = "(host)";
|
|
341
|
+
/** With `debug`, next states one system queued and another replaced before the marker. */
|
|
342
|
+
const nextStateReplacements = new Map();
|
|
316
343
|
const noteCommand = (kind, name) => {
|
|
317
344
|
let uses = commandUses.get(commandOrigin);
|
|
318
345
|
if (uses === undefined) {
|
|
@@ -348,6 +375,41 @@ const makeValidatedRuntime = (options) => {
|
|
|
348
375
|
requirementViolations.set(key, { system: commandOrigin, command, component, missing, entity, count: (seen?.count ?? 0) + 1 });
|
|
349
376
|
}
|
|
350
377
|
};
|
|
378
|
+
const streamLosses = new Map();
|
|
379
|
+
const watchStreamLoss = (view, system, stream, paused) => {
|
|
380
|
+
const key = `${system}/${stream}`;
|
|
381
|
+
const entry = () => {
|
|
382
|
+
let found = streamLosses.get(key);
|
|
383
|
+
if (found === undefined) {
|
|
384
|
+
found = { system, stream, lostRuns: 0, checksLagged: false, paused };
|
|
385
|
+
streamLosses.set(key, found);
|
|
386
|
+
}
|
|
387
|
+
return found;
|
|
388
|
+
};
|
|
389
|
+
return {
|
|
390
|
+
...view,
|
|
391
|
+
all: () => {
|
|
392
|
+
const status = entry();
|
|
393
|
+
if (view.lagged())
|
|
394
|
+
status.lostRuns += 1;
|
|
395
|
+
return view.all();
|
|
396
|
+
},
|
|
397
|
+
lagged: () => {
|
|
398
|
+
entry().checksLagged = true;
|
|
399
|
+
return view.lagged();
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
};
|
|
403
|
+
const staleTargets = new Map();
|
|
404
|
+
const reportStaleTarget = (command, entity) => {
|
|
405
|
+
if (tracing)
|
|
406
|
+
recordEffect({ kind: "staleTarget", entity, command });
|
|
407
|
+
if (debugEnabled) {
|
|
408
|
+
const key = `${commandOrigin}/${command}`;
|
|
409
|
+
const seen = staleTargets.get(key);
|
|
410
|
+
staleTargets.set(key, { system: commandOrigin, command, entity, count: (seen?.count ?? 0) + 1 });
|
|
411
|
+
}
|
|
412
|
+
};
|
|
351
413
|
const holds = (id) => {
|
|
352
414
|
const record = world.records.get(id);
|
|
353
415
|
return (descriptor) => record !== undefined && world.has(record, world.ordinalOf(descriptor));
|
|
@@ -431,6 +493,9 @@ const makeValidatedRuntime = (options) => {
|
|
|
431
493
|
world.writeComponent(id, descriptor, value);
|
|
432
494
|
}
|
|
433
495
|
},
|
|
496
|
+
reportStaleTarget(command, id) {
|
|
497
|
+
reportStaleTarget(command, id);
|
|
498
|
+
},
|
|
434
499
|
tryRelate(id, relation, target) {
|
|
435
500
|
if (debugEnabled) {
|
|
436
501
|
noteRelationMutation(relation);
|
|
@@ -597,6 +662,13 @@ const makeValidatedRuntime = (options) => {
|
|
|
597
662
|
const resourceOriginals = new Map();
|
|
598
663
|
const machineOriginals = new Map();
|
|
599
664
|
const emittedEvents = new Map();
|
|
665
|
+
let phase;
|
|
666
|
+
/** Keeps a reader's position from before the phase, the first time the phase moves it. */
|
|
667
|
+
const journalReader = (reader) => {
|
|
668
|
+
if (phase !== undefined && !phase.readers.has(reader)) {
|
|
669
|
+
phase.readers.set(reader, [reader.since, reader.lastRun, reader.streamSince, reader.streamLastRun]);
|
|
670
|
+
}
|
|
671
|
+
};
|
|
600
672
|
const journalStoreWrite = (store, originals) => (key, value) => {
|
|
601
673
|
if (!originals.has(key)) {
|
|
602
674
|
originals.set(key, store.has(key) ? store.get(key) : absentValue);
|
|
@@ -622,11 +694,23 @@ const makeValidatedRuntime = (options) => {
|
|
|
622
694
|
};
|
|
623
695
|
const commitSystemTransaction = () => {
|
|
624
696
|
world.commitTransaction();
|
|
697
|
+
if (phase !== undefined) {
|
|
698
|
+
for (const [key, before] of resourceOriginals) {
|
|
699
|
+
if (!phase.resources.has(key))
|
|
700
|
+
phase.resources.set(key, before);
|
|
701
|
+
}
|
|
702
|
+
for (const [key, before] of machineOriginals) {
|
|
703
|
+
if (!phase.machines.has(key))
|
|
704
|
+
phase.machines.set(key, before);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
625
707
|
if (watchedResources.size > 0) {
|
|
626
708
|
for (const [key, before] of resourceOriginals) {
|
|
627
709
|
if (!watchedResources.has(key))
|
|
628
710
|
continue;
|
|
629
711
|
if (before === absentValue || !sameValue(before, resources.get(key))) {
|
|
712
|
+
if (phase !== undefined && !phase.resourceChangedAt.has(key))
|
|
713
|
+
phase.resourceChangedAt.set(key, resourceChangedAt.get(key));
|
|
630
714
|
resourceChangedAt.set(key, world.currentTick());
|
|
631
715
|
}
|
|
632
716
|
}
|
|
@@ -666,6 +750,59 @@ const makeValidatedRuntime = (options) => {
|
|
|
666
750
|
machineOriginals.clear();
|
|
667
751
|
emittedEvents.clear();
|
|
668
752
|
};
|
|
753
|
+
/**
|
|
754
|
+
* Runs `run` as one phase: on failure, or a thrown defect, everything the
|
|
755
|
+
* systems in it committed is undone.
|
|
756
|
+
*/
|
|
757
|
+
const runAsPhase = (run) => {
|
|
758
|
+
const opened = {
|
|
759
|
+
tick: world.currentTick(),
|
|
760
|
+
commands: pendingCommands.length,
|
|
761
|
+
origins: pendingOrigins.length,
|
|
762
|
+
resources: new Map(),
|
|
763
|
+
resourceChangedAt: new Map(),
|
|
764
|
+
machines: new Map(),
|
|
765
|
+
readers: new Map()
|
|
766
|
+
};
|
|
767
|
+
phase = opened;
|
|
768
|
+
world.beginPhase();
|
|
769
|
+
let result;
|
|
770
|
+
try {
|
|
771
|
+
result = run();
|
|
772
|
+
}
|
|
773
|
+
finally {
|
|
774
|
+
phase = undefined;
|
|
775
|
+
if (result?.ok === true) {
|
|
776
|
+
world.commitPhase();
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
world.rollbackPhase();
|
|
780
|
+
restoreStore(resources, opened.resources);
|
|
781
|
+
for (const [key, value] of opened.machines) {
|
|
782
|
+
if (value === absentValue)
|
|
783
|
+
pendingMachines.delete(key);
|
|
784
|
+
else
|
|
785
|
+
pendingMachines.set(key, value);
|
|
786
|
+
}
|
|
787
|
+
for (const [key, tick] of opened.resourceChangedAt) {
|
|
788
|
+
if (tick === undefined)
|
|
789
|
+
resourceChangedAt.delete(key);
|
|
790
|
+
else
|
|
791
|
+
resourceChangedAt.set(key, tick);
|
|
792
|
+
}
|
|
793
|
+
for (const [reader, [since, lastRun, streamSince, streamLastRun]] of opened.readers) {
|
|
794
|
+
reader.since = since;
|
|
795
|
+
reader.lastRun = lastRun;
|
|
796
|
+
reader.streamSince = streamSince;
|
|
797
|
+
reader.streamLastRun = streamLastRun;
|
|
798
|
+
}
|
|
799
|
+
events.retract(opened.tick);
|
|
800
|
+
pendingCommands.length = opened.commands;
|
|
801
|
+
pendingOrigins.length = opened.origins;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return result;
|
|
805
|
+
};
|
|
669
806
|
const makeResourceWriteView = (descriptor, store, write) => Cells.storeWrite(store, descriptor.key, write, DescriptorModule.constructorOf(descriptor));
|
|
670
807
|
const makeResourceChangedView = (descriptor, reader) => {
|
|
671
808
|
const key = descriptor.key;
|
|
@@ -747,7 +884,24 @@ const makeValidatedRuntime = (options) => {
|
|
|
747
884
|
});
|
|
748
885
|
}
|
|
749
886
|
journalMachine(stateMachine.key);
|
|
750
|
-
|
|
887
|
+
if (debugEnabled) {
|
|
888
|
+
const previous = pendingMachines.get(stateMachine.key);
|
|
889
|
+
// A set from a transition schedule waits for the next marker: it replaces nothing queued for this one.
|
|
890
|
+
if (previous?.by !== undefined && previous.by !== runningSystem && previous.value !== value && activeTransitions.size === 0) {
|
|
891
|
+
const key = `${stateMachine.name}/${previous.by}/${runningSystem}/${String(previous.value)}/${String(value)}`;
|
|
892
|
+
const seen = nextStateReplacements.get(key);
|
|
893
|
+
nextStateReplacements.set(key, {
|
|
894
|
+
machine: stateMachine.name,
|
|
895
|
+
replaced: { system: previous.by, value: previous.value },
|
|
896
|
+
by: { system: runningSystem, value: value },
|
|
897
|
+
count: (seen?.count ?? 0) + 1
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
pendingMachines.set(stateMachine.key, { value, skipIfSame, by: runningSystem });
|
|
901
|
+
}
|
|
902
|
+
else {
|
|
903
|
+
pendingMachines.set(stateMachine.key, { value, skipIfSame });
|
|
904
|
+
}
|
|
751
905
|
return (within === undefined ? undefined : succeeded);
|
|
752
906
|
};
|
|
753
907
|
const makeNextMachineWriteView = (stateMachine) => ({
|
|
@@ -804,8 +958,12 @@ const makeValidatedRuntime = (options) => {
|
|
|
804
958
|
const makeContext = (system, reader, queue) => {
|
|
805
959
|
const spec = system.spec;
|
|
806
960
|
const lookup = makeLookup(reader);
|
|
961
|
+
const paused = spec.pauseWhen.length > 0;
|
|
962
|
+
/** On a debug runtime, records reads after a loss and calls of `lagged()`, for the `stream-loss-unread` lint. */
|
|
963
|
+
const watch = (view, stream) => debugEnabled ? watchStreamLoss(view, system.name, stream, paused) : view;
|
|
807
964
|
return {
|
|
808
965
|
local: spec.local?.(),
|
|
966
|
+
// A filtered query hands out the same object; its type hides the lookups.
|
|
809
967
|
queries: mapRecord(spec.queries, (query) => makeQueryHandle(query, reader, lookup)),
|
|
810
968
|
lookup,
|
|
811
969
|
resources: mapRecord(spec.resources, (access) => access.mode === "read"
|
|
@@ -814,15 +972,15 @@ const makeValidatedRuntime = (options) => {
|
|
|
814
972
|
? makeResourceChangedView(access.descriptor, reader)
|
|
815
973
|
: makeResourceWriteView(access.descriptor, resources, writeResource)),
|
|
816
974
|
events: mapRecord(spec.events, (access) => access.mode === "read"
|
|
817
|
-
? makeEventReadView(access.descriptor.key, reader)
|
|
975
|
+
? watch(makeEventReadView(access.descriptor.key, reader), `events ${access.descriptor.name}`)
|
|
818
976
|
: makeEventWriteView(access.descriptor.key)),
|
|
819
977
|
machines: mapRecord(spec.machines, (access) => makeMachineReadView(access.machine)),
|
|
820
978
|
nextMachines: mapRecord(spec.nextMachines, (access) => makeNextMachineWriteView(access.machine)),
|
|
821
|
-
transitionEvents: mapRecord(spec.transitionEvents, (access) => makeTransitionEventReadView(access.machine, reader)),
|
|
979
|
+
transitionEvents: mapRecord(spec.transitionEvents, (access) => watch(makeTransitionEventReadView(access.machine, reader), `transition events ${access.machine.name}`)),
|
|
822
980
|
transitions: mapRecord(spec.transitions, (access) => makeTransitionReadView(access.machine)),
|
|
823
|
-
removed: mapRecord(spec.removed, (access) => makeRemovedReadView(access.descriptor, reader)),
|
|
824
|
-
despawned: mapRecord(spec.despawned, () => makeDespawnedReadView(reader)),
|
|
825
|
-
relationFailures: mapRecord(spec.relationFailures, (access) => makeRelationFailureReadView(access.relation, reader)),
|
|
981
|
+
removed: mapRecord(spec.removed, (access) => watch(makeRemovedReadView(access.descriptor, reader), `removed ${access.descriptor.name}`)),
|
|
982
|
+
despawned: mapRecord(spec.despawned, () => watch(makeDespawnedReadView(reader), "despawned")),
|
|
983
|
+
relationFailures: mapRecord(spec.relationFailures, (access) => watch(makeRelationFailureReadView(access.relation, reader), `relation failures ${access.relation.name}`)),
|
|
826
984
|
services: mapRecord(spec.services, (access) => providedServices[access.descriptor.name]),
|
|
827
985
|
// The queue serves every declared permission set; the spec's types narrow it.
|
|
828
986
|
commands: queue
|
|
@@ -985,6 +1143,7 @@ const makeValidatedRuntime = (options) => {
|
|
|
985
1143
|
const skipped = slotOf(system, true);
|
|
986
1144
|
if (tracing)
|
|
987
1145
|
traceSkipped(system, condition, skipped.reader, false);
|
|
1146
|
+
journalReader(skipped.reader);
|
|
988
1147
|
skipped.reader.streamLastRun = world.currentTick();
|
|
989
1148
|
return succeeded;
|
|
990
1149
|
}
|
|
@@ -1001,8 +1160,11 @@ const makeValidatedRuntime = (options) => {
|
|
|
1001
1160
|
}
|
|
1002
1161
|
}
|
|
1003
1162
|
const { context, commands, reader } = slotOf(system, true);
|
|
1004
|
-
if (debugEnabled)
|
|
1163
|
+
if (debugEnabled) {
|
|
1005
1164
|
ranSystems.add(system.name);
|
|
1165
|
+
runningSystem = system.name;
|
|
1166
|
+
}
|
|
1167
|
+
journalReader(reader);
|
|
1006
1168
|
// Changes stamped after the previous completed run are visible to this run.
|
|
1007
1169
|
reader.since = reader.lastRun;
|
|
1008
1170
|
reader.streamSince = reader.streamLastRun;
|
|
@@ -1168,11 +1330,19 @@ const makeValidatedRuntime = (options) => {
|
|
|
1168
1330
|
- (machineDefinitionOrder.get(rightKey) ?? Number.MAX_SAFE_INTEGER));
|
|
1169
1331
|
const schedules = bundle?.entries ?? [];
|
|
1170
1332
|
for (const [machineKey, pending] of scheduledTransitions) {
|
|
1171
|
-
if (
|
|
1333
|
+
if (discardedPending.has(pending)) {
|
|
1172
1334
|
// Discarded earlier in this marker: its parent left the state it exists in.
|
|
1173
1335
|
continue;
|
|
1174
1336
|
}
|
|
1175
|
-
|
|
1337
|
+
// A transition schedule earlier in this marker may have queued a newer
|
|
1338
|
+
// value for this machine: that one waits for the next marker.
|
|
1339
|
+
if (pendingMachines.get(machineKey) === pending)
|
|
1340
|
+
pendingMachines.delete(machineKey);
|
|
1341
|
+
/** Keeps this request queued after a failure, unless a newer one replaced it. */
|
|
1342
|
+
const requeue = () => {
|
|
1343
|
+
if (!pendingMachines.has(machineKey))
|
|
1344
|
+
pendingMachines.set(machineKey, pending);
|
|
1345
|
+
};
|
|
1176
1346
|
const current = currentMachines.get(machineKey);
|
|
1177
1347
|
if (current === undefined) {
|
|
1178
1348
|
continue;
|
|
@@ -1189,48 +1359,46 @@ const makeValidatedRuntime = (options) => {
|
|
|
1189
1359
|
// Sub-states that exist in the state being left exit first, deepest
|
|
1190
1360
|
// first: their exit schedules run before the parent's, while they
|
|
1191
1361
|
// still have their values, and they lose them at the parent's commit.
|
|
1362
|
+
// The exit phase is one transaction: a failure undoes all of it, and
|
|
1363
|
+
// the transition stays queued for a later attempt.
|
|
1192
1364
|
const leaving = activeSubStatesIn(machineKey, snapshot.from);
|
|
1193
|
-
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
if (tracing)
|
|
1199
|
-
traceTransition(machineKey, snapshot, "failed");
|
|
1200
|
-
return result;
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
for (const schedule of schedules) {
|
|
1204
|
-
const transition = schedule.transition;
|
|
1205
|
-
if (transition.phase === "exit" && transition.machine.key === machineKey && transition.state === snapshot.from) {
|
|
1206
|
-
const result = runTransitionSchedule(schedule, snapshot);
|
|
1207
|
-
if (!result.ok) {
|
|
1208
|
-
// The transition did not happen; keep it queued for a later attempt.
|
|
1209
|
-
pendingMachines.set(machineKey, pending);
|
|
1210
|
-
if (tracing)
|
|
1211
|
-
traceTransition(machineKey, snapshot, "failed");
|
|
1365
|
+
const exited = runAsPhase(() => {
|
|
1366
|
+
for (const sub of leaving) {
|
|
1367
|
+
const value = currentMachines.get(sub.key);
|
|
1368
|
+
const result = runPhase(sub.key, "exit", value, { from: value, to: undefined }, schedules);
|
|
1369
|
+
if (!result.ok)
|
|
1212
1370
|
return result;
|
|
1213
|
-
}
|
|
1214
1371
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
if (
|
|
1226
|
-
|
|
1227
|
-
return result;
|
|
1372
|
+
const left = runPhase(machineKey, "exit", snapshot.from, snapshot, schedules);
|
|
1373
|
+
if (!left.ok)
|
|
1374
|
+
return left;
|
|
1375
|
+
for (const schedule of schedules) {
|
|
1376
|
+
const transition = schedule.transition;
|
|
1377
|
+
if (transition.phase === "transition"
|
|
1378
|
+
&& transition.machine.key === machineKey
|
|
1379
|
+
&& transition.from === snapshot.from
|
|
1380
|
+
&& transition.to === snapshot.to) {
|
|
1381
|
+
const result = runTransitionSchedule(schedule, snapshot);
|
|
1382
|
+
if (!result.ok)
|
|
1383
|
+
return result;
|
|
1228
1384
|
}
|
|
1229
1385
|
}
|
|
1386
|
+
return succeeded;
|
|
1387
|
+
});
|
|
1388
|
+
if (!exited.ok) {
|
|
1389
|
+
requeue();
|
|
1390
|
+
if (tracing)
|
|
1391
|
+
traceTransition(machineKey, snapshot, "failed");
|
|
1392
|
+
return exited;
|
|
1230
1393
|
}
|
|
1231
1394
|
for (const sub of leaving)
|
|
1232
1395
|
commitMachine(sub.key, currentMachines.get(sub.key), undefined);
|
|
1233
1396
|
commitMachine(machineKey, current, pending.value);
|
|
1397
|
+
// Sub-states that exist in the entered state enter their value together
|
|
1398
|
+
// with the parent, parents first, before any onEnter schedule runs: a
|
|
1399
|
+
// failing onEnter cannot leave them without a value, and their scope
|
|
1400
|
+
// triggers fire before what the onEnter schedules spawn.
|
|
1401
|
+
const entered = commitSubStates(machineKey, snapshot.to);
|
|
1234
1402
|
for (const schedule of schedules) {
|
|
1235
1403
|
const transition = schedule.transition;
|
|
1236
1404
|
if (transition.phase === "enter" && transition.machine.key === machineKey && transition.state === snapshot.to) {
|
|
@@ -1242,10 +1410,9 @@ const makeValidatedRuntime = (options) => {
|
|
|
1242
1410
|
}
|
|
1243
1411
|
}
|
|
1244
1412
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
return entered;
|
|
1413
|
+
const enteredSchedules = runSubStateEnters(entered, schedules);
|
|
1414
|
+
if (!enteredSchedules.ok)
|
|
1415
|
+
return enteredSchedules;
|
|
1249
1416
|
}
|
|
1250
1417
|
return succeeded;
|
|
1251
1418
|
};
|
|
@@ -1260,6 +1427,9 @@ const makeValidatedRuntime = (options) => {
|
|
|
1260
1427
|
previousMachines.set(machineKey, from);
|
|
1261
1428
|
if (to === undefined) {
|
|
1262
1429
|
currentMachines.delete(machineKey);
|
|
1430
|
+
const discarded = pendingMachines.get(machineKey);
|
|
1431
|
+
if (discarded !== undefined)
|
|
1432
|
+
discardedPending.add(discarded);
|
|
1263
1433
|
pendingMachines.delete(machineKey);
|
|
1264
1434
|
}
|
|
1265
1435
|
else {
|
|
@@ -1296,25 +1466,29 @@ const makeValidatedRuntime = (options) => {
|
|
|
1296
1466
|
};
|
|
1297
1467
|
/**
|
|
1298
1468
|
* Gives the sub-states of `parentKey` that exist in `state` their entry
|
|
1299
|
-
* value,
|
|
1300
|
-
* sub-states.
|
|
1469
|
+
* value, and theirs, parents first. Returns what it entered, in that order.
|
|
1301
1470
|
*/
|
|
1302
|
-
const
|
|
1471
|
+
const commitSubStates = (parentKey, state) => {
|
|
1472
|
+
const entered = [];
|
|
1303
1473
|
for (const sub of subStates.get(parentKey) ?? []) {
|
|
1304
1474
|
if (sub.within.state !== state || currentMachines.has(sub.key) || !subStateEntry.has(sub.key))
|
|
1305
1475
|
continue;
|
|
1306
1476
|
const value = subStateEntry.get(sub.key);
|
|
1307
1477
|
commitMachine(sub.key, undefined, value);
|
|
1478
|
+
entered.push([sub.key, value], ...commitSubStates(sub.key, value));
|
|
1479
|
+
}
|
|
1480
|
+
return entered;
|
|
1481
|
+
};
|
|
1482
|
+
/** Runs the onEnter schedules of sub-states entered by {@link commitSubStates}, in that order. */
|
|
1483
|
+
const runSubStateEnters = (entered, schedules) => {
|
|
1484
|
+
for (const [key, value] of entered) {
|
|
1308
1485
|
const snapshot = { from: undefined, to: value };
|
|
1309
|
-
const result = runPhase(
|
|
1486
|
+
const result = runPhase(key, "enter", value, snapshot, schedules);
|
|
1310
1487
|
if (!result.ok) {
|
|
1311
1488
|
if (tracing)
|
|
1312
|
-
traceTransition(
|
|
1489
|
+
traceTransition(key, snapshot, "enterFailed");
|
|
1313
1490
|
return result;
|
|
1314
1491
|
}
|
|
1315
|
-
const nested = enterSubStates(sub.key, value, schedules);
|
|
1316
|
-
if (!nested.ok)
|
|
1317
|
-
return nested;
|
|
1318
1492
|
}
|
|
1319
1493
|
return succeeded;
|
|
1320
1494
|
};
|
|
@@ -1349,8 +1523,14 @@ const makeValidatedRuntime = (options) => {
|
|
|
1349
1523
|
}
|
|
1350
1524
|
return succeeded;
|
|
1351
1525
|
}
|
|
1352
|
-
/** Whether
|
|
1526
|
+
/** Whether `runtime.tick` is on the stack: a nested `tick` is a defect. */
|
|
1353
1527
|
let ticking = false;
|
|
1528
|
+
/**
|
|
1529
|
+
* Whether a tick is advancing its frame or running its schedules (in
|
|
1530
|
+
* `tick` and `tryTick`): `send`, `snapshot`, and `restore` are defects
|
|
1531
|
+
* meanwhile. It is cleared before `tick.end` is announced to host listeners.
|
|
1532
|
+
*/
|
|
1533
|
+
let inTick = false;
|
|
1354
1534
|
/**
|
|
1355
1535
|
* Runs one schedule. A failing system stops the schedule; systems that
|
|
1356
1536
|
* already succeeded stay committed, and their pending work stays pending.
|
|
@@ -1372,7 +1552,7 @@ const makeValidatedRuntime = (options) => {
|
|
|
1372
1552
|
/** Events declared as sent by the host (`debug.declareHostEvents`). */
|
|
1373
1553
|
let hostEvents = new Set();
|
|
1374
1554
|
const send = (descriptor, value) => {
|
|
1375
|
-
if (
|
|
1555
|
+
if (inTick)
|
|
1376
1556
|
throw new Error(`runtime.send(${descriptor.name}) was called while a schedule is running: emit from a system instead`);
|
|
1377
1557
|
if (!eventsByKey.has(descriptor.key))
|
|
1378
1558
|
throw new Error(`runtime.send: ${descriptor.name} is not an event of this runtime's schema`);
|
|
@@ -1446,6 +1626,7 @@ const makeValidatedRuntime = (options) => {
|
|
|
1446
1626
|
if (debugEnabled)
|
|
1447
1627
|
reportUnread("next-tick");
|
|
1448
1628
|
ticking = true;
|
|
1629
|
+
inTick = true;
|
|
1449
1630
|
try {
|
|
1450
1631
|
advanceFrame();
|
|
1451
1632
|
return watchResult(observedTick(schedules, () => {
|
|
@@ -1459,6 +1640,7 @@ const makeValidatedRuntime = (options) => {
|
|
|
1459
1640
|
}
|
|
1460
1641
|
finally {
|
|
1461
1642
|
ticking = false;
|
|
1643
|
+
inTick = false;
|
|
1462
1644
|
}
|
|
1463
1645
|
};
|
|
1464
1646
|
/** Runs one tick's schedules, telling `observeHost` listeners when it starts and how it ends. */
|
|
@@ -1471,9 +1653,11 @@ const makeValidatedRuntime = (options) => {
|
|
|
1471
1653
|
result = run();
|
|
1472
1654
|
}
|
|
1473
1655
|
catch (defect) {
|
|
1656
|
+
inTick = false;
|
|
1474
1657
|
emitHost({ type: "tick.end", schedules, frame: frameCount, outcome: "defect", error: defect });
|
|
1475
1658
|
throw defect;
|
|
1476
1659
|
}
|
|
1660
|
+
inTick = false;
|
|
1477
1661
|
emitHost(result.ok
|
|
1478
1662
|
? { type: "tick.end", schedules, frame: frameCount, outcome: "ok" }
|
|
1479
1663
|
: { type: "tick.end", schedules, frame: frameCount, outcome: "failed", error: result.error });
|
|
@@ -1506,8 +1690,16 @@ const makeValidatedRuntime = (options) => {
|
|
|
1506
1690
|
if (issues.length > 0) {
|
|
1507
1691
|
return watchResult(Result.failure({ kind: "TickOrderViolation", issues }), [schedule]);
|
|
1508
1692
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1693
|
+
if (inTick)
|
|
1694
|
+
throw new Error("runtime.tryTick cannot run inside another tick");
|
|
1695
|
+
inTick = true;
|
|
1696
|
+
try {
|
|
1697
|
+
advanceFrame();
|
|
1698
|
+
return watchResult(observedTick([schedule], () => runScheduleUnsafe(schedule)), [schedule]);
|
|
1699
|
+
}
|
|
1700
|
+
finally {
|
|
1701
|
+
inTick = false;
|
|
1702
|
+
}
|
|
1511
1703
|
};
|
|
1512
1704
|
/**
|
|
1513
1705
|
* Evaluates a declared read-only projection without running a schedule.
|
|
@@ -1535,6 +1727,8 @@ const makeValidatedRuntime = (options) => {
|
|
|
1535
1727
|
for (const provision of machineEntries)
|
|
1536
1728
|
machinesByName.set(provision.machine.name, provision.machine);
|
|
1537
1729
|
const snapshot = () => {
|
|
1730
|
+
if (inTick)
|
|
1731
|
+
throw new Error("runtime.snapshot was called while a schedule is running: it would capture writes a failing system rolls back; take it between ticks");
|
|
1538
1732
|
const savedResources = {};
|
|
1539
1733
|
for (const [name, descriptor] of resourcesByName) {
|
|
1540
1734
|
if (!DescriptorModule.isTransient(descriptor) && resources.has(descriptor.key)) {
|
|
@@ -1555,6 +1749,7 @@ const makeValidatedRuntime = (options) => {
|
|
|
1555
1749
|
}
|
|
1556
1750
|
return {
|
|
1557
1751
|
version: Snapshot.version,
|
|
1752
|
+
gameVersion: options.saves?.latest ?? 0,
|
|
1558
1753
|
nextEntity: world.nextEntityValue(),
|
|
1559
1754
|
entities: world.exportEntities(DescriptorModule.isTransient),
|
|
1560
1755
|
relations: world.exportRelations(),
|
|
@@ -1570,7 +1765,10 @@ const makeValidatedRuntime = (options) => {
|
|
|
1570
1765
|
const parsed = Snapshot.parse(data);
|
|
1571
1766
|
if (!parsed.ok)
|
|
1572
1767
|
return parsed;
|
|
1573
|
-
const
|
|
1768
|
+
const migrated = migrateSave(parsed.value, options.saves);
|
|
1769
|
+
if (!migrated.ok)
|
|
1770
|
+
return migrated;
|
|
1771
|
+
const saved = migrated.value;
|
|
1574
1772
|
// Validate everything before touching the world.
|
|
1575
1773
|
const ids = new Set();
|
|
1576
1774
|
const entities = [];
|
|
@@ -1669,6 +1867,8 @@ const makeValidatedRuntime = (options) => {
|
|
|
1669
1867
|
const scoped = new Set();
|
|
1670
1868
|
const memberships = [];
|
|
1671
1869
|
for (const [name, members] of Object.entries(saved.scopes)) {
|
|
1870
|
+
if (!options.scopeDefinitions.some((scope) => scope.name === name))
|
|
1871
|
+
return Result.failure({ _tag: "UnknownScope", name });
|
|
1672
1872
|
for (const id of members) {
|
|
1673
1873
|
if (!ids.has(id) || scoped.has(id))
|
|
1674
1874
|
return Result.failure({ _tag: "InvalidScope", name, entityId: id });
|
|
@@ -1708,6 +1908,8 @@ const makeValidatedRuntime = (options) => {
|
|
|
1708
1908
|
return succeeded;
|
|
1709
1909
|
};
|
|
1710
1910
|
const tracedRestore = (data) => {
|
|
1911
|
+
if (inTick)
|
|
1912
|
+
throw new Error("runtime.restore was called while a schedule is running: the running systems' queued commands would land on the restored world; restore between ticks");
|
|
1711
1913
|
const result = restore(data);
|
|
1712
1914
|
if (tracing)
|
|
1713
1915
|
emit({ type: "restore", frame: frameCount, tick: world.currentTick(), ok: result.ok });
|
|
@@ -1856,6 +2058,9 @@ const makeValidatedRuntime = (options) => {
|
|
|
1856
2058
|
commandUses,
|
|
1857
2059
|
ranSystems,
|
|
1858
2060
|
requirementViolations: [...requirementViolations.values()],
|
|
2061
|
+
nextStateReplacements: [...nextStateReplacements.values()],
|
|
2062
|
+
staleTargets: [...staleTargets.values()],
|
|
2063
|
+
streamLosses: [...streamLosses.values()],
|
|
1859
2064
|
hasResource: (descriptor) => resources.has(descriptor.key),
|
|
1860
2065
|
schedules: [...scheduleNames].map(([schedule, name]) => [name, schedule])
|
|
1861
2066
|
}),
|
|
@@ -1895,8 +2100,9 @@ const makeValidatedRuntime = (options) => {
|
|
|
1895
2100
|
* Creates a runtime for a fully built schema and a set of external services.
|
|
1896
2101
|
*
|
|
1897
2102
|
* Resources owned by constructed descriptors take raw input and are validated
|
|
1898
|
-
* here
|
|
1899
|
-
*
|
|
2103
|
+
* here. The runtime always comes back as a `Result`: typed `Success` when
|
|
2104
|
+
* nothing provided can fail (read `.value`), and a `Result` to check when a
|
|
2105
|
+
* constructed resource is provided.
|
|
1900
2106
|
*
|
|
1901
2107
|
* The runtime does not own the outer frame loop. It only owns ECS state plus
|
|
1902
2108
|
* the host-provided services that systems are allowed to depend on.
|
|
@@ -1909,13 +2115,10 @@ const makeValidatedRuntime = (options) => {
|
|
|
1909
2115
|
* ),
|
|
1910
2116
|
* resources: { Score: 0 },
|
|
1911
2117
|
* machines: Game.Runtime.machines(Game.Runtime.machine(Flow, "Menu"))
|
|
1912
|
-
* })
|
|
2118
|
+
* }).value
|
|
1913
2119
|
* ```
|
|
1914
2120
|
*/
|
|
1915
2121
|
export const make = (options) => {
|
|
1916
|
-
const fallible = Object.entries(options.schema.resources).some(([key, descriptor]) => options.resources !== undefined
|
|
1917
|
-
&& options.resources[key] !== undefined
|
|
1918
|
-
&& DescriptorModule.hasConstructor(descriptor));
|
|
1919
2122
|
const validated = collectConstructedRegistryValues(options.schema.resources, options.resources);
|
|
1920
2123
|
if (!validated.ok) {
|
|
1921
2124
|
return Result.failure({ resources: validated.error });
|
|
@@ -1927,9 +2130,18 @@ export const make = (options) => {
|
|
|
1927
2130
|
machines: options.machines,
|
|
1928
2131
|
machineDefinitions: options.machineDefinitions ?? [],
|
|
1929
2132
|
scopeDefinitions: options.scopeDefinitions ?? [],
|
|
1930
|
-
debug: options.debug
|
|
2133
|
+
debug: options.debug,
|
|
2134
|
+
saves: options.saves
|
|
1931
2135
|
});
|
|
1932
|
-
return
|
|
2136
|
+
return Result.success(runtime);
|
|
2137
|
+
};
|
|
2138
|
+
const rejectRepeated = (names, kind) => {
|
|
2139
|
+
const seen = new Set();
|
|
2140
|
+
for (const name of names) {
|
|
2141
|
+
if (seen.has(name))
|
|
2142
|
+
throw new Error(`${name} is provided twice: keep one Game.Runtime.${kind}(...) for it`);
|
|
2143
|
+
seen.add(name);
|
|
2144
|
+
}
|
|
1933
2145
|
};
|
|
1934
2146
|
/**
|
|
1935
2147
|
* Builds the descriptor-backed runtime service environment.
|
|
@@ -1940,6 +2152,8 @@ export const make = (options) => {
|
|
|
1940
2152
|
*
|
|
1941
2153
|
* The helper keeps the runtime map keyed by service descriptors instead of
|
|
1942
2154
|
* ad hoc strings, so the provision site cannot drift from the declaration site.
|
|
2155
|
+
* Each service is provided once: a second provision of the same one does not
|
|
2156
|
+
* compile.
|
|
1943
2157
|
*
|
|
1944
2158
|
* @example
|
|
1945
2159
|
* ```ts
|
|
@@ -1951,6 +2165,7 @@ export const make = (options) => {
|
|
|
1951
2165
|
* ```
|
|
1952
2166
|
*/
|
|
1953
2167
|
export const services = (...entries) => {
|
|
2168
|
+
rejectRepeated(entries.map((entry) => entry.descriptor.name), "service");
|
|
1954
2169
|
const provided = {};
|
|
1955
2170
|
for (const { descriptor, implementation } of entries) {
|
|
1956
2171
|
provided[descriptor.name] = implementation;
|
|
@@ -1973,9 +2188,12 @@ export const service = (descriptor, implementation) => ({
|
|
|
1973
2188
|
* Builds the machine initialization environment from machine definitions.
|
|
1974
2189
|
*
|
|
1975
2190
|
* Use this when gameplay phases need a known committed starting state before
|
|
1976
|
-
* any schedule runs, for example `"Boot"`, `"Menu"`, or `"Playing"`.
|
|
2191
|
+
* any schedule runs, for example `"Boot"`, `"Menu"`, or `"Playing"`. Each
|
|
2192
|
+
* machine is provided once: a second provision of the same one does not
|
|
2193
|
+
* compile.
|
|
1977
2194
|
*/
|
|
1978
2195
|
export const machines = (...entries) => {
|
|
2196
|
+
rejectRepeated(entries.map((entry) => entry.machine.name), "machine");
|
|
1979
2197
|
const provided = {};
|
|
1980
2198
|
for (const { machine, initial } of entries) {
|
|
1981
2199
|
provided[machine.name] = initial;
|