@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/src/Runtime.ts
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: {
|
|
@@ -88,7 +89,8 @@ import type {
|
|
|
88
89
|
ResourceChangedView,
|
|
89
90
|
EventWriteView,
|
|
90
91
|
LookupApi,
|
|
91
|
-
|
|
92
|
+
QueryIteration,
|
|
93
|
+
QueryLookups,
|
|
92
94
|
RelationFailureReadView,
|
|
93
95
|
RemovedReadView,
|
|
94
96
|
TransitionEventReadView,
|
|
@@ -118,7 +120,7 @@ import type {
|
|
|
118
120
|
* services: Game.Runtime.services(
|
|
119
121
|
* Game.Runtime.service(Logger, { log: console.log })
|
|
120
122
|
* )
|
|
121
|
-
* })
|
|
123
|
+
* }).value
|
|
122
124
|
*
|
|
123
125
|
* runtime.tick(updateSchedule)
|
|
124
126
|
* ```
|
|
@@ -157,6 +159,17 @@ export type RuntimeResources<S extends Schema.Any> = Partial<{
|
|
|
157
159
|
: Schema.ResourceValue<S, K>
|
|
158
160
|
}>
|
|
159
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Rejects keys of `Provided` that are not resource keys of the schema, naming
|
|
164
|
+
* each one. A misspelled key next to known ones would otherwise compile and
|
|
165
|
+
* be dropped.
|
|
166
|
+
*/
|
|
167
|
+
export type ExactResources<S extends Schema.Any, Provided> = {
|
|
168
|
+
readonly [K in Exclude<keyof Provided, keyof Schema.Resources<S>>]: {
|
|
169
|
+
readonly __fixResources__: `${K & string} is not a resource key of this schema: use a key of the fragment's resources: { ... }`
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
160
173
|
type ConstructedKeys<S extends Schema.Any, Provided> = {
|
|
161
174
|
readonly [K in keyof Provided]: K extends keyof Schema.Resources<S>
|
|
162
175
|
? Schema.Resources<S>[K] extends DescriptorModule.ConstructedDescriptor<"resource", string, any, any, any> ? K : never
|
|
@@ -193,8 +206,9 @@ export type WithDebug<R, Enabled, S extends Schema.Any, Root> =
|
|
|
193
206
|
: R & Partial<Debug.Member<S, Root>>
|
|
194
207
|
|
|
195
208
|
/**
|
|
196
|
-
*
|
|
197
|
-
* provided
|
|
209
|
+
* What `make` returns: always a `Result`. It is a `Success` when nothing
|
|
210
|
+
* provided can fail, so `.value` is the runtime; when a provided resource
|
|
211
|
+
* goes through a constructor, it may be a `Failure` and must be checked.
|
|
198
212
|
*/
|
|
199
213
|
export type MakeRuntimeResult<
|
|
200
214
|
S extends Schema.Any,
|
|
@@ -204,7 +218,7 @@ export type MakeRuntimeResult<
|
|
|
204
218
|
Machines extends Record<string, unknown>,
|
|
205
219
|
Enabled = undefined
|
|
206
220
|
> = [ConstructedKeys<S, Provided>] extends [never]
|
|
207
|
-
? WithDebug<Runtime<S, Services, ProvidedResources<S, Provided>, Root, Machines>, Enabled, S, Root
|
|
221
|
+
? Result.Success<WithDebug<Runtime<S, Services, ProvidedResources<S, Provided>, Root, Machines>, Enabled, S, Root>>
|
|
208
222
|
: Result.Result<
|
|
209
223
|
WithDebug<Runtime<S, Services, ProvidedResources<S, Provided>, Root, Machines>, Enabled, S, Root>,
|
|
210
224
|
RuntimeConstructionError<S, Provided>
|
|
@@ -637,12 +651,16 @@ export interface Runtime<
|
|
|
637
651
|
* resources are skipped.
|
|
638
652
|
*
|
|
639
653
|
* Callable only when every component and resource is constructed or
|
|
640
|
-
* transient; see the `Snapshot` module.
|
|
654
|
+
* transient; see the `Snapshot` module. Take it between ticks: calling it
|
|
655
|
+
* while a schedule is running (from a system, a service, or a trace
|
|
656
|
+
* callback) throws.
|
|
641
657
|
*/
|
|
642
658
|
readonly snapshot: Snapshot.Gate<S, () => Snapshot.WorldSnapshot>
|
|
643
659
|
/**
|
|
644
660
|
* Replaces the world with a snapshot after validating it against the
|
|
645
|
-
* schema. On failure nothing changes. See the `Snapshot` module.
|
|
661
|
+
* schema. On failure nothing changes. See the `Snapshot` module. Restore
|
|
662
|
+
* between ticks: calling it while a schedule is running throws. A current
|
|
663
|
+
* resource the save lacks keeps its value.
|
|
646
664
|
*/
|
|
647
665
|
readonly restore: Snapshot.Gate<S, (data: unknown) => Result.Result<void, Snapshot.RestoreError>>
|
|
648
666
|
/**
|
|
@@ -661,7 +679,7 @@ export interface Runtime<
|
|
|
661
679
|
* The event must belong to the schema. Calling it while a schedule is
|
|
662
680
|
* running is a defect and throws: inside a tick, emit from a system.
|
|
663
681
|
*/
|
|
664
|
-
readonly send: <D extends Schema.EventDescriptor<S>>(descriptor: D, value: Descriptor.Value<D
|
|
682
|
+
readonly send: <D extends Schema.EventDescriptor<S>>(descriptor: D, value: ReadonlyValue<Descriptor.Value<D>>) => void
|
|
665
683
|
}
|
|
666
684
|
|
|
667
685
|
/**
|
|
@@ -685,7 +703,7 @@ export interface Runtime<
|
|
|
685
703
|
* resources: {
|
|
686
704
|
* score: 0
|
|
687
705
|
* }
|
|
688
|
-
* })
|
|
706
|
+
* }).value
|
|
689
707
|
* ```
|
|
690
708
|
*/
|
|
691
709
|
/**
|
|
@@ -696,6 +714,23 @@ export interface Runtime<
|
|
|
696
714
|
*/
|
|
697
715
|
export const streamCapacity = 65_536
|
|
698
716
|
|
|
717
|
+
/**
|
|
718
|
+
* Brings a parsed save to `migrations.latest`: runs every step after its
|
|
719
|
+
* game version, checking the structure after each.
|
|
720
|
+
*/
|
|
721
|
+
const migrateSave = (save: Snapshot.WorldSnapshot, saves: Snapshot.Migrations | undefined): Result.Result<Snapshot.WorldSnapshot, Snapshot.RestoreError> => {
|
|
722
|
+
const latest = saves?.latest ?? 0
|
|
723
|
+
if (save.gameVersion > latest) return Result.failure({ _tag: "NewerGameVersion", gameVersion: save.gameVersion, latest })
|
|
724
|
+
let current = save
|
|
725
|
+
for (const step of saves?.steps ?? []) {
|
|
726
|
+
if (step.to <= current.gameVersion) continue
|
|
727
|
+
const next = Snapshot.parse({ ...step.migrate(current), gameVersion: step.to })
|
|
728
|
+
if (!next.ok) return next
|
|
729
|
+
current = next.value
|
|
730
|
+
}
|
|
731
|
+
return Result.success(current)
|
|
732
|
+
}
|
|
733
|
+
|
|
699
734
|
const makeValidatedRuntime = <
|
|
700
735
|
S extends Schema.Any,
|
|
701
736
|
Services extends Record<string, unknown>,
|
|
@@ -710,6 +745,7 @@ const makeValidatedRuntime = <
|
|
|
710
745
|
readonly machineDefinitions: ReadonlyArray<Machine.StateMachine.Any>
|
|
711
746
|
readonly scopeDefinitions: ReadonlyArray<EntityScopeModule.EntityScope.Any>
|
|
712
747
|
readonly debug: Debug.Option | undefined
|
|
748
|
+
readonly saves: Snapshot.Migrations | undefined
|
|
713
749
|
}): Runtime<S, Services, Resources, Root, Machines> => {
|
|
714
750
|
const world = makeWorld(options.schema, streamCapacity)
|
|
715
751
|
const queries = makeQueryEngine(world)
|
|
@@ -724,7 +760,10 @@ const makeValidatedRuntime = <
|
|
|
724
760
|
* Machine-keyed committed, pending, and previous values.
|
|
725
761
|
*/
|
|
726
762
|
const currentMachines = new Map<symbol, unknown>()
|
|
727
|
-
|
|
763
|
+
/** Queued next values; with `debug`, `by` is the system that queued it, for the `next-state-replaced` lint. */
|
|
764
|
+
const pendingMachines = new Map<symbol, { value: unknown; skipIfSame: boolean; by?: string }>()
|
|
765
|
+
/** Requests dropped because their sub-state stopped existing: skipped by the marker that queued them. */
|
|
766
|
+
const discardedPending = new WeakSet<object>()
|
|
728
767
|
const previousMachines = new Map<symbol, unknown>()
|
|
729
768
|
/**
|
|
730
769
|
* Machine keys changed by transitions applied during the current schedule run.
|
|
@@ -886,6 +925,10 @@ const makeValidatedRuntime = <
|
|
|
886
925
|
*/
|
|
887
926
|
const commandUses = new Map<string, Set<string>>()
|
|
888
927
|
const ranSystems = new Set<string>()
|
|
928
|
+
/** With `debug`, the system running now: who queued a next state. */
|
|
929
|
+
let runningSystem = "(host)"
|
|
930
|
+
/** With `debug`, next states one system queued and another replaced before the marker. */
|
|
931
|
+
const nextStateReplacements = new Map<string, Debug.NextStateReplacement>()
|
|
889
932
|
const noteCommand = (kind: string, name: string): void => {
|
|
890
933
|
let uses = commandUses.get(commandOrigin)
|
|
891
934
|
if (uses === undefined) {
|
|
@@ -925,6 +968,39 @@ const makeValidatedRuntime = <
|
|
|
925
968
|
requirementViolations.set(key, { system: commandOrigin, command, component, missing, entity, count: (seen?.count ?? 0) + 1 })
|
|
926
969
|
}
|
|
927
970
|
}
|
|
971
|
+
const streamLosses = new Map<string, Debug.StreamLoss>()
|
|
972
|
+
const watchStreamLoss = <V extends { all(): unknown; lagged(): boolean }>(view: V, system: string, stream: string, paused: boolean): V => {
|
|
973
|
+
const key = `${system}/${stream}`
|
|
974
|
+
const entry = (): { -readonly [K in keyof Debug.StreamLoss]: Debug.StreamLoss[K] } => {
|
|
975
|
+
let found = streamLosses.get(key)
|
|
976
|
+
if (found === undefined) {
|
|
977
|
+
found = { system, stream, lostRuns: 0, checksLagged: false, paused }
|
|
978
|
+
streamLosses.set(key, found)
|
|
979
|
+
}
|
|
980
|
+
return found as { -readonly [K in keyof Debug.StreamLoss]: Debug.StreamLoss[K] }
|
|
981
|
+
}
|
|
982
|
+
return {
|
|
983
|
+
...view,
|
|
984
|
+
all: () => {
|
|
985
|
+
const status = entry()
|
|
986
|
+
if (view.lagged()) status.lostRuns += 1
|
|
987
|
+
return view.all()
|
|
988
|
+
},
|
|
989
|
+
lagged: () => {
|
|
990
|
+
entry().checksLagged = true
|
|
991
|
+
return view.lagged()
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
const staleTargets = new Map<string, Debug.StaleTarget>()
|
|
996
|
+
const reportStaleTarget = (command: Debug.StaleTarget["command"], entity: number): void => {
|
|
997
|
+
if (tracing) recordEffect({ kind: "staleTarget", entity, command })
|
|
998
|
+
if (debugEnabled) {
|
|
999
|
+
const key = `${commandOrigin}/${command}`
|
|
1000
|
+
const seen = staleTargets.get(key)
|
|
1001
|
+
staleTargets.set(key, { system: commandOrigin, command, entity, count: (seen?.count ?? 0) + 1 })
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
928
1004
|
const holds = (id: number) => {
|
|
929
1005
|
const record = world.records.get(id)
|
|
930
1006
|
return (descriptor: Descriptor<"component", string, any>): boolean =>
|
|
@@ -1007,6 +1083,9 @@ const makeValidatedRuntime = <
|
|
|
1007
1083
|
world.writeComponent(id, descriptor, value)
|
|
1008
1084
|
}
|
|
1009
1085
|
},
|
|
1086
|
+
reportStaleTarget(command, id) {
|
|
1087
|
+
reportStaleTarget(command, id)
|
|
1088
|
+
},
|
|
1010
1089
|
tryRelate(id, relation, target) {
|
|
1011
1090
|
if (debugEnabled) {
|
|
1012
1091
|
noteRelationMutation(relation)
|
|
@@ -1140,7 +1219,7 @@ const makeValidatedRuntime = <
|
|
|
1140
1219
|
return lookup
|
|
1141
1220
|
}
|
|
1142
1221
|
|
|
1143
|
-
const makeQueryHandle = <Q extends Query.Query.Any<Root>>(query: Q, reader: ReaderState, lookup: LookupApi<S, Root>):
|
|
1222
|
+
const makeQueryHandle = <Q extends Query.Query.Any<Root>>(query: Q, reader: ReaderState, lookup: LookupApi<S, Root>): QueryIteration<S, Q> & QueryLookups<S, Q> => {
|
|
1144
1223
|
const each = () => queries.each(query, reader.since) as ReadonlyArray<QueryMatch<S, Q>>
|
|
1145
1224
|
const matching = (ids: ReadonlyArray<Entity.EntityId<S, Root>>): Array<QueryMatch<S, Q>> => {
|
|
1146
1225
|
const matches: Array<QueryMatch<S, Q>> = []
|
|
@@ -1198,6 +1277,32 @@ const makeValidatedRuntime = <
|
|
|
1198
1277
|
const machineOriginals = new Map<symbol, { value: unknown; skipIfSame: boolean } | typeof absentValue>()
|
|
1199
1278
|
const emittedEvents = new Map<symbol, Array<unknown>>()
|
|
1200
1279
|
|
|
1280
|
+
/**
|
|
1281
|
+
* The open phase, or `undefined`. A transition's exit phase (the exit
|
|
1282
|
+
* schedules of the sub-states it ends and of the state it leaves, then its
|
|
1283
|
+
* `onTransition` schedules) commits or rolls back as one: when one of its
|
|
1284
|
+
* systems fails, the writes, events, queued next states, queued commands,
|
|
1285
|
+
* and stream positions of the systems that ran before it are undone too, so
|
|
1286
|
+
* the retry at a later marker starts from the same state.
|
|
1287
|
+
*/
|
|
1288
|
+
interface Phase {
|
|
1289
|
+
readonly tick: number
|
|
1290
|
+
readonly commands: number
|
|
1291
|
+
readonly origins: number
|
|
1292
|
+
readonly resources: Map<symbol, unknown>
|
|
1293
|
+
readonly resourceChangedAt: Map<symbol, number | undefined>
|
|
1294
|
+
readonly machines: Map<symbol, { value: unknown; skipIfSame: boolean } | typeof absentValue>
|
|
1295
|
+
readonly readers: Map<ReaderState, readonly [since: number, lastRun: number, streamSince: number, streamLastRun: number]>
|
|
1296
|
+
}
|
|
1297
|
+
let phase: Phase | undefined
|
|
1298
|
+
|
|
1299
|
+
/** Keeps a reader's position from before the phase, the first time the phase moves it. */
|
|
1300
|
+
const journalReader = (reader: ReaderState): void => {
|
|
1301
|
+
if (phase !== undefined && !phase.readers.has(reader)) {
|
|
1302
|
+
phase.readers.set(reader, [reader.since, reader.lastRun, reader.streamSince, reader.streamLastRun])
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1201
1306
|
const journalStoreWrite = (store: Map<symbol, unknown>, originals: Map<symbol, unknown>) =>
|
|
1202
1307
|
(key: symbol, value: unknown): void => {
|
|
1203
1308
|
if (!originals.has(key)) {
|
|
@@ -1228,10 +1333,19 @@ const makeValidatedRuntime = <
|
|
|
1228
1333
|
|
|
1229
1334
|
const commitSystemTransaction = (): void => {
|
|
1230
1335
|
world.commitTransaction()
|
|
1336
|
+
if (phase !== undefined) {
|
|
1337
|
+
for (const [key, before] of resourceOriginals) {
|
|
1338
|
+
if (!phase.resources.has(key)) phase.resources.set(key, before)
|
|
1339
|
+
}
|
|
1340
|
+
for (const [key, before] of machineOriginals) {
|
|
1341
|
+
if (!phase.machines.has(key)) phase.machines.set(key, before)
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1231
1344
|
if (watchedResources.size > 0) {
|
|
1232
1345
|
for (const [key, before] of resourceOriginals) {
|
|
1233
1346
|
if (!watchedResources.has(key)) continue
|
|
1234
1347
|
if (before === absentValue || !sameValue(before, resources.get(key))) {
|
|
1348
|
+
if (phase !== undefined && !phase.resourceChangedAt.has(key)) phase.resourceChangedAt.set(key, resourceChangedAt.get(key))
|
|
1235
1349
|
resourceChangedAt.set(key, world.currentTick())
|
|
1236
1350
|
}
|
|
1237
1351
|
}
|
|
@@ -1272,6 +1386,54 @@ const makeValidatedRuntime = <
|
|
|
1272
1386
|
emittedEvents.clear()
|
|
1273
1387
|
}
|
|
1274
1388
|
|
|
1389
|
+
/**
|
|
1390
|
+
* Runs `run` as one phase: on failure, or a thrown defect, everything the
|
|
1391
|
+
* systems in it committed is undone.
|
|
1392
|
+
*/
|
|
1393
|
+
const runAsPhase = (run: () => Result.Result<void, SystemFailure>): Result.Result<void, SystemFailure> => {
|
|
1394
|
+
const opened: Phase = {
|
|
1395
|
+
tick: world.currentTick(),
|
|
1396
|
+
commands: pendingCommands.length,
|
|
1397
|
+
origins: pendingOrigins.length,
|
|
1398
|
+
resources: new Map(),
|
|
1399
|
+
resourceChangedAt: new Map(),
|
|
1400
|
+
machines: new Map(),
|
|
1401
|
+
readers: new Map()
|
|
1402
|
+
}
|
|
1403
|
+
phase = opened
|
|
1404
|
+
world.beginPhase()
|
|
1405
|
+
let result: Result.Result<void, SystemFailure> | undefined
|
|
1406
|
+
try {
|
|
1407
|
+
result = run()
|
|
1408
|
+
} finally {
|
|
1409
|
+
phase = undefined
|
|
1410
|
+
if (result?.ok === true) {
|
|
1411
|
+
world.commitPhase()
|
|
1412
|
+
} else {
|
|
1413
|
+
world.rollbackPhase()
|
|
1414
|
+
restoreStore(resources, opened.resources)
|
|
1415
|
+
for (const [key, value] of opened.machines) {
|
|
1416
|
+
if (value === absentValue) pendingMachines.delete(key)
|
|
1417
|
+
else pendingMachines.set(key, value)
|
|
1418
|
+
}
|
|
1419
|
+
for (const [key, tick] of opened.resourceChangedAt) {
|
|
1420
|
+
if (tick === undefined) resourceChangedAt.delete(key)
|
|
1421
|
+
else resourceChangedAt.set(key, tick)
|
|
1422
|
+
}
|
|
1423
|
+
for (const [reader, [since, lastRun, streamSince, streamLastRun]] of opened.readers) {
|
|
1424
|
+
reader.since = since
|
|
1425
|
+
reader.lastRun = lastRun
|
|
1426
|
+
reader.streamSince = streamSince
|
|
1427
|
+
reader.streamLastRun = streamLastRun
|
|
1428
|
+
}
|
|
1429
|
+
events.retract(opened.tick)
|
|
1430
|
+
pendingCommands.length = opened.commands
|
|
1431
|
+
pendingOrigins.length = opened.origins
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
return result
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1275
1437
|
const makeResourceWriteView = (
|
|
1276
1438
|
descriptor: Descriptor<"resource", string, any>,
|
|
1277
1439
|
store: Map<symbol, unknown>,
|
|
@@ -1371,7 +1533,23 @@ const makeValidatedRuntime = <
|
|
|
1371
1533
|
} satisfies Machine.SubStateInactive) as Machine.SetResult<M>
|
|
1372
1534
|
}
|
|
1373
1535
|
journalMachine(stateMachine.key)
|
|
1374
|
-
|
|
1536
|
+
if (debugEnabled) {
|
|
1537
|
+
const previous = pendingMachines.get(stateMachine.key)
|
|
1538
|
+
// A set from a transition schedule waits for the next marker: it replaces nothing queued for this one.
|
|
1539
|
+
if (previous?.by !== undefined && previous.by !== runningSystem && previous.value !== value && activeTransitions.size === 0) {
|
|
1540
|
+
const key = `${stateMachine.name}/${previous.by}/${runningSystem}/${String(previous.value)}/${String(value)}`
|
|
1541
|
+
const seen = nextStateReplacements.get(key)
|
|
1542
|
+
nextStateReplacements.set(key, {
|
|
1543
|
+
machine: stateMachine.name,
|
|
1544
|
+
replaced: { system: previous.by, value: previous.value as Machine.StateValue },
|
|
1545
|
+
by: { system: runningSystem, value: value as Machine.StateValue },
|
|
1546
|
+
count: (seen?.count ?? 0) + 1
|
|
1547
|
+
})
|
|
1548
|
+
}
|
|
1549
|
+
pendingMachines.set(stateMachine.key, { value, skipIfSame, by: runningSystem })
|
|
1550
|
+
} else {
|
|
1551
|
+
pendingMachines.set(stateMachine.key, { value, skipIfSame })
|
|
1552
|
+
}
|
|
1375
1553
|
return (within === undefined ? undefined : succeeded) as Machine.SetResult<M>
|
|
1376
1554
|
}
|
|
1377
1555
|
|
|
@@ -1433,9 +1611,14 @@ const makeValidatedRuntime = <
|
|
|
1433
1611
|
const makeContext = (system: SystemDefinition<any, any, any>, reader: ReaderState, queue: Command.CommandQueue<S, Root>): SystemContext<any> => {
|
|
1434
1612
|
const spec = system.spec
|
|
1435
1613
|
const lookup = makeLookup(reader)
|
|
1614
|
+
const paused = (spec.pauseWhen as ReadonlyArray<unknown>).length > 0
|
|
1615
|
+
/** On a debug runtime, records reads after a loss and calls of `lagged()`, for the `stream-loss-unread` lint. */
|
|
1616
|
+
const watch = <V extends { all(): unknown; lagged(): boolean }>(view: V, stream: string): V =>
|
|
1617
|
+
debugEnabled ? watchStreamLoss(view, system.name, stream, paused) : view
|
|
1436
1618
|
return {
|
|
1437
1619
|
local: (spec.local as (() => unknown) | undefined)?.(),
|
|
1438
|
-
|
|
1620
|
+
// A filtered query hands out the same object; its type hides the lookups.
|
|
1621
|
+
queries: mapRecord(spec.queries as Record<string, Query.Query.Any<Root>>, (query) => makeQueryHandle(query, reader, lookup) as never),
|
|
1439
1622
|
lookup,
|
|
1440
1623
|
resources: mapRecord(spec.resources as Record<string, any>, (access) =>
|
|
1441
1624
|
access.mode === "read"
|
|
@@ -1445,15 +1628,15 @@ const makeValidatedRuntime = <
|
|
|
1445
1628
|
: makeResourceWriteView(access.descriptor, resources, writeResource)),
|
|
1446
1629
|
events: mapRecord(spec.events as Record<string, any>, (access) =>
|
|
1447
1630
|
access.mode === "read"
|
|
1448
|
-
? makeEventReadView(access.descriptor.key, reader)
|
|
1631
|
+
? watch(makeEventReadView(access.descriptor.key, reader), `events ${access.descriptor.name}`)
|
|
1449
1632
|
: makeEventWriteView(access.descriptor.key)),
|
|
1450
1633
|
machines: mapRecord(spec.machines as Record<string, any>, (access) => makeMachineReadView(access.machine)),
|
|
1451
1634
|
nextMachines: mapRecord(spec.nextMachines as Record<string, any>, (access) => makeNextMachineWriteView(access.machine)),
|
|
1452
|
-
transitionEvents: mapRecord(spec.transitionEvents as Record<string, any>, (access) => makeTransitionEventReadView(access.machine, reader)),
|
|
1635
|
+
transitionEvents: mapRecord(spec.transitionEvents as Record<string, any>, (access) => watch(makeTransitionEventReadView(access.machine, reader), `transition events ${access.machine.name}`)),
|
|
1453
1636
|
transitions: mapRecord(spec.transitions as Record<string, any>, (access) => makeTransitionReadView(access.machine)),
|
|
1454
|
-
removed: mapRecord(spec.removed as Record<string, any>, (access) => makeRemovedReadView(access.descriptor, reader)),
|
|
1455
|
-
despawned: mapRecord(spec.despawned as Record<string, any>, () => makeDespawnedReadView(reader)),
|
|
1456
|
-
relationFailures: mapRecord(spec.relationFailures as Record<string, any>, (access) => makeRelationFailureReadView(access.relation, reader)),
|
|
1637
|
+
removed: mapRecord(spec.removed as Record<string, any>, (access) => watch(makeRemovedReadView(access.descriptor, reader), `removed ${access.descriptor.name}`)),
|
|
1638
|
+
despawned: mapRecord(spec.despawned as Record<string, any>, () => watch(makeDespawnedReadView(reader), "despawned")),
|
|
1639
|
+
relationFailures: mapRecord(spec.relationFailures as Record<string, any>, (access) => watch(makeRelationFailureReadView(access.relation, reader), `relation failures ${access.relation.name}`)),
|
|
1457
1640
|
services: mapRecord(spec.services as Record<string, any>, (access) =>
|
|
1458
1641
|
providedServices[access.descriptor.name as keyof Services]),
|
|
1459
1642
|
// The queue serves every declared permission set; the spec's types narrow it.
|
|
@@ -1646,6 +1829,7 @@ const makeValidatedRuntime = <
|
|
|
1646
1829
|
// neither holds them nor receives a backlog when it runs again.
|
|
1647
1830
|
const skipped = slotOf(system, true)
|
|
1648
1831
|
if (tracing) traceSkipped(system, condition, skipped.reader, false)
|
|
1832
|
+
journalReader(skipped.reader)
|
|
1649
1833
|
skipped.reader.streamLastRun = world.currentTick()
|
|
1650
1834
|
return succeeded
|
|
1651
1835
|
}
|
|
@@ -1661,7 +1845,11 @@ const makeValidatedRuntime = <
|
|
|
1661
1845
|
}
|
|
1662
1846
|
}
|
|
1663
1847
|
const { context, commands, reader } = slotOf(system, true)
|
|
1664
|
-
if (debugEnabled)
|
|
1848
|
+
if (debugEnabled) {
|
|
1849
|
+
ranSystems.add(system.name)
|
|
1850
|
+
runningSystem = system.name
|
|
1851
|
+
}
|
|
1852
|
+
journalReader(reader)
|
|
1665
1853
|
// Changes stamped after the previous completed run are visible to this run.
|
|
1666
1854
|
reader.since = reader.lastRun
|
|
1667
1855
|
reader.streamSince = reader.streamLastRun
|
|
@@ -1833,11 +2021,17 @@ const makeValidatedRuntime = <
|
|
|
1833
2021
|
|
|
1834
2022
|
const schedules = bundle?.entries ?? []
|
|
1835
2023
|
for (const [machineKey, pending] of scheduledTransitions) {
|
|
1836
|
-
if (
|
|
2024
|
+
if (discardedPending.has(pending)) {
|
|
1837
2025
|
// Discarded earlier in this marker: its parent left the state it exists in.
|
|
1838
2026
|
continue
|
|
1839
2027
|
}
|
|
1840
|
-
|
|
2028
|
+
// A transition schedule earlier in this marker may have queued a newer
|
|
2029
|
+
// value for this machine: that one waits for the next marker.
|
|
2030
|
+
if (pendingMachines.get(machineKey) === pending) pendingMachines.delete(machineKey)
|
|
2031
|
+
/** Keeps this request queued after a failure, unless a newer one replaced it. */
|
|
2032
|
+
const requeue = () => {
|
|
2033
|
+
if (!pendingMachines.has(machineKey)) pendingMachines.set(machineKey, pending)
|
|
2034
|
+
}
|
|
1841
2035
|
const current = currentMachines.get(machineKey)
|
|
1842
2036
|
if (current === undefined) {
|
|
1843
2037
|
continue
|
|
@@ -1855,48 +2049,44 @@ const makeValidatedRuntime = <
|
|
|
1855
2049
|
// Sub-states that exist in the state being left exit first, deepest
|
|
1856
2050
|
// first: their exit schedules run before the parent's, while they
|
|
1857
2051
|
// still have their values, and they lose them at the parent's commit.
|
|
2052
|
+
// The exit phase is one transaction: a failure undoes all of it, and
|
|
2053
|
+
// the transition stays queued for a later attempt.
|
|
1858
2054
|
const leaving = activeSubStatesIn(machineKey, snapshot.from)
|
|
1859
|
-
|
|
1860
|
-
const
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
if (tracing) traceTransition(machineKey, snapshot, "failed")
|
|
1865
|
-
return result
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
for (const schedule of schedules) {
|
|
1870
|
-
const transition = schedule.transition
|
|
1871
|
-
if (transition.phase === "exit" && transition.machine.key === machineKey && transition.state === snapshot.from) {
|
|
1872
|
-
const result = runTransitionSchedule(schedule, snapshot)
|
|
1873
|
-
if (!result.ok) {
|
|
1874
|
-
// The transition did not happen; keep it queued for a later attempt.
|
|
1875
|
-
pendingMachines.set(machineKey, pending)
|
|
1876
|
-
if (tracing) traceTransition(machineKey, snapshot, "failed")
|
|
1877
|
-
return result
|
|
1878
|
-
}
|
|
2055
|
+
const exited = runAsPhase(() => {
|
|
2056
|
+
for (const sub of leaving) {
|
|
2057
|
+
const value = currentMachines.get(sub.key)
|
|
2058
|
+
const result = runPhase(sub.key, "exit", value, { from: value, to: undefined } as unknown as Machine.TransitionSnapshot, schedules)
|
|
2059
|
+
if (!result.ok) return result
|
|
1879
2060
|
}
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
const
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
if (tracing) traceTransition(machineKey, snapshot, "failed")
|
|
1893
|
-
return result
|
|
2061
|
+
const left = runPhase(machineKey, "exit", snapshot.from, snapshot, schedules)
|
|
2062
|
+
if (!left.ok) return left
|
|
2063
|
+
for (const schedule of schedules) {
|
|
2064
|
+
const transition = schedule.transition
|
|
2065
|
+
if (
|
|
2066
|
+
transition.phase === "transition"
|
|
2067
|
+
&& transition.machine.key === machineKey
|
|
2068
|
+
&& transition.from === snapshot.from
|
|
2069
|
+
&& transition.to === snapshot.to
|
|
2070
|
+
) {
|
|
2071
|
+
const result = runTransitionSchedule(schedule, snapshot)
|
|
2072
|
+
if (!result.ok) return result
|
|
1894
2073
|
}
|
|
1895
2074
|
}
|
|
2075
|
+
return succeeded
|
|
2076
|
+
})
|
|
2077
|
+
if (!exited.ok) {
|
|
2078
|
+
requeue()
|
|
2079
|
+
if (tracing) traceTransition(machineKey, snapshot, "failed")
|
|
2080
|
+
return exited
|
|
1896
2081
|
}
|
|
1897
2082
|
|
|
1898
2083
|
for (const sub of leaving) commitMachine(sub.key, currentMachines.get(sub.key), undefined)
|
|
1899
2084
|
commitMachine(machineKey, current, pending.value)
|
|
2085
|
+
// Sub-states that exist in the entered state enter their value together
|
|
2086
|
+
// with the parent, parents first, before any onEnter schedule runs: a
|
|
2087
|
+
// failing onEnter cannot leave them without a value, and their scope
|
|
2088
|
+
// triggers fire before what the onEnter schedules spawn.
|
|
2089
|
+
const entered = commitSubStates(machineKey, snapshot.to)
|
|
1900
2090
|
|
|
1901
2091
|
for (const schedule of schedules) {
|
|
1902
2092
|
const transition = schedule.transition
|
|
@@ -1908,9 +2098,8 @@ const makeValidatedRuntime = <
|
|
|
1908
2098
|
}
|
|
1909
2099
|
}
|
|
1910
2100
|
}
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
if (!entered.ok) return entered
|
|
2101
|
+
const enteredSchedules = runSubStateEnters(entered, schedules)
|
|
2102
|
+
if (!enteredSchedules.ok) return enteredSchedules
|
|
1914
2103
|
}
|
|
1915
2104
|
return succeeded
|
|
1916
2105
|
}
|
|
@@ -1925,6 +2114,8 @@ const makeValidatedRuntime = <
|
|
|
1925
2114
|
if (from !== undefined) previousMachines.set(machineKey, from)
|
|
1926
2115
|
if (to === undefined) {
|
|
1927
2116
|
currentMachines.delete(machineKey)
|
|
2117
|
+
const discarded = pendingMachines.get(machineKey)
|
|
2118
|
+
if (discarded !== undefined) discardedPending.add(discarded)
|
|
1928
2119
|
pendingMachines.delete(machineKey)
|
|
1929
2120
|
} else {
|
|
1930
2121
|
currentMachines.set(machineKey, to)
|
|
@@ -1968,22 +2159,28 @@ const makeValidatedRuntime = <
|
|
|
1968
2159
|
|
|
1969
2160
|
/**
|
|
1970
2161
|
* Gives the sub-states of `parentKey` that exist in `state` their entry
|
|
1971
|
-
* value,
|
|
1972
|
-
* sub-states.
|
|
2162
|
+
* value, and theirs, parents first. Returns what it entered, in that order.
|
|
1973
2163
|
*/
|
|
1974
|
-
const
|
|
2164
|
+
const commitSubStates = (parentKey: symbol, state: unknown): Array<readonly [symbol, unknown]> => {
|
|
2165
|
+
const entered: Array<readonly [symbol, unknown]> = []
|
|
1975
2166
|
for (const sub of subStates.get(parentKey) ?? []) {
|
|
1976
2167
|
if (sub.within!.state !== state || currentMachines.has(sub.key) || !subStateEntry.has(sub.key)) continue
|
|
1977
2168
|
const value = subStateEntry.get(sub.key)
|
|
1978
2169
|
commitMachine(sub.key, undefined, value)
|
|
2170
|
+
entered.push([sub.key, value], ...commitSubStates(sub.key, value))
|
|
2171
|
+
}
|
|
2172
|
+
return entered
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
/** Runs the onEnter schedules of sub-states entered by {@link commitSubStates}, in that order. */
|
|
2176
|
+
const runSubStateEnters = (entered: ReadonlyArray<readonly [symbol, unknown]>, schedules: TransitionSchedules): Result.Result<void, SystemFailure> => {
|
|
2177
|
+
for (const [key, value] of entered) {
|
|
1979
2178
|
const snapshot = { from: undefined, to: value } as unknown as Machine.TransitionSnapshot
|
|
1980
|
-
const result = runPhase(
|
|
2179
|
+
const result = runPhase(key, "enter", value, snapshot, schedules)
|
|
1981
2180
|
if (!result.ok) {
|
|
1982
|
-
if (tracing) traceTransition(
|
|
2181
|
+
if (tracing) traceTransition(key, snapshot, "enterFailed")
|
|
1983
2182
|
return result
|
|
1984
2183
|
}
|
|
1985
|
-
const nested = enterSubStates(sub.key, value, schedules)
|
|
1986
|
-
if (!nested.ok) return nested
|
|
1987
2184
|
}
|
|
1988
2185
|
return succeeded
|
|
1989
2186
|
}
|
|
@@ -2020,8 +2217,14 @@ const makeValidatedRuntime = <
|
|
|
2020
2217
|
return succeeded
|
|
2021
2218
|
}
|
|
2022
2219
|
|
|
2023
|
-
/** Whether
|
|
2220
|
+
/** Whether `runtime.tick` is on the stack: a nested `tick` is a defect. */
|
|
2024
2221
|
let ticking = false
|
|
2222
|
+
/**
|
|
2223
|
+
* Whether a tick is advancing its frame or running its schedules (in
|
|
2224
|
+
* `tick` and `tryTick`): `send`, `snapshot`, and `restore` are defects
|
|
2225
|
+
* meanwhile. It is cleared before `tick.end` is announced to host listeners.
|
|
2226
|
+
*/
|
|
2227
|
+
let inTick = false
|
|
2025
2228
|
|
|
2026
2229
|
/**
|
|
2027
2230
|
* Runs one schedule. A failing system stops the schedule; systems that
|
|
@@ -2045,7 +2248,7 @@ const makeValidatedRuntime = <
|
|
|
2045
2248
|
let hostEvents = new Set<string>()
|
|
2046
2249
|
|
|
2047
2250
|
const send = (descriptor: Descriptor<"event", string, any>, value: unknown): void => {
|
|
2048
|
-
if (
|
|
2251
|
+
if (inTick) throw new Error(`runtime.send(${descriptor.name}) was called while a schedule is running: emit from a system instead`)
|
|
2049
2252
|
if (!eventsByKey.has(descriptor.key)) throw new Error(`runtime.send: ${descriptor.name} is not an event of this runtime's schema`)
|
|
2050
2253
|
const published = world.advanceTick()
|
|
2051
2254
|
events.append(descriptor.key, published, [value], true)
|
|
@@ -2116,6 +2319,7 @@ const makeValidatedRuntime = <
|
|
|
2116
2319
|
if (ticking) throw new Error("runtime.tick cannot run inside another tick")
|
|
2117
2320
|
if (debugEnabled) reportUnread("next-tick")
|
|
2118
2321
|
ticking = true
|
|
2322
|
+
inTick = true
|
|
2119
2323
|
try {
|
|
2120
2324
|
advanceFrame()
|
|
2121
2325
|
return watchResult(observedTick(schedules, () => {
|
|
@@ -2127,6 +2331,7 @@ const makeValidatedRuntime = <
|
|
|
2127
2331
|
}), schedules)
|
|
2128
2332
|
} finally {
|
|
2129
2333
|
ticking = false
|
|
2334
|
+
inTick = false
|
|
2130
2335
|
}
|
|
2131
2336
|
}
|
|
2132
2337
|
|
|
@@ -2141,9 +2346,11 @@ const makeValidatedRuntime = <
|
|
|
2141
2346
|
try {
|
|
2142
2347
|
result = run()
|
|
2143
2348
|
} catch (defect) {
|
|
2349
|
+
inTick = false
|
|
2144
2350
|
emitHost({ type: "tick.end", schedules, frame: frameCount, outcome: "defect", error: defect })
|
|
2145
2351
|
throw defect
|
|
2146
2352
|
}
|
|
2353
|
+
inTick = false
|
|
2147
2354
|
emitHost(result.ok
|
|
2148
2355
|
? { type: "tick.end", schedules, frame: frameCount, outcome: "ok" }
|
|
2149
2356
|
: { type: "tick.end", schedules, frame: frameCount, outcome: "failed", error: result.error })
|
|
@@ -2182,8 +2389,14 @@ const makeValidatedRuntime = <
|
|
|
2182
2389
|
return watchResult(Result.failure({ kind: "TickOrderViolation" as const, issues }), [schedule])
|
|
2183
2390
|
}
|
|
2184
2391
|
|
|
2185
|
-
|
|
2186
|
-
|
|
2392
|
+
if (inTick) throw new Error("runtime.tryTick cannot run inside another tick")
|
|
2393
|
+
inTick = true
|
|
2394
|
+
try {
|
|
2395
|
+
advanceFrame()
|
|
2396
|
+
return watchResult(observedTick([schedule], () => runScheduleUnsafe(schedule)), [schedule]) as Result.Result<void, Schedule.FailureOf<Selected>>
|
|
2397
|
+
} finally {
|
|
2398
|
+
inTick = false
|
|
2399
|
+
}
|
|
2187
2400
|
}
|
|
2188
2401
|
|
|
2189
2402
|
/**
|
|
@@ -2220,6 +2433,7 @@ const makeValidatedRuntime = <
|
|
|
2220
2433
|
for (const provision of machineEntries) machinesByName.set(provision.machine.name, provision.machine)
|
|
2221
2434
|
|
|
2222
2435
|
const snapshot = (): Snapshot.WorldSnapshot => {
|
|
2436
|
+
if (inTick) 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")
|
|
2223
2437
|
const savedResources: Record<string, unknown> = {}
|
|
2224
2438
|
for (const [name, descriptor] of resourcesByName) {
|
|
2225
2439
|
if (!DescriptorModule.isTransient(descriptor) && resources.has(descriptor.key)) {
|
|
@@ -2238,6 +2452,7 @@ const makeValidatedRuntime = <
|
|
|
2238
2452
|
}
|
|
2239
2453
|
return {
|
|
2240
2454
|
version: Snapshot.version,
|
|
2455
|
+
gameVersion: options.saves?.latest ?? 0,
|
|
2241
2456
|
nextEntity: world.nextEntityValue(),
|
|
2242
2457
|
entities: world.exportEntities(DescriptorModule.isTransient),
|
|
2243
2458
|
relations: world.exportRelations(),
|
|
@@ -2255,7 +2470,9 @@ const makeValidatedRuntime = <
|
|
|
2255
2470
|
const restore = (data: unknown): Result.Result<void, Snapshot.RestoreError> => {
|
|
2256
2471
|
const parsed = Snapshot.parse(data)
|
|
2257
2472
|
if (!parsed.ok) return parsed
|
|
2258
|
-
const
|
|
2473
|
+
const migrated = migrateSave(parsed.value, options.saves)
|
|
2474
|
+
if (!migrated.ok) return migrated
|
|
2475
|
+
const saved = migrated.value
|
|
2259
2476
|
|
|
2260
2477
|
// Validate everything before touching the world.
|
|
2261
2478
|
const ids = new Set<number>()
|
|
@@ -2343,6 +2560,7 @@ const makeValidatedRuntime = <
|
|
|
2343
2560
|
const scoped = new Set<number>()
|
|
2344
2561
|
const memberships: Array<readonly [number, symbol]> = []
|
|
2345
2562
|
for (const [name, members] of Object.entries(saved.scopes)) {
|
|
2563
|
+
if (!options.scopeDefinitions.some((scope) => scope.name === name)) return Result.failure({ _tag: "UnknownScope", name })
|
|
2346
2564
|
for (const id of members) {
|
|
2347
2565
|
if (!ids.has(id) || scoped.has(id)) return Result.failure({ _tag: "InvalidScope", name, entityId: id })
|
|
2348
2566
|
scoped.add(id)
|
|
@@ -2382,6 +2600,7 @@ const makeValidatedRuntime = <
|
|
|
2382
2600
|
}
|
|
2383
2601
|
|
|
2384
2602
|
const tracedRestore = (data: unknown): Result.Result<void, Snapshot.RestoreError> => {
|
|
2603
|
+
if (inTick) 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")
|
|
2385
2604
|
const result = restore(data)
|
|
2386
2605
|
if (tracing) emit({ type: "restore", frame: frameCount, tick: world.currentTick(), ok: result.ok })
|
|
2387
2606
|
return result
|
|
@@ -2533,6 +2752,9 @@ const makeValidatedRuntime = <
|
|
|
2533
2752
|
commandUses,
|
|
2534
2753
|
ranSystems,
|
|
2535
2754
|
requirementViolations: [...requirementViolations.values()],
|
|
2755
|
+
nextStateReplacements: [...nextStateReplacements.values()],
|
|
2756
|
+
staleTargets: [...staleTargets.values()],
|
|
2757
|
+
streamLosses: [...streamLosses.values()],
|
|
2536
2758
|
hasResource: (descriptor) => resources.has(descriptor.key),
|
|
2537
2759
|
schedules: [...scheduleNames].map(([schedule, name]) => [name, schedule as ExecutableScheduleDefinition<S>] as const)
|
|
2538
2760
|
}),
|
|
@@ -2573,8 +2795,9 @@ const makeValidatedRuntime = <
|
|
|
2573
2795
|
* Creates a runtime for a fully built schema and a set of external services.
|
|
2574
2796
|
*
|
|
2575
2797
|
* Resources owned by constructed descriptors take raw input and are validated
|
|
2576
|
-
* here
|
|
2577
|
-
*
|
|
2798
|
+
* here. The runtime always comes back as a `Result`: typed `Success` when
|
|
2799
|
+
* nothing provided can fail (read `.value`), and a `Result` to check when a
|
|
2800
|
+
* constructed resource is provided.
|
|
2578
2801
|
*
|
|
2579
2802
|
* The runtime does not own the outer frame loop. It only owns ECS state plus
|
|
2580
2803
|
* the host-provided services that systems are allowed to depend on.
|
|
@@ -2587,7 +2810,7 @@ const makeValidatedRuntime = <
|
|
|
2587
2810
|
* ),
|
|
2588
2811
|
* resources: { Score: 0 },
|
|
2589
2812
|
* machines: Game.Runtime.machines(Game.Runtime.machine(Flow, "Menu"))
|
|
2590
|
-
* })
|
|
2813
|
+
* }).value
|
|
2591
2814
|
* ```
|
|
2592
2815
|
*/
|
|
2593
2816
|
export const make = <
|
|
@@ -2600,19 +2823,16 @@ export const make = <
|
|
|
2600
2823
|
>(options: {
|
|
2601
2824
|
readonly schema: S
|
|
2602
2825
|
readonly services: ProvidedServices
|
|
2603
|
-
readonly resources?: Resources
|
|
2826
|
+
readonly resources?: Resources & ExactResources<S, Resources>
|
|
2604
2827
|
readonly machines?: ProvidedMachines
|
|
2605
2828
|
readonly machineDefinitions?: ReadonlyArray<Machine.StateMachine.Any>
|
|
2606
2829
|
/** Scopes whose members a committed state change despawns; `Game.Runtime.make` passes every `Game.EntityScope`. */
|
|
2607
2830
|
readonly scopeDefinitions?: ReadonlyArray<EntityScopeModule.EntityScope.Any>
|
|
2608
2831
|
/** Attaches a `debug` handle to the runtime, timed by `hostClock`; see the `Debug` module. */
|
|
2609
2832
|
readonly debug?: Enabled
|
|
2833
|
+
/** The game's save migrations (`Snapshot.migrations`), applied by `restore`. */
|
|
2834
|
+
readonly saves?: Snapshot.Migrations
|
|
2610
2835
|
}): MakeRuntimeResult<S, Simplify<RuntimeServicesOf<ProvidedServices>>, Resources, Root, RuntimeMachinesOf<ProvidedMachines>, Enabled> => {
|
|
2611
|
-
const fallible = Object.entries(options.schema.resources).some(([key, descriptor]) =>
|
|
2612
|
-
options.resources !== undefined
|
|
2613
|
-
&& (options.resources as Record<string, unknown>)[key] !== undefined
|
|
2614
|
-
&& DescriptorModule.hasConstructor(descriptor)
|
|
2615
|
-
)
|
|
2616
2836
|
const validated = collectConstructedRegistryValues(options.schema.resources, options.resources)
|
|
2617
2837
|
if (!validated.ok) {
|
|
2618
2838
|
return Result.failure({ resources: validated.error }) as never
|
|
@@ -2624,9 +2844,43 @@ export const make = <
|
|
|
2624
2844
|
machines: options.machines,
|
|
2625
2845
|
machineDefinitions: options.machineDefinitions ?? [],
|
|
2626
2846
|
scopeDefinitions: options.scopeDefinitions ?? [],
|
|
2627
|
-
debug: options.debug
|
|
2847
|
+
debug: options.debug,
|
|
2848
|
+
saves: options.saves
|
|
2628
2849
|
})
|
|
2629
|
-
return
|
|
2850
|
+
return Result.success(runtime) as never
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
/** Names listed more than once in `Names` (a tuple), in order of their second listing. */
|
|
2854
|
+
type RepeatedNames<Names extends ReadonlyArray<string>, Seen extends string = never> =
|
|
2855
|
+
Names extends readonly [infer Head extends string, ...infer Tail extends ReadonlyArray<string>]
|
|
2856
|
+
? (string extends Head ? never : Head extends Seen ? Head : never) | RepeatedNames<Tail, Seen | Head>
|
|
2857
|
+
: never
|
|
2858
|
+
|
|
2859
|
+
type ProvidedServiceNames<Entries extends ReadonlyArray<ServiceProvision>> = {
|
|
2860
|
+
readonly [K in keyof Entries]: Entries[K] extends ServiceProvision<infer D> ? D["name"] : string
|
|
2861
|
+
}
|
|
2862
|
+
type ProvidedMachineNames<Entries extends ReadonlyArray<MachineProvision>> = {
|
|
2863
|
+
readonly [K in keyof Entries]: Entries[K] extends MachineProvision<infer M> ? M["name"] : string
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
/** Rejects a service provided twice: only the last would be used. */
|
|
2867
|
+
type UniqueServices<Entries extends ReadonlyArray<ServiceProvision>> =
|
|
2868
|
+
[RepeatedNames<ProvidedServiceNames<Entries>>] extends [never] ? unknown : {
|
|
2869
|
+
readonly __fixServices__: `${RepeatedNames<ProvidedServiceNames<Entries>>} is provided twice: keep one Game.Runtime.service(...) for it`
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
/** Rejects a machine started twice: only the last initial value would be used. */
|
|
2873
|
+
type UniqueMachines<Entries extends ReadonlyArray<MachineProvision>> =
|
|
2874
|
+
[RepeatedNames<ProvidedMachineNames<Entries>>] extends [never] ? unknown : {
|
|
2875
|
+
readonly __fixMachines__: `${RepeatedNames<ProvidedMachineNames<Entries>>} is provided twice: keep one Game.Runtime.machine(...) for it`
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
const rejectRepeated = (names: ReadonlyArray<string>, kind: "service" | "machine"): void => {
|
|
2879
|
+
const seen = new Set<string>()
|
|
2880
|
+
for (const name of names) {
|
|
2881
|
+
if (seen.has(name)) throw new Error(`${name} is provided twice: keep one Game.Runtime.${kind}(...) for it`)
|
|
2882
|
+
seen.add(name)
|
|
2883
|
+
}
|
|
2630
2884
|
}
|
|
2631
2885
|
|
|
2632
2886
|
/**
|
|
@@ -2638,6 +2892,8 @@ export const make = <
|
|
|
2638
2892
|
*
|
|
2639
2893
|
* The helper keeps the runtime map keyed by service descriptors instead of
|
|
2640
2894
|
* ad hoc strings, so the provision site cannot drift from the declaration site.
|
|
2895
|
+
* Each service is provided once: a second provision of the same one does not
|
|
2896
|
+
* compile.
|
|
2641
2897
|
*
|
|
2642
2898
|
* @example
|
|
2643
2899
|
* ```ts
|
|
@@ -2650,7 +2906,8 @@ export const make = <
|
|
|
2650
2906
|
*/
|
|
2651
2907
|
export const services = <
|
|
2652
2908
|
const Entries extends ReadonlyArray<ServiceProvision>
|
|
2653
|
-
>(...entries: Entries): RuntimeServices<ServiceEntriesToRecord<Entries>> => {
|
|
2909
|
+
>(...entries: Entries & UniqueServices<Entries>): RuntimeServices<ServiceEntriesToRecord<Entries>> => {
|
|
2910
|
+
rejectRepeated(entries.map((entry) => entry.descriptor.name), "service")
|
|
2654
2911
|
const provided: Record<string, unknown> = {}
|
|
2655
2912
|
for (const { descriptor, implementation } of entries) {
|
|
2656
2913
|
provided[descriptor.name] = implementation
|
|
@@ -2680,11 +2937,14 @@ export const service = <
|
|
|
2680
2937
|
* Builds the machine initialization environment from machine definitions.
|
|
2681
2938
|
*
|
|
2682
2939
|
* Use this when gameplay phases need a known committed starting state before
|
|
2683
|
-
* any schedule runs, for example `"Boot"`, `"Menu"`, or `"Playing"`.
|
|
2940
|
+
* any schedule runs, for example `"Boot"`, `"Menu"`, or `"Playing"`. Each
|
|
2941
|
+
* machine is provided once: a second provision of the same one does not
|
|
2942
|
+
* compile.
|
|
2684
2943
|
*/
|
|
2685
2944
|
export const machines = <
|
|
2686
2945
|
const Entries extends ReadonlyArray<MachineProvision>
|
|
2687
|
-
>(...entries: Entries): RuntimeMachines<MachineEntriesToRecord<Entries>> => {
|
|
2946
|
+
>(...entries: Entries & UniqueMachines<Entries>): RuntimeMachines<MachineEntriesToRecord<Entries>> => {
|
|
2947
|
+
rejectRepeated(entries.map((entry) => entry.machine.name), "machine")
|
|
2688
2948
|
const provided: Record<string, unknown> = {}
|
|
2689
2949
|
for (const { machine, initial } of entries) {
|
|
2690
2950
|
provided[machine.name] = initial
|