@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/src/Runtime.ts
CHANGED
|
@@ -63,8 +63,10 @@ import type { Descriptor } from "./Descriptor.ts"
|
|
|
63
63
|
import type * as Entity from "./Entity.ts"
|
|
64
64
|
import type * as Inspector from "./Inspector.ts"
|
|
65
65
|
import * as Cells from "./internal/cells.ts"
|
|
66
|
+
import { sameValue } from "./internal/equal.ts"
|
|
66
67
|
import * as DebugInternal from "./internal/debug.ts"
|
|
67
68
|
import { makeQueryEngine } from "./internal/queries.ts"
|
|
69
|
+
import { requirementIndex } from "./internal/requirements.ts"
|
|
68
70
|
import * as Streams from "./internal/streams.ts"
|
|
69
71
|
import { ABSENT, makeWorld } from "./internal/world.ts"
|
|
70
72
|
import type { WorldHooks } from "./internal/world.ts"
|
|
@@ -83,6 +85,7 @@ import type { Registry, Schema } from "./Schema.ts"
|
|
|
83
85
|
import type {
|
|
84
86
|
DespawnedReadView,
|
|
85
87
|
EventReadView,
|
|
88
|
+
ResourceChangedView,
|
|
86
89
|
EventWriteView,
|
|
87
90
|
LookupApi,
|
|
88
91
|
QueryHandle,
|
|
@@ -705,6 +708,7 @@ const makeValidatedRuntime = <
|
|
|
705
708
|
readonly resources: Partial<Record<string, unknown>>
|
|
706
709
|
readonly machines: RuntimeMachines<any> | undefined
|
|
707
710
|
readonly machineDefinitions: ReadonlyArray<Machine.StateMachine.Any>
|
|
711
|
+
readonly scopeDefinitions: ReadonlyArray<EntityScopeModule.EntityScope.Any>
|
|
708
712
|
readonly debug: Debug.Option | undefined
|
|
709
713
|
}): Runtime<S, Services, Resources, Root, Machines> => {
|
|
710
714
|
const world = makeWorld(options.schema, streamCapacity)
|
|
@@ -801,8 +805,48 @@ const makeValidatedRuntime = <
|
|
|
801
805
|
options.machineDefinitions.map((machine, index) => [machine.key, index] as const)
|
|
802
806
|
)
|
|
803
807
|
|
|
808
|
+
/** Machines the runtime was given an initial value for. */
|
|
809
|
+
const providedMachineKeys = new Set<symbol>()
|
|
804
810
|
for (const provision of machineEntries) {
|
|
805
811
|
currentMachines.set(provision.machine.key, provision.initial)
|
|
812
|
+
providedMachineKeys.add(provision.machine.key)
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Sub-states by parent machine key, in definition order, and the value
|
|
817
|
+
* each enters whenever its parent enters the state it exists in (its
|
|
818
|
+
* runtime-provided value). A sub-state has no current value while its
|
|
819
|
+
* parent is elsewhere, and never one when the runtime does not provide it.
|
|
820
|
+
*/
|
|
821
|
+
const subStates = new Map<symbol, Array<Machine.StateMachine.Any>>()
|
|
822
|
+
const subStateEntry = new Map<symbol, unknown>()
|
|
823
|
+
for (const machine of options.machineDefinitions) {
|
|
824
|
+
if (machine.within === undefined) continue
|
|
825
|
+
const siblings = subStates.get(machine.within.machine.key)
|
|
826
|
+
if (siblings === undefined) subStates.set(machine.within.machine.key, [machine])
|
|
827
|
+
else siblings.push(machine)
|
|
828
|
+
if (!currentMachines.has(machine.key)) continue
|
|
829
|
+
subStateEntry.set(machine.key, currentMachines.get(machine.key))
|
|
830
|
+
}
|
|
831
|
+
// Definition order puts a parent before its sub-states, so a sub-state of
|
|
832
|
+
// an inactive sub-state is cleared too.
|
|
833
|
+
for (const machine of options.machineDefinitions) {
|
|
834
|
+
if (machine.within !== undefined && currentMachines.get(machine.within.machine.key) !== machine.within.state) {
|
|
835
|
+
currentMachines.delete(machine.key)
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Scope despawn triggers by machine key, in scope definition order
|
|
841
|
+
* (`Game.EntityScope(name, { despawnOnExit, despawnOnEnter })`).
|
|
842
|
+
*/
|
|
843
|
+
const scopeTriggers = new Map<symbol, Array<{ readonly trigger: EntityScopeModule.Trigger; readonly scope: EntityScopeModule.EntityScope.Any }>>()
|
|
844
|
+
for (const scope of options.scopeDefinitions) {
|
|
845
|
+
for (const trigger of scope.despawnOn) {
|
|
846
|
+
const triggers = scopeTriggers.get(trigger.machine.key)
|
|
847
|
+
if (triggers === undefined) scopeTriggers.set(trigger.machine.key, [{ trigger, scope }])
|
|
848
|
+
else triggers.push({ trigger, scope })
|
|
849
|
+
}
|
|
806
850
|
}
|
|
807
851
|
|
|
808
852
|
const appendRelationFailure = (
|
|
@@ -859,13 +903,63 @@ const makeValidatedRuntime = <
|
|
|
859
903
|
systems.add(commandOrigin)
|
|
860
904
|
}
|
|
861
905
|
|
|
906
|
+
/**
|
|
907
|
+
* `Schema.requires`, checked when commands are applied. Types reject a
|
|
908
|
+
* breaking command when they can see it; this catches the rest: a query
|
|
909
|
+
* proof gone stale within one flush (another system's command applied in
|
|
910
|
+
* between), erased types, and raw ids. A breaking command is skipped
|
|
911
|
+
* whole, traced, and reported by the `requirement-violated` lint.
|
|
912
|
+
*/
|
|
913
|
+
const requirements = requirementIndex(options.schema)
|
|
914
|
+
/** With `debug`, skipped commands by system, command, and requirement, for the lint. */
|
|
915
|
+
const requirementViolations = new Map<string, Debug.RequirementViolation>()
|
|
916
|
+
const violateRequirement = (
|
|
917
|
+
command: Debug.RequirementViolation["command"],
|
|
918
|
+
entity: number,
|
|
919
|
+
[component, missing]: readonly [string, string]
|
|
920
|
+
): void => {
|
|
921
|
+
if (tracing) recordEffect({ kind: "requirementViolated", entity, command, component, missing })
|
|
922
|
+
if (debugEnabled) {
|
|
923
|
+
const key = `${commandOrigin}/${command}/${component}/${missing}`
|
|
924
|
+
const seen = requirementViolations.get(key)
|
|
925
|
+
requirementViolations.set(key, { system: commandOrigin, command, component, missing, entity, count: (seen?.count ?? 0) + 1 })
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
const holds = (id: number) => {
|
|
929
|
+
const record = world.records.get(id)
|
|
930
|
+
return (descriptor: Descriptor<"component", string, any>): boolean =>
|
|
931
|
+
record !== undefined && world.has(record, world.ordinalOf(descriptor))
|
|
932
|
+
}
|
|
933
|
+
/** The first requirement `components` break, as `[requirer, missing]`. */
|
|
934
|
+
const missingIn = (
|
|
935
|
+
components: ReadonlyArray<Entity.StagedComponent>,
|
|
936
|
+
alsoHeld: (descriptor: Descriptor<"component", string, any>) => boolean
|
|
937
|
+
): readonly [string, string] | undefined => {
|
|
938
|
+
for (const [descriptor] of components) {
|
|
939
|
+
for (const required of requirements.requires.get(descriptor.name) ?? []) {
|
|
940
|
+
if (!alsoHeld(required) && !components.some(([staged]) => staged.name === required.name)) {
|
|
941
|
+
return [descriptor.name, required.name]
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return undefined
|
|
946
|
+
}
|
|
947
|
+
|
|
862
948
|
/**
|
|
863
949
|
* World adapter used by deferred commands.
|
|
864
950
|
*/
|
|
865
951
|
const commandWorld: Command.InternalWorld<S> = {
|
|
866
952
|
spawnEntity(id, components) {
|
|
953
|
+
if (!requirements.empty) {
|
|
954
|
+
const missing = missingIn(components, () => false)
|
|
955
|
+
if (missing !== undefined) {
|
|
956
|
+
violateRequirement("spawn", id, missing)
|
|
957
|
+
return false
|
|
958
|
+
}
|
|
959
|
+
}
|
|
867
960
|
if (debugEnabled) for (const [descriptor] of components) noteCommand("spawns", descriptor.name)
|
|
868
961
|
world.spawnEntity(id, components)
|
|
962
|
+
return true
|
|
869
963
|
},
|
|
870
964
|
destroyEntity(id) {
|
|
871
965
|
if (debugEnabled) {
|
|
@@ -883,13 +977,35 @@ const makeValidatedRuntime = <
|
|
|
883
977
|
if (debugEnabled) noteCommand("despawnsScope", scope.name)
|
|
884
978
|
world.destroyEntityScope(scope.key)
|
|
885
979
|
},
|
|
886
|
-
|
|
887
|
-
if (
|
|
888
|
-
|
|
980
|
+
removeComponents(id, descriptors) {
|
|
981
|
+
if (!requirements.empty && world.records.has(id)) {
|
|
982
|
+
const held = holds(id)
|
|
983
|
+
for (const descriptor of descriptors) {
|
|
984
|
+
for (const requirer of requirements.requiredBy.get(descriptor.name) ?? []) {
|
|
985
|
+
if (held(requirer) && !descriptors.some((removed) => removed.name === requirer.name)) {
|
|
986
|
+
violateRequirement("remove", id, [requirer.name, descriptor.name])
|
|
987
|
+
return
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
for (const descriptor of descriptors) {
|
|
993
|
+
if (debugEnabled) noteCommand("removes", descriptor.name)
|
|
994
|
+
world.removeComponent(id, descriptor)
|
|
995
|
+
}
|
|
889
996
|
},
|
|
890
|
-
|
|
891
|
-
if (
|
|
892
|
-
|
|
997
|
+
insertComponents(id, entries) {
|
|
998
|
+
if (!requirements.empty && world.records.has(id)) {
|
|
999
|
+
const missing = missingIn(entries, holds(id))
|
|
1000
|
+
if (missing !== undefined) {
|
|
1001
|
+
violateRequirement("insert", id, missing)
|
|
1002
|
+
return
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
for (const [descriptor, value] of entries) {
|
|
1006
|
+
if (debugEnabled) noteCommand("inserts", descriptor.name)
|
|
1007
|
+
world.writeComponent(id, descriptor, value)
|
|
1008
|
+
}
|
|
893
1009
|
},
|
|
894
1010
|
tryRelate(id, relation, target) {
|
|
895
1011
|
if (debugEnabled) {
|
|
@@ -1091,6 +1207,15 @@ const makeValidatedRuntime = <
|
|
|
1091
1207
|
}
|
|
1092
1208
|
const writeResource = journalStoreWrite(resources, resourceOriginals)
|
|
1093
1209
|
|
|
1210
|
+
/**
|
|
1211
|
+
* Resource change ticks for `changedResource` readers. Only resources some
|
|
1212
|
+
* system reads with `changedResource` are compared at commit; a reader's
|
|
1213
|
+
* first run reports a change anyway, so writes before it registers are not
|
|
1214
|
+
* needed.
|
|
1215
|
+
*/
|
|
1216
|
+
const watchedResources = new Set<symbol>()
|
|
1217
|
+
const resourceChangedAt = new Map<symbol, number>()
|
|
1218
|
+
|
|
1094
1219
|
const journalMachine = (key: symbol): void => {
|
|
1095
1220
|
if (!machineOriginals.has(key)) {
|
|
1096
1221
|
machineOriginals.set(key, pendingMachines.get(key) ?? absentValue)
|
|
@@ -1103,6 +1228,14 @@ const makeValidatedRuntime = <
|
|
|
1103
1228
|
|
|
1104
1229
|
const commitSystemTransaction = (): void => {
|
|
1105
1230
|
world.commitTransaction()
|
|
1231
|
+
if (watchedResources.size > 0) {
|
|
1232
|
+
for (const [key, before] of resourceOriginals) {
|
|
1233
|
+
if (!watchedResources.has(key)) continue
|
|
1234
|
+
if (before === absentValue || !sameValue(before, resources.get(key))) {
|
|
1235
|
+
resourceChangedAt.set(key, world.currentTick())
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1106
1239
|
if (emittedEvents.size > 0) {
|
|
1107
1240
|
const published = world.advanceTick()
|
|
1108
1241
|
for (const [key, values] of emittedEvents) {
|
|
@@ -1145,6 +1278,17 @@ const makeValidatedRuntime = <
|
|
|
1145
1278
|
write: (key: symbol, value: unknown) => void
|
|
1146
1279
|
) => Cells.storeWrite(store, descriptor.key, write, DescriptorModule.constructorOf(descriptor))
|
|
1147
1280
|
|
|
1281
|
+
const makeResourceChangedView = (descriptor: Descriptor<"resource", string, any>, reader: ReaderState): ResourceChangedView<unknown> => {
|
|
1282
|
+
const key = descriptor.key
|
|
1283
|
+
watchedResources.add(key)
|
|
1284
|
+
const read = Cells.storeRead(resources, key)
|
|
1285
|
+
return {
|
|
1286
|
+
get: () => read.get(),
|
|
1287
|
+
// `since` is the previous completed run; 0 before the first one.
|
|
1288
|
+
changed: () => reader.since === 0 || (resourceChangedAt.get(key) ?? 0) > reader.since
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1148
1292
|
const makeEventReadView = <T>(descriptorKey: symbol, reader: ReaderState): EventReadView<T> => ({
|
|
1149
1293
|
all() {
|
|
1150
1294
|
return events.since(descriptorKey, reader.streamSince) as ReadonlyArray<ReadonlyValue<T>>
|
|
@@ -1211,19 +1355,33 @@ const makeValidatedRuntime = <
|
|
|
1211
1355
|
})
|
|
1212
1356
|
|
|
1213
1357
|
const makeMachineReadView = <M extends Machine.StateMachine.Any>(stateMachine: M): MachineReadView<M> => ({
|
|
1214
|
-
get: () => currentMachines.get(stateMachine.key) as Machine.
|
|
1358
|
+
get: () => currentMachines.get(stateMachine.key) as Machine.CurrentValue<M>,
|
|
1215
1359
|
is: (value) => currentMachines.get(stateMachine.key) === value
|
|
1216
1360
|
})
|
|
1217
1361
|
|
|
1362
|
+
/** Queues a next value; a sub-state whose parent is in another state queues nothing and fails. */
|
|
1363
|
+
const queueMachine = <M extends Machine.StateMachine.Any>(stateMachine: M, value: unknown, skipIfSame: boolean): Machine.SetResult<M> => {
|
|
1364
|
+
const within = stateMachine.within
|
|
1365
|
+
if (within !== undefined && !currentMachines.has(stateMachine.key)) {
|
|
1366
|
+
return Result.failure({
|
|
1367
|
+
_tag: "SubStateInactive",
|
|
1368
|
+
machine: stateMachine.name,
|
|
1369
|
+
parent: within.machine.name,
|
|
1370
|
+
requires: within.state
|
|
1371
|
+
} satisfies Machine.SubStateInactive) as Machine.SetResult<M>
|
|
1372
|
+
}
|
|
1373
|
+
journalMachine(stateMachine.key)
|
|
1374
|
+
pendingMachines.set(stateMachine.key, { value, skipIfSame })
|
|
1375
|
+
return (within === undefined ? undefined : succeeded) as Machine.SetResult<M>
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1218
1378
|
const makeNextMachineWriteView = <M extends Machine.StateMachine.Any>(stateMachine: M): NextMachineWriteView<M> => ({
|
|
1219
1379
|
getPending: () => pendingMachines.get(stateMachine.key)?.value as Machine.StateMachine.Value<M> | undefined,
|
|
1220
1380
|
set(value) {
|
|
1221
|
-
|
|
1222
|
-
pendingMachines.set(stateMachine.key, { value, skipIfSame: false })
|
|
1381
|
+
return queueMachine(stateMachine, value, false)
|
|
1223
1382
|
},
|
|
1224
1383
|
setIfChanged(value) {
|
|
1225
|
-
|
|
1226
|
-
pendingMachines.set(stateMachine.key, { value, skipIfSame: true })
|
|
1384
|
+
return queueMachine(stateMachine, value, true)
|
|
1227
1385
|
},
|
|
1228
1386
|
reset() {
|
|
1229
1387
|
journalMachine(stateMachine.key)
|
|
@@ -1282,7 +1440,9 @@ const makeValidatedRuntime = <
|
|
|
1282
1440
|
resources: mapRecord(spec.resources as Record<string, any>, (access) =>
|
|
1283
1441
|
access.mode === "read"
|
|
1284
1442
|
? Cells.storeRead(resources, access.descriptor.key)
|
|
1285
|
-
:
|
|
1443
|
+
: access.mode === "changed"
|
|
1444
|
+
? makeResourceChangedView(access.descriptor, reader)
|
|
1445
|
+
: makeResourceWriteView(access.descriptor, resources, writeResource)),
|
|
1286
1446
|
events: mapRecord(spec.events as Record<string, any>, (access) =>
|
|
1287
1447
|
access.mode === "read"
|
|
1288
1448
|
? makeEventReadView(access.descriptor.key, reader)
|
|
@@ -1626,6 +1786,36 @@ const makeValidatedRuntime = <
|
|
|
1626
1786
|
}
|
|
1627
1787
|
}
|
|
1628
1788
|
|
|
1789
|
+
/**
|
|
1790
|
+
* Despawns the scopes whose trigger matches a committed transition: leaving
|
|
1791
|
+
* `snapshot.from` or entering `snapshot.to`. It runs right after the
|
|
1792
|
+
* commit, so `onExit` systems still saw the members and entities spawned by
|
|
1793
|
+
* `onEnter` survive. Members are the entities in the scope at that moment:
|
|
1794
|
+
* commands queued by `onExit` systems and not yet applied are not.
|
|
1795
|
+
*/
|
|
1796
|
+
const despawnTriggeredScopes = (machineKey: symbol, snapshot: Machine.TransitionSnapshot): void => {
|
|
1797
|
+
const triggers = scopeTriggers.get(machineKey)
|
|
1798
|
+
if (triggers === undefined) return
|
|
1799
|
+
const applied: Array<Debug.AppliedCommand> = []
|
|
1800
|
+
for (const { trigger, scope } of triggers) {
|
|
1801
|
+
const matches = trigger.phase === "exit" ? trigger.state === snapshot.from : trigger.state === snapshot.to
|
|
1802
|
+
if (!matches) continue
|
|
1803
|
+
const origin = `${trigger.phase === "exit" ? "onExit" : "onEnter"}(${trigger.machine.name}=${String(trigger.state)})`
|
|
1804
|
+
const effects: Array<Debug.Effect> = []
|
|
1805
|
+
commandEffects = tracing ? effects : undefined
|
|
1806
|
+
commandOrigin = origin
|
|
1807
|
+
try {
|
|
1808
|
+
commandWorld.destroyEntityScope(scope)
|
|
1809
|
+
} finally {
|
|
1810
|
+
commandEffects = undefined
|
|
1811
|
+
}
|
|
1812
|
+
if (tracing) applied.push({ tag: `despawnScope ${scope.name}`, system: origin, effects })
|
|
1813
|
+
}
|
|
1814
|
+
if (applied.length > 0) {
|
|
1815
|
+
emit({ type: "deferred", frame: frameCount, tick: world.currentTick(), schedule: currentSchedule(), marker: "applyStateTransitions", commands: applied })
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1629
1819
|
/**
|
|
1630
1820
|
* Applies queued commands, then queued machine transitions, running the
|
|
1631
1821
|
* matching exit, transition, and enter schedules from the bundle.
|
|
@@ -1643,6 +1833,10 @@ const makeValidatedRuntime = <
|
|
|
1643
1833
|
|
|
1644
1834
|
const schedules = bundle?.entries ?? []
|
|
1645
1835
|
for (const [machineKey, pending] of scheduledTransitions) {
|
|
1836
|
+
if (pendingMachines.get(machineKey) !== pending) {
|
|
1837
|
+
// Discarded earlier in this marker: its parent left the state it exists in.
|
|
1838
|
+
continue
|
|
1839
|
+
}
|
|
1646
1840
|
pendingMachines.delete(machineKey)
|
|
1647
1841
|
const current = currentMachines.get(machineKey)
|
|
1648
1842
|
if (current === undefined) {
|
|
@@ -1653,12 +1847,25 @@ const makeValidatedRuntime = <
|
|
|
1653
1847
|
continue
|
|
1654
1848
|
}
|
|
1655
1849
|
|
|
1656
|
-
previousMachines.set(machineKey, current)
|
|
1657
1850
|
const snapshot = {
|
|
1658
1851
|
from: current as Machine.StateValue,
|
|
1659
1852
|
to: pending.value as Machine.StateValue
|
|
1660
1853
|
}
|
|
1661
1854
|
|
|
1855
|
+
// Sub-states that exist in the state being left exit first, deepest
|
|
1856
|
+
// first: their exit schedules run before the parent's, while they
|
|
1857
|
+
// still have their values, and they lose them at the parent's commit.
|
|
1858
|
+
const leaving = activeSubStatesIn(machineKey, snapshot.from)
|
|
1859
|
+
for (const sub of leaving) {
|
|
1860
|
+
const value = currentMachines.get(sub.key)
|
|
1861
|
+
const result = runPhase(sub.key, "exit", value, { from: value, to: undefined } as unknown as Machine.TransitionSnapshot, schedules)
|
|
1862
|
+
if (!result.ok) {
|
|
1863
|
+
pendingMachines.set(machineKey, pending)
|
|
1864
|
+
if (tracing) traceTransition(machineKey, snapshot, "failed")
|
|
1865
|
+
return result
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1662
1869
|
for (const schedule of schedules) {
|
|
1663
1870
|
const transition = schedule.transition
|
|
1664
1871
|
if (transition.phase === "exit" && transition.machine.key === machineKey && transition.state === snapshot.from) {
|
|
@@ -1688,10 +1895,8 @@ const makeValidatedRuntime = <
|
|
|
1688
1895
|
}
|
|
1689
1896
|
}
|
|
1690
1897
|
|
|
1691
|
-
currentMachines.
|
|
1692
|
-
|
|
1693
|
-
transitionEvents.append(machineKey, world.advanceTick(), [snapshot])
|
|
1694
|
-
if (tracing) traceTransition(machineKey, snapshot, "applied")
|
|
1898
|
+
for (const sub of leaving) commitMachine(sub.key, currentMachines.get(sub.key), undefined)
|
|
1899
|
+
commitMachine(machineKey, current, pending.value)
|
|
1695
1900
|
|
|
1696
1901
|
for (const schedule of schedules) {
|
|
1697
1902
|
const transition = schedule.transition
|
|
@@ -1703,6 +1908,82 @@ const makeValidatedRuntime = <
|
|
|
1703
1908
|
}
|
|
1704
1909
|
}
|
|
1705
1910
|
}
|
|
1911
|
+
// Sub-states that exist in the entered state enter their value, parents first.
|
|
1912
|
+
const entered = enterSubStates(machineKey, snapshot.to, schedules)
|
|
1913
|
+
if (!entered.ok) return entered
|
|
1914
|
+
}
|
|
1915
|
+
return succeeded
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
/**
|
|
1919
|
+
* Commits one machine's value (`undefined` for a sub-state that stops
|
|
1920
|
+
* existing): records the change and its transition event, traces it, and
|
|
1921
|
+
* despawns the scopes it triggers.
|
|
1922
|
+
*/
|
|
1923
|
+
const commitMachine = (machineKey: symbol, from: unknown, to: unknown): void => {
|
|
1924
|
+
const snapshot = { from, to } as unknown as Machine.TransitionSnapshot
|
|
1925
|
+
if (from !== undefined) previousMachines.set(machineKey, from)
|
|
1926
|
+
if (to === undefined) {
|
|
1927
|
+
currentMachines.delete(machineKey)
|
|
1928
|
+
pendingMachines.delete(machineKey)
|
|
1929
|
+
} else {
|
|
1930
|
+
currentMachines.set(machineKey, to)
|
|
1931
|
+
}
|
|
1932
|
+
changedMachines.add(machineKey)
|
|
1933
|
+
transitionEvents.append(machineKey, world.advanceTick(), [snapshot])
|
|
1934
|
+
if (tracing) traceTransition(machineKey, snapshot, "applied")
|
|
1935
|
+
despawnTriggeredScopes(machineKey, snapshot)
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
type TransitionSchedules = ReadonlyArray<Machine.StateMachine.AnyTransitionSchedule<S, Root>>
|
|
1939
|
+
|
|
1940
|
+
/** Runs every schedule of `phase` for `machineKey` in `state`, in bundle order. */
|
|
1941
|
+
const runPhase = (
|
|
1942
|
+
machineKey: symbol,
|
|
1943
|
+
phase: "enter" | "exit",
|
|
1944
|
+
state: unknown,
|
|
1945
|
+
snapshot: Machine.TransitionSnapshot,
|
|
1946
|
+
schedules: TransitionSchedules
|
|
1947
|
+
): Result.Result<void, SystemFailure> => {
|
|
1948
|
+
for (const schedule of schedules) {
|
|
1949
|
+
const transition = schedule.transition
|
|
1950
|
+
if (transition.phase === phase && transition.machine.key === machineKey && transition.state === state) {
|
|
1951
|
+
const result = runTransitionSchedule(schedule, snapshot)
|
|
1952
|
+
if (!result.ok) return result
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
return succeeded
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
/** The sub-states with a value that exist in `state` of `parentKey`, and theirs, deepest first. */
|
|
1959
|
+
const activeSubStatesIn = (parentKey: symbol, state: unknown): Array<Machine.StateMachine.Any> => {
|
|
1960
|
+
const result: Array<Machine.StateMachine.Any> = []
|
|
1961
|
+
for (const sub of subStates.get(parentKey) ?? []) {
|
|
1962
|
+
const value = currentMachines.get(sub.key)
|
|
1963
|
+
if (sub.within!.state !== state || value === undefined) continue
|
|
1964
|
+
result.push(...activeSubStatesIn(sub.key, value), sub)
|
|
1965
|
+
}
|
|
1966
|
+
return result
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
/**
|
|
1970
|
+
* Gives the sub-states of `parentKey` that exist in `state` their entry
|
|
1971
|
+
* value, then runs their `onEnter` schedules and enters their own
|
|
1972
|
+
* sub-states.
|
|
1973
|
+
*/
|
|
1974
|
+
const enterSubStates = (parentKey: symbol, state: unknown, schedules: TransitionSchedules): Result.Result<void, SystemFailure> => {
|
|
1975
|
+
for (const sub of subStates.get(parentKey) ?? []) {
|
|
1976
|
+
if (sub.within!.state !== state || currentMachines.has(sub.key) || !subStateEntry.has(sub.key)) continue
|
|
1977
|
+
const value = subStateEntry.get(sub.key)
|
|
1978
|
+
commitMachine(sub.key, undefined, value)
|
|
1979
|
+
const snapshot = { from: undefined, to: value } as unknown as Machine.TransitionSnapshot
|
|
1980
|
+
const result = runPhase(sub.key, "enter", value, snapshot, schedules)
|
|
1981
|
+
if (!result.ok) {
|
|
1982
|
+
if (tracing) traceTransition(sub.key, snapshot, "enterFailed")
|
|
1983
|
+
return result
|
|
1984
|
+
}
|
|
1985
|
+
const nested = enterSubStates(sub.key, value, schedules)
|
|
1986
|
+
if (!nested.ok) return nested
|
|
1706
1987
|
}
|
|
1707
1988
|
return succeeded
|
|
1708
1989
|
}
|
|
@@ -1876,7 +2157,8 @@ const makeValidatedRuntime = <
|
|
|
1876
2157
|
case "resource":
|
|
1877
2158
|
return resources.has(requirement.key)
|
|
1878
2159
|
case "stateMachine":
|
|
1879
|
-
|
|
2160
|
+
// An inactive sub-state has no value but is provided.
|
|
2161
|
+
return providedMachineKeys.has(requirement.key) || currentMachines.has(requirement.key)
|
|
1880
2162
|
}
|
|
1881
2163
|
}
|
|
1882
2164
|
|
|
@@ -1992,6 +2274,11 @@ const makeValidatedRuntime = <
|
|
|
1992
2274
|
if (!value.ok) return Result.failure({ _tag: "InvalidComponent", entityId: entity.id, name, error: value.error })
|
|
1993
2275
|
components.push([descriptor, value.value])
|
|
1994
2276
|
}
|
|
2277
|
+
// Transient components are never saved, so a requirement on one is not checked here.
|
|
2278
|
+
const missing = missingIn(components, DescriptorModule.isTransient)
|
|
2279
|
+
if (missing !== undefined) {
|
|
2280
|
+
return Result.failure({ _tag: "MissingRequiredComponent", entityId: entity.id, name: missing[1], requiredBy: missing[0] })
|
|
2281
|
+
}
|
|
1995
2282
|
entities.push({ id: entity.id, components })
|
|
1996
2283
|
}
|
|
1997
2284
|
const validatedResources: Array<readonly [symbol, unknown]> = []
|
|
@@ -2028,12 +2315,29 @@ const makeValidatedRuntime = <
|
|
|
2028
2315
|
}
|
|
2029
2316
|
}
|
|
2030
2317
|
}
|
|
2031
|
-
const
|
|
2318
|
+
const restoredMachines = new Map(currentMachines)
|
|
2032
2319
|
for (const [name, value] of Object.entries(saved.machines)) {
|
|
2033
2320
|
const machine = machinesByName.get(name)
|
|
2034
2321
|
if (!machine) return Result.failure({ _tag: "UnknownMachine", name })
|
|
2035
2322
|
if (!machine.values.includes(value)) return Result.failure({ _tag: "InvalidMachineState", name, value })
|
|
2036
|
-
|
|
2323
|
+
restoredMachines.set(machine.key, value)
|
|
2324
|
+
}
|
|
2325
|
+
// A save has no value for a sub-state whose parent is elsewhere; one that
|
|
2326
|
+
// has a value must have its parent in the state it exists in. A sub-state
|
|
2327
|
+
// missing while its parent is there enters its entry value.
|
|
2328
|
+
for (const machine of options.machineDefinitions) {
|
|
2329
|
+
const within = machine.within
|
|
2330
|
+
if (within === undefined) continue
|
|
2331
|
+
const active = restoredMachines.get(within.machine.key) === within.state
|
|
2332
|
+
if (Object.hasOwn(saved.machines, machine.name)) {
|
|
2333
|
+
if (!active) {
|
|
2334
|
+
return Result.failure({ _tag: "InactiveSubState", name: machine.name, parent: within.machine.name, requires: within.state })
|
|
2335
|
+
}
|
|
2336
|
+
} else if (active && subStateEntry.has(machine.key)) {
|
|
2337
|
+
restoredMachines.set(machine.key, subStateEntry.get(machine.key))
|
|
2338
|
+
} else {
|
|
2339
|
+
restoredMachines.delete(machine.key)
|
|
2340
|
+
}
|
|
2037
2341
|
}
|
|
2038
2342
|
|
|
2039
2343
|
const scoped = new Set<number>()
|
|
@@ -2066,8 +2370,12 @@ const makeValidatedRuntime = <
|
|
|
2066
2370
|
world.tryRelate(source, relation, target)
|
|
2067
2371
|
}
|
|
2068
2372
|
for (const [id, key] of memberships) world.assignEntityScope(id, key)
|
|
2069
|
-
for (const [key, value] of validatedResources)
|
|
2070
|
-
|
|
2373
|
+
for (const [key, value] of validatedResources) {
|
|
2374
|
+
resources.set(key, value)
|
|
2375
|
+
resourceChangedAt.set(key, world.currentTick())
|
|
2376
|
+
}
|
|
2377
|
+
currentMachines.clear()
|
|
2378
|
+
for (const [key, value] of restoredMachines) currentMachines.set(key, value)
|
|
2071
2379
|
// Locals are not saved: each system starts over from its factory.
|
|
2072
2380
|
localSlots.forEach(({ slot }, index) => { (slot.context as { local: unknown }).local = rebuiltLocals[index] })
|
|
2073
2381
|
return succeeded
|
|
@@ -2217,11 +2525,14 @@ const makeValidatedRuntime = <
|
|
|
2217
2525
|
schema: options.schema,
|
|
2218
2526
|
machines: [...new Set(machinesByName.values())],
|
|
2219
2527
|
currentMachine: (machine) => currentMachines.get(machine.key) as Machine.StateValue | undefined,
|
|
2528
|
+
providesMachine: (machine) => providedMachineKeys.has(machine.key),
|
|
2529
|
+
scopes: options.scopeDefinitions,
|
|
2220
2530
|
providedServices: Object.keys(providedServices),
|
|
2221
2531
|
externallyWrittenEvents: new Set([...externallyWrittenEvents, ...hostEvents]),
|
|
2222
2532
|
relationMutations,
|
|
2223
2533
|
commandUses,
|
|
2224
2534
|
ranSystems,
|
|
2535
|
+
requirementViolations: [...requirementViolations.values()],
|
|
2225
2536
|
hasResource: (descriptor) => resources.has(descriptor.key),
|
|
2226
2537
|
schedules: [...scheduleNames].map(([schedule, name]) => [name, schedule as ExecutableScheduleDefinition<S>] as const)
|
|
2227
2538
|
}),
|
|
@@ -2292,6 +2603,8 @@ export const make = <
|
|
|
2292
2603
|
readonly resources?: Resources
|
|
2293
2604
|
readonly machines?: ProvidedMachines
|
|
2294
2605
|
readonly machineDefinitions?: ReadonlyArray<Machine.StateMachine.Any>
|
|
2606
|
+
/** Scopes whose members a committed state change despawns; `Game.Runtime.make` passes every `Game.EntityScope`. */
|
|
2607
|
+
readonly scopeDefinitions?: ReadonlyArray<EntityScopeModule.EntityScope.Any>
|
|
2295
2608
|
/** Attaches a `debug` handle to the runtime, timed by `hostClock`; see the `Debug` module. */
|
|
2296
2609
|
readonly debug?: Enabled
|
|
2297
2610
|
}): MakeRuntimeResult<S, Simplify<RuntimeServicesOf<ProvidedServices>>, Resources, Root, RuntimeMachinesOf<ProvidedMachines>, Enabled> => {
|
|
@@ -2310,6 +2623,7 @@ export const make = <
|
|
|
2310
2623
|
resources: validated.value,
|
|
2311
2624
|
machines: options.machines,
|
|
2312
2625
|
machineDefinitions: options.machineDefinitions ?? [],
|
|
2626
|
+
scopeDefinitions: options.scopeDefinitions ?? [],
|
|
2313
2627
|
debug: options.debug
|
|
2314
2628
|
})
|
|
2315
2629
|
return (fallible ? Result.success(runtime) : runtime) as never
|