@typeonce/ritmo 0.6.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/README.md +2 -0
- package/dist/Brand.d.ts +9 -4
- package/dist/Brand.d.ts.map +1 -1
- package/dist/Brand.js +9 -4
- package/dist/Brand.js.map +1 -1
- package/dist/Clock.d.ts +186 -0
- package/dist/Clock.d.ts.map +1 -0
- package/dist/Clock.js +111 -0
- package/dist/Clock.js.map +1 -0
- package/dist/Command.d.ts +250 -41
- package/dist/Command.d.ts.map +1 -1
- package/dist/Command.js +59 -23
- 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 +163 -15
- package/dist/Debug.d.ts.map +1 -1
- package/dist/Debug.js.map +1 -1
- package/dist/Decode.d.ts +1 -1
- package/dist/Decode.js +1 -1
- package/dist/Descriptor.d.ts +67 -24
- package/dist/Descriptor.d.ts.map +1 -1
- package/dist/Descriptor.js +32 -15
- package/dist/Descriptor.js.map +1 -1
- package/dist/Entity.d.ts +22 -5
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js +2 -2
- package/dist/Entity.js.map +1 -1
- package/dist/EntityScope.d.ts +26 -1
- package/dist/EntityScope.d.ts.map +1 -1
- package/dist/EntityScope.js +14 -2
- package/dist/EntityScope.js.map +1 -1
- package/dist/HostClock.d.ts +49 -0
- package/dist/HostClock.d.ts.map +1 -0
- package/dist/HostClock.js +46 -0
- package/dist/HostClock.js.map +1 -0
- package/dist/Inspector.d.ts +7 -1
- package/dist/Inspector.d.ts.map +1 -1
- package/dist/Inspector.js.map +1 -1
- package/dist/Machine.d.ts +67 -36
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +10 -3
- package/dist/Machine.js.map +1 -1
- package/dist/Query.d.ts +74 -26
- package/dist/Query.d.ts.map +1 -1
- package/dist/Query.js +32 -28
- package/dist/Query.js.map +1 -1
- package/dist/Runtime.d.ts +39 -16
- package/dist/Runtime.d.ts.map +1 -1
- package/dist/Runtime.js +556 -87
- package/dist/Runtime.js.map +1 -1
- package/dist/Schedule.d.ts +217 -10
- package/dist/Schedule.d.ts.map +1 -1
- package/dist/Schedule.js +5 -0
- package/dist/Schedule.js.map +1 -1
- package/dist/Schema.d.ts +196 -44
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +29 -11
- 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 +212 -58
- package/dist/System.d.ts.map +1 -1
- package/dist/System.js +84 -6
- package/dist/System.js.map +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -7
- package/dist/index.js.map +1 -1
- package/dist/internal/cells.d.ts +3 -3
- package/dist/internal/cells.d.ts.map +1 -1
- package/dist/internal/cells.js +9 -11
- package/dist/internal/cells.js.map +1 -1
- package/dist/internal/debug.d.ts +13 -0
- package/dist/internal/debug.d.ts.map +1 -1
- package/dist/internal/debug.js +192 -7
- 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 +41 -6
- package/dist/internal/game.js.map +1 -1
- package/dist/internal/order.d.ts +10 -0
- package/dist/internal/order.d.ts.map +1 -0
- package/dist/internal/order.js +122 -0
- package/dist/internal/order.js.map +1 -0
- package/dist/internal/queries.d.ts.map +1 -1
- package/dist/internal/queries.js +32 -10
- 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/Brand.ts +9 -4
- package/src/Clock.ts +311 -0
- package/src/Command.ts +407 -89
- package/src/Condition.ts +17 -1
- package/src/Debug.ts +172 -14
- package/src/Decode.ts +1 -1
- package/src/Descriptor.ts +92 -37
- package/src/Entity.ts +24 -5
- package/src/EntityScope.ts +27 -2
- package/src/HostClock.ts +64 -0
- package/src/Inspector.ts +7 -2
- package/src/Machine.ts +81 -37
- package/src/Query.ts +110 -38
- package/src/Runtime.ts +607 -111
- package/src/Schedule.ts +380 -13
- package/src/Schema.ts +249 -63
- package/src/Snapshot.ts +4 -0
- package/src/System.ts +248 -93
- package/src/index.ts +8 -7
- package/src/internal/cells.ts +10 -12
- package/src/internal/debug.ts +208 -12
- package/src/internal/equal.ts +31 -0
- package/src/internal/fragments.ts +12 -2
- package/src/internal/game.ts +61 -7
- package/src/internal/order.ts +144 -0
- package/src/internal/queries.ts +37 -13
- package/src/internal/requirements.ts +71 -0
- package/dist/Definition.d.ts +0 -99
- package/dist/Definition.d.ts.map +0 -1
- package/dist/Definition.js +0 -68
- package/dist/Definition.js.map +0 -1
- package/dist/Feature.d.ts +0 -106
- package/dist/Feature.d.ts.map +0 -1
- package/dist/Feature.js +0 -87
- package/dist/Feature.js.map +0 -1
- package/dist/Fx.d.ts +0 -128
- package/dist/Fx.d.ts.map +0 -1
- package/dist/Fx.js +0 -112
- package/dist/Fx.js.map +0 -1
- package/dist/internal/definition.d.ts +0 -17
- package/dist/internal/definition.d.ts.map +0 -1
- package/dist/internal/definition.js +0 -25
- package/dist/internal/definition.js.map +0 -1
- package/src/Definition.ts +0 -106
- package/src/Feature.ts +0 -241
- package/src/Fx.ts +0 -176
- package/src/internal/definition.ts +0 -56
package/src/Runtime.ts
CHANGED
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
* @groupDescription Functions
|
|
56
56
|
* Public runtime constructors and helpers for services, machines, and bootstrap input.
|
|
57
57
|
*/
|
|
58
|
+
import * as HostClockModule from "./HostClock.ts"
|
|
58
59
|
import * as Command from "./Command.ts"
|
|
59
60
|
import type * as Debug from "./Debug.ts"
|
|
60
61
|
import * as DescriptorModule from "./Descriptor.ts"
|
|
@@ -62,8 +63,10 @@ import type { Descriptor } from "./Descriptor.ts"
|
|
|
62
63
|
import type * as Entity from "./Entity.ts"
|
|
63
64
|
import type * as Inspector from "./Inspector.ts"
|
|
64
65
|
import * as Cells from "./internal/cells.ts"
|
|
66
|
+
import { sameValue } from "./internal/equal.ts"
|
|
65
67
|
import * as DebugInternal from "./internal/debug.ts"
|
|
66
68
|
import { makeQueryEngine } from "./internal/queries.ts"
|
|
69
|
+
import { requirementIndex } from "./internal/requirements.ts"
|
|
67
70
|
import * as Streams from "./internal/streams.ts"
|
|
68
71
|
import { ABSENT, makeWorld } from "./internal/world.ts"
|
|
69
72
|
import type { WorldHooks } from "./internal/world.ts"
|
|
@@ -74,13 +77,15 @@ import * as Relation from "./Relation.ts"
|
|
|
74
77
|
import type * as Requirement from "./Requirement.ts"
|
|
75
78
|
import * as Result from "./Result.ts"
|
|
76
79
|
import * as Schedule from "./Schedule.ts"
|
|
80
|
+
import * as OrderInternal from "./internal/order.ts"
|
|
77
81
|
import * as EntityScopeModule from "./EntityScope.ts"
|
|
78
82
|
import * as Snapshot from "./Snapshot.ts"
|
|
79
|
-
import type { ExecutableScheduleDefinition } from "./Schedule.ts"
|
|
83
|
+
import type { AnyScheduleOrder, ExecutableScheduleDefinition } from "./Schedule.ts"
|
|
80
84
|
import type { Registry, Schema } from "./Schema.ts"
|
|
81
85
|
import type {
|
|
82
86
|
DespawnedReadView,
|
|
83
87
|
EventReadView,
|
|
88
|
+
ResourceChangedView,
|
|
84
89
|
EventWriteView,
|
|
85
90
|
LookupApi,
|
|
86
91
|
QueryHandle,
|
|
@@ -179,10 +184,13 @@ export type RuntimeConstructionError<S extends Schema.Any, Provided> = Simplify<
|
|
|
179
184
|
}>
|
|
180
185
|
|
|
181
186
|
/**
|
|
182
|
-
* A runtime plus the `debug` handle when it was made with `debug:
|
|
187
|
+
* A runtime plus the `debug` handle when it was made with `debug: { hostClock }`
|
|
188
|
+
* (optional when the option may be `undefined`).
|
|
183
189
|
*/
|
|
184
190
|
export type WithDebug<R, Enabled, S extends Schema.Any, Root> =
|
|
185
|
-
[Enabled] extends [
|
|
191
|
+
[Enabled] extends [undefined] ? R
|
|
192
|
+
: [Enabled] extends [Debug.Option] ? R & Debug.Member<S, Root>
|
|
193
|
+
: R & Partial<Debug.Member<S, Root>>
|
|
186
194
|
|
|
187
195
|
/**
|
|
188
196
|
* The runtime returned by `make`, wrapped in a `Result` exactly when a
|
|
@@ -367,23 +375,23 @@ type RequirementErrorsFor<
|
|
|
367
375
|
: never
|
|
368
376
|
|
|
369
377
|
type RequirementErrorsOfSchedule<
|
|
370
|
-
Selected extends ExecutableScheduleDefinition<any, any, any, any, any>,
|
|
378
|
+
Selected extends ExecutableScheduleDefinition<any, any, any, any, any, AnyScheduleOrder>,
|
|
371
379
|
Services extends Record<string, unknown>,
|
|
372
380
|
Resources extends object,
|
|
373
381
|
Machines extends object
|
|
374
|
-
> = Selected extends ExecutableScheduleDefinition<infer S, any, any, any, any>
|
|
382
|
+
> = Selected extends ExecutableScheduleDefinition<infer S, any, any, any, any, AnyScheduleOrder>
|
|
375
383
|
? RequirementErrorsFor<Requirement.Of<Selected>, S, Services, Resources, Machines>
|
|
376
384
|
: never
|
|
377
385
|
|
|
378
386
|
export type ValidateSchedules<
|
|
379
|
-
Schedules extends ReadonlyArray<ExecutableScheduleDefinition<any, any, any, any, any>>,
|
|
387
|
+
Schedules extends ReadonlyArray<ExecutableScheduleDefinition<any, any, any, any, any, AnyScheduleOrder>>,
|
|
380
388
|
Services extends Record<string, unknown>,
|
|
381
389
|
Resources extends object,
|
|
382
390
|
Machines extends object
|
|
383
391
|
> = {
|
|
384
392
|
readonly [K in keyof Schedules]:
|
|
385
393
|
Schedules[K] & ValidateSchedule<
|
|
386
|
-
Extract<Schedules[K], ExecutableScheduleDefinition<any, any, any, any, any>>,
|
|
394
|
+
Extract<Schedules[K], ExecutableScheduleDefinition<any, any, any, any, any, AnyScheduleOrder>>,
|
|
387
395
|
Services,
|
|
388
396
|
Resources,
|
|
389
397
|
Machines
|
|
@@ -391,7 +399,7 @@ export type ValidateSchedules<
|
|
|
391
399
|
}
|
|
392
400
|
|
|
393
401
|
export type ValidateScheduleArray<
|
|
394
|
-
Schedules extends ReadonlyArray<ExecutableScheduleDefinition<any, any, any, any, any>>,
|
|
402
|
+
Schedules extends ReadonlyArray<ExecutableScheduleDefinition<any, any, any, any, any, AnyScheduleOrder>>,
|
|
395
403
|
Services extends Record<string, unknown>,
|
|
396
404
|
Resources extends object,
|
|
397
405
|
Machines extends object
|
|
@@ -402,18 +410,23 @@ export type ValidateScheduleArray<
|
|
|
402
410
|
}
|
|
403
411
|
|
|
404
412
|
type ValidateScheduleArgs<
|
|
405
|
-
Schedules extends ReadonlyArray<ExecutableScheduleDefinition<any, any, any, any, any>>,
|
|
413
|
+
Schedules extends ReadonlyArray<ExecutableScheduleDefinition<any, any, any, any, any, AnyScheduleOrder>>,
|
|
406
414
|
Services extends Record<string, unknown>,
|
|
407
415
|
Resources extends object,
|
|
408
416
|
Machines extends object
|
|
409
417
|
> = [RequirementErrorsOfSchedule<NoInfer<Schedules[number]>, Services, Resources, Machines>] extends [never]
|
|
410
|
-
? Schedules
|
|
418
|
+
? [Schedule.TickOrderErrors<NoInfer<Schedules>>] extends [never]
|
|
419
|
+
? Schedules
|
|
420
|
+
: Schedules & {
|
|
421
|
+
/** `Game.System.thisTick` reads whose writers do not all run earlier in this call. */
|
|
422
|
+
readonly __fixTickOrder__: Schedule.TickOrderErrors<NoInfer<Schedules>>
|
|
423
|
+
}
|
|
411
424
|
: Schedules & {
|
|
412
425
|
readonly __fixRuntimeRequirements__: RequirementErrorsOfSchedule<NoInfer<Schedules[number]>, Services, Resources, Machines>
|
|
413
426
|
}
|
|
414
427
|
|
|
415
428
|
type ValidateSchedule<
|
|
416
|
-
Schedule extends ExecutableScheduleDefinition<any, any, any, any, any>,
|
|
429
|
+
Schedule extends ExecutableScheduleDefinition<any, any, any, any, any, AnyScheduleOrder>,
|
|
417
430
|
Services extends Record<string, unknown>,
|
|
418
431
|
Resources extends object,
|
|
419
432
|
Machines extends object
|
|
@@ -432,7 +445,13 @@ type RequirementErrorsOfInspector<
|
|
|
432
445
|
? RequirementErrorsFor<Needs, Spec["schema"], Services, Resources, Machines>
|
|
433
446
|
: never
|
|
434
447
|
|
|
435
|
-
|
|
448
|
+
/**
|
|
449
|
+
* Resolves to `unknown` when the runtime provides every resource, service,
|
|
450
|
+
* and machine the inspector reads, otherwise to an object naming each
|
|
451
|
+
* missing or incompatible one. `runtime.inspect(...)` uses it; tools that
|
|
452
|
+
* run inspectors on a runtime they receive (devtools invariants) reuse it.
|
|
453
|
+
*/
|
|
454
|
+
export type ValidateInspector<
|
|
436
455
|
Selected extends Inspector.Inspector.Any,
|
|
437
456
|
Services extends Record<string, unknown>,
|
|
438
457
|
Resources extends object,
|
|
@@ -451,6 +470,15 @@ export interface MissingRuntimeRequirement {
|
|
|
451
470
|
readonly name: string
|
|
452
471
|
}
|
|
453
472
|
|
|
473
|
+
/**
|
|
474
|
+
* Returned by `tryTick` when a `Game.System.thisTick` read has a writer that
|
|
475
|
+
* runs after it in the schedule. Nothing ran.
|
|
476
|
+
*/
|
|
477
|
+
export interface TickOrderViolation {
|
|
478
|
+
readonly kind: "TickOrderViolation"
|
|
479
|
+
readonly issues: ReadonlyArray<Schedule.OrderIssue>
|
|
480
|
+
}
|
|
481
|
+
|
|
454
482
|
/** Explicit failure returned by the dynamic schedule execution path. */
|
|
455
483
|
export interface MissingRuntimeRequirements {
|
|
456
484
|
readonly kind: "MissingRuntimeRequirements"
|
|
@@ -584,20 +612,22 @@ export interface Runtime<
|
|
|
584
612
|
* system's ECS writes are rolled back and earlier systems stay committed.
|
|
585
613
|
*/
|
|
586
614
|
readonly tick: {
|
|
587
|
-
<const Schedules extends ReadonlyArray<ExecutableScheduleDefinition<S, any, Root, any, any>>>(
|
|
615
|
+
<const Schedules extends ReadonlyArray<ExecutableScheduleDefinition<S, any, Root, any, any, AnyScheduleOrder>>>(
|
|
588
616
|
...schedules: ValidateScheduleArgs<Schedules, Services, Resources, Machines>
|
|
589
617
|
): Result.Result<void, Schedule.FailureOf<Schedules[number]>>
|
|
590
618
|
}
|
|
591
619
|
/**
|
|
592
|
-
* Runs a schedule whose exact requirements are not statically known
|
|
593
|
-
*
|
|
620
|
+
* Runs a schedule whose exact requirements are not statically known: for
|
|
621
|
+
* tools that hold schedules with erased types, such as devtools sessions.
|
|
622
|
+
* Games use `tick(...)`, which rejects missing requirements at compile time.
|
|
594
623
|
*
|
|
595
624
|
* Requirements are checked against current provisioning first and missing
|
|
596
|
-
* ones are returned as data.
|
|
625
|
+
* ones are returned as data, then `Game.System.thisTick` reads are checked
|
|
626
|
+
* against the schedule's order (`TickOrderViolation`).
|
|
597
627
|
*/
|
|
598
|
-
readonly tryTick: <const Selected extends ExecutableScheduleDefinition<S, any, any, any, any>>(
|
|
628
|
+
readonly tryTick: <const Selected extends ExecutableScheduleDefinition<S, any, any, any, any, AnyScheduleOrder>>(
|
|
599
629
|
schedule: Selected
|
|
600
|
-
) => Result.Result<void, MissingRuntimeRequirements | Schedule.FailureOf<Selected>>
|
|
630
|
+
) => Result.Result<void, MissingRuntimeRequirements | TickOrderViolation | Schedule.FailureOf<Selected>>
|
|
601
631
|
/**
|
|
602
632
|
* Captures the world as plain data: entities with their ids, components,
|
|
603
633
|
* relation edges, resources, and committed machine values.
|
|
@@ -678,7 +708,8 @@ const makeValidatedRuntime = <
|
|
|
678
708
|
readonly resources: Partial<Record<string, unknown>>
|
|
679
709
|
readonly machines: RuntimeMachines<any> | undefined
|
|
680
710
|
readonly machineDefinitions: ReadonlyArray<Machine.StateMachine.Any>
|
|
681
|
-
readonly
|
|
711
|
+
readonly scopeDefinitions: ReadonlyArray<EntityScopeModule.EntityScope.Any>
|
|
712
|
+
readonly debug: Debug.Option | undefined
|
|
682
713
|
}): Runtime<S, Services, Resources, Root, Machines> => {
|
|
683
714
|
const world = makeWorld(options.schema, streamCapacity)
|
|
684
715
|
const queries = makeQueryEngine(world)
|
|
@@ -723,12 +754,12 @@ const makeValidatedRuntime = <
|
|
|
723
754
|
const pendingCommands: Array<Command.DeferredCommand<S>> = []
|
|
724
755
|
|
|
725
756
|
/**
|
|
726
|
-
* Debug state. With `debug
|
|
757
|
+
* Debug state. With `debug`, `pendingOrigins` names the system that
|
|
727
758
|
* queued each pending command, in step with `pendingCommands`. Tracing is
|
|
728
759
|
* on only while `observe` listeners are registered; untraced runs pay one
|
|
729
760
|
* boolean check at each system, schedule, and marker boundary.
|
|
730
761
|
*/
|
|
731
|
-
const debugEnabled = options.debug
|
|
762
|
+
const debugEnabled = options.debug !== undefined
|
|
732
763
|
const pendingOrigins: Array<string> = []
|
|
733
764
|
const traceListeners = new Set<(event: Debug.TraceEvent) => void>()
|
|
734
765
|
let tracing = false
|
|
@@ -740,7 +771,9 @@ const makeValidatedRuntime = <
|
|
|
740
771
|
const scheduleNames = new Map<object, string>()
|
|
741
772
|
const anonymousScheduleNames = new WeakMap<object, string>()
|
|
742
773
|
let anonymousScheduleCount = 0
|
|
743
|
-
|
|
774
|
+
// Only a debug runtime times anything; the clock is the one it was given.
|
|
775
|
+
const hostClock: HostClockModule.HostClock = options.debug?.hostClock ?? { now: () => 0 }
|
|
776
|
+
const now = (): number => hostClock.now()
|
|
744
777
|
const emit = (event: Debug.TraceEvent): void => {
|
|
745
778
|
for (const listener of traceListeners) listener(event)
|
|
746
779
|
}
|
|
@@ -772,8 +805,48 @@ const makeValidatedRuntime = <
|
|
|
772
805
|
options.machineDefinitions.map((machine, index) => [machine.key, index] as const)
|
|
773
806
|
)
|
|
774
807
|
|
|
808
|
+
/** Machines the runtime was given an initial value for. */
|
|
809
|
+
const providedMachineKeys = new Set<symbol>()
|
|
775
810
|
for (const provision of machineEntries) {
|
|
776
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
|
+
}
|
|
777
850
|
}
|
|
778
851
|
|
|
779
852
|
const appendRelationFailure = (
|
|
@@ -797,29 +870,148 @@ const makeValidatedRuntime = <
|
|
|
797
870
|
])
|
|
798
871
|
}
|
|
799
872
|
|
|
873
|
+
/**
|
|
874
|
+
* With `debug`, the relations commands have tried to relate or
|
|
875
|
+
* reorder, with the systems that queued those commands, for the
|
|
876
|
+
* `relation-failure-never-read` lint. `commandOrigin` names the system whose
|
|
877
|
+
* command is being applied.
|
|
878
|
+
*/
|
|
879
|
+
const relationMutations = new Map<string, Set<string>>()
|
|
880
|
+
let commandOrigin = "(unknown)"
|
|
881
|
+
/**
|
|
882
|
+
* With `debug`, what each system's applied commands did, as
|
|
883
|
+
* `kind:name` (`spawns:Enemy`, `despawns:Enemy` for every component the
|
|
884
|
+
* despawned entity had), and the systems that ran: for the
|
|
885
|
+
* `command-permission-unused` lint.
|
|
886
|
+
*/
|
|
887
|
+
const commandUses = new Map<string, Set<string>>()
|
|
888
|
+
const ranSystems = new Set<string>()
|
|
889
|
+
const noteCommand = (kind: string, name: string): void => {
|
|
890
|
+
let uses = commandUses.get(commandOrigin)
|
|
891
|
+
if (uses === undefined) {
|
|
892
|
+
uses = new Set()
|
|
893
|
+
commandUses.set(commandOrigin, uses)
|
|
894
|
+
}
|
|
895
|
+
uses.add(`${kind}:${name}`)
|
|
896
|
+
}
|
|
897
|
+
const noteRelationMutation = (relation: Relation.Relation.Any): void => {
|
|
898
|
+
let systems = relationMutations.get(relation.name)
|
|
899
|
+
if (systems === undefined) {
|
|
900
|
+
systems = new Set()
|
|
901
|
+
relationMutations.set(relation.name, systems)
|
|
902
|
+
}
|
|
903
|
+
systems.add(commandOrigin)
|
|
904
|
+
}
|
|
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
|
+
|
|
800
948
|
/**
|
|
801
949
|
* World adapter used by deferred commands.
|
|
802
950
|
*/
|
|
803
951
|
const commandWorld: Command.InternalWorld<S> = {
|
|
804
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
|
+
}
|
|
960
|
+
if (debugEnabled) for (const [descriptor] of components) noteCommand("spawns", descriptor.name)
|
|
805
961
|
world.spawnEntity(id, components)
|
|
962
|
+
return true
|
|
806
963
|
},
|
|
807
964
|
destroyEntity(id) {
|
|
965
|
+
if (debugEnabled) {
|
|
966
|
+
const record = world.records.get(id)
|
|
967
|
+
record?.values.forEach((value, ordinal) => {
|
|
968
|
+
if (value !== ABSENT) noteCommand("despawns", world.descriptorAt(ordinal).name)
|
|
969
|
+
})
|
|
970
|
+
}
|
|
808
971
|
world.destroyEntity(id)
|
|
809
972
|
},
|
|
810
973
|
assignEntityScope(id, scope) {
|
|
811
974
|
world.assignEntityScope(id, scope.key)
|
|
812
975
|
},
|
|
813
976
|
destroyEntityScope(scope) {
|
|
977
|
+
if (debugEnabled) noteCommand("despawnsScope", scope.name)
|
|
814
978
|
world.destroyEntityScope(scope.key)
|
|
815
979
|
},
|
|
816
|
-
|
|
817
|
-
world.
|
|
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
|
+
}
|
|
818
996
|
},
|
|
819
|
-
|
|
820
|
-
world.
|
|
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
|
+
}
|
|
821
1009
|
},
|
|
822
1010
|
tryRelate(id, relation, target) {
|
|
1011
|
+
if (debugEnabled) {
|
|
1012
|
+
noteRelationMutation(relation)
|
|
1013
|
+
noteCommand("relates", relation.name)
|
|
1014
|
+
}
|
|
823
1015
|
const result = world.tryRelate(id, relation, target)
|
|
824
1016
|
if (!result.ok) {
|
|
825
1017
|
appendRelationFailure(relation, "relate", id, target, result.error)
|
|
@@ -827,9 +1019,14 @@ const makeValidatedRuntime = <
|
|
|
827
1019
|
return result
|
|
828
1020
|
},
|
|
829
1021
|
unrelate(id, relation) {
|
|
1022
|
+
if (debugEnabled) noteCommand("relates", relation.name)
|
|
830
1023
|
world.unrelate(id, relation)
|
|
831
1024
|
},
|
|
832
1025
|
reorderChildren(id, relation, children) {
|
|
1026
|
+
if (debugEnabled) {
|
|
1027
|
+
noteRelationMutation(relation)
|
|
1028
|
+
noteCommand("relates", relation.name)
|
|
1029
|
+
}
|
|
833
1030
|
const result = world.reorderChildren(id, relation, children.map((child) => child))
|
|
834
1031
|
if (!result.ok) {
|
|
835
1032
|
const targetId = result.error._tag === "MissingChildEntity"
|
|
@@ -882,12 +1079,6 @@ const makeValidatedRuntime = <
|
|
|
882
1079
|
|
|
883
1080
|
const makeLookup = (reader: ReaderState): LookupApi<S, Root> => {
|
|
884
1081
|
const lookup: LookupApi<S, Root> = {
|
|
885
|
-
get(target, query) {
|
|
886
|
-
return resolve(target, query, reader)
|
|
887
|
-
},
|
|
888
|
-
getHandle(handle, query) {
|
|
889
|
-
return resolve(handle, query, reader)
|
|
890
|
-
},
|
|
891
1082
|
related: relatedTarget,
|
|
892
1083
|
relatedSources(target, relation) {
|
|
893
1084
|
if (!world.records.has(target)) {
|
|
@@ -895,19 +1086,6 @@ const makeValidatedRuntime = <
|
|
|
895
1086
|
}
|
|
896
1087
|
return Result.success(world.relatedSourceIds(relation, target).map(entityId))
|
|
897
1088
|
},
|
|
898
|
-
childMatches(target, relation, query) {
|
|
899
|
-
if (!world.records.has(target)) {
|
|
900
|
-
return Result.failure(Relation.missingEntityError(target))
|
|
901
|
-
}
|
|
902
|
-
const matches: Array<QueryMatch<S, typeof query>> = []
|
|
903
|
-
for (const sourceId of world.relatedSourceIds(relation, target)) {
|
|
904
|
-
const resolved = resolve(sourceId, query, reader)
|
|
905
|
-
if (resolved.ok) {
|
|
906
|
-
matches.push(resolved.value)
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
return Result.success(matches)
|
|
910
|
-
},
|
|
911
1089
|
parent: relatedTarget,
|
|
912
1090
|
ancestors(target, relation) {
|
|
913
1091
|
if (!world.records.has(target)) {
|
|
@@ -946,20 +1124,6 @@ const makeValidatedRuntime = <
|
|
|
946
1124
|
}
|
|
947
1125
|
return Result.success(descendants)
|
|
948
1126
|
},
|
|
949
|
-
descendantMatches(target, relation, query, options) {
|
|
950
|
-
const descendants = lookup.descendants(target, relation, options)
|
|
951
|
-
if (!descendants.ok) {
|
|
952
|
-
return descendants
|
|
953
|
-
}
|
|
954
|
-
const matches: Array<QueryMatch<S, typeof query>> = []
|
|
955
|
-
for (const descendantId of descendants.value) {
|
|
956
|
-
const resolved = resolve(descendantId, query, reader)
|
|
957
|
-
if (resolved.ok) {
|
|
958
|
-
matches.push(resolved.value)
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
return Result.success(matches)
|
|
962
|
-
},
|
|
963
1127
|
root(target, relation) {
|
|
964
1128
|
if (!world.records.has(target)) {
|
|
965
1129
|
return Result.failure(Relation.missingEntityError(target))
|
|
@@ -976,13 +1140,32 @@ const makeValidatedRuntime = <
|
|
|
976
1140
|
return lookup
|
|
977
1141
|
}
|
|
978
1142
|
|
|
979
|
-
const makeQueryHandle = <Q extends Query.Query.Any<Root>>(query: Q, reader: ReaderState): QueryHandle<S, Q> => {
|
|
1143
|
+
const makeQueryHandle = <Q extends Query.Query.Any<Root>>(query: Q, reader: ReaderState, lookup: LookupApi<S, Root>): QueryHandle<S, Q> => {
|
|
980
1144
|
const each = () => queries.each(query, reader.since) as ReadonlyArray<QueryMatch<S, Q>>
|
|
1145
|
+
const matching = (ids: ReadonlyArray<Entity.EntityId<S, Root>>): Array<QueryMatch<S, Q>> => {
|
|
1146
|
+
const matches: Array<QueryMatch<S, Q>> = []
|
|
1147
|
+
for (const id of ids) {
|
|
1148
|
+
const resolved = resolve(id, query, reader)
|
|
1149
|
+
if (resolved.ok) matches.push(resolved.value)
|
|
1150
|
+
}
|
|
1151
|
+
return matches
|
|
1152
|
+
}
|
|
981
1153
|
return {
|
|
982
1154
|
each,
|
|
983
1155
|
get(target) {
|
|
984
1156
|
return resolve(target, query, reader)
|
|
985
1157
|
},
|
|
1158
|
+
getHandle(handle) {
|
|
1159
|
+
return resolve(handle as Entity.Handle<Root, any>, query, reader)
|
|
1160
|
+
},
|
|
1161
|
+
children(target, relation) {
|
|
1162
|
+
if (!world.records.has(target)) return Result.failure(Relation.missingEntityError(target))
|
|
1163
|
+
return Result.success(matching(world.relatedSourceIds(relation, target).map(entityId)))
|
|
1164
|
+
},
|
|
1165
|
+
descendants(target, relation, options) {
|
|
1166
|
+
const ids = lookup.descendants(target, relation, options)
|
|
1167
|
+
return ids.ok ? Result.success(matching(ids.value)) : ids
|
|
1168
|
+
},
|
|
986
1169
|
single() {
|
|
987
1170
|
const matches = each()
|
|
988
1171
|
if (matches.length === 0) {
|
|
@@ -1024,6 +1207,15 @@ const makeValidatedRuntime = <
|
|
|
1024
1207
|
}
|
|
1025
1208
|
const writeResource = journalStoreWrite(resources, resourceOriginals)
|
|
1026
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
|
+
|
|
1027
1219
|
const journalMachine = (key: symbol): void => {
|
|
1028
1220
|
if (!machineOriginals.has(key)) {
|
|
1029
1221
|
machineOriginals.set(key, pendingMachines.get(key) ?? absentValue)
|
|
@@ -1036,6 +1228,14 @@ const makeValidatedRuntime = <
|
|
|
1036
1228
|
|
|
1037
1229
|
const commitSystemTransaction = (): void => {
|
|
1038
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
|
+
}
|
|
1039
1239
|
if (emittedEvents.size > 0) {
|
|
1040
1240
|
const published = world.advanceTick()
|
|
1041
1241
|
for (const [key, values] of emittedEvents) {
|
|
@@ -1078,6 +1278,17 @@ const makeValidatedRuntime = <
|
|
|
1078
1278
|
write: (key: symbol, value: unknown) => void
|
|
1079
1279
|
) => Cells.storeWrite(store, descriptor.key, write, DescriptorModule.constructorOf(descriptor))
|
|
1080
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
|
+
|
|
1081
1292
|
const makeEventReadView = <T>(descriptorKey: symbol, reader: ReaderState): EventReadView<T> => ({
|
|
1082
1293
|
all() {
|
|
1083
1294
|
return events.since(descriptorKey, reader.streamSince) as ReadonlyArray<ReadonlyValue<T>>
|
|
@@ -1115,6 +1326,9 @@ const makeValidatedRuntime = <
|
|
|
1115
1326
|
return {
|
|
1116
1327
|
all() {
|
|
1117
1328
|
return world.removedSince(ordinal, reader.since).map(entityId)
|
|
1329
|
+
},
|
|
1330
|
+
lagged() {
|
|
1331
|
+
return world.removedLagged(ordinal, reader.since, reader.registeredAt)
|
|
1118
1332
|
}
|
|
1119
1333
|
}
|
|
1120
1334
|
}
|
|
@@ -1122,6 +1336,9 @@ const makeValidatedRuntime = <
|
|
|
1122
1336
|
const makeDespawnedReadView = (reader: ReaderState): DespawnedReadView<S, Root> => ({
|
|
1123
1337
|
all() {
|
|
1124
1338
|
return world.despawnedSince(reader.since).map(entityId)
|
|
1339
|
+
},
|
|
1340
|
+
lagged() {
|
|
1341
|
+
return world.despawnedLagged(reader.since, reader.registeredAt)
|
|
1125
1342
|
}
|
|
1126
1343
|
})
|
|
1127
1344
|
|
|
@@ -1138,19 +1355,33 @@ const makeValidatedRuntime = <
|
|
|
1138
1355
|
})
|
|
1139
1356
|
|
|
1140
1357
|
const makeMachineReadView = <M extends Machine.StateMachine.Any>(stateMachine: M): MachineReadView<M> => ({
|
|
1141
|
-
get: () => currentMachines.get(stateMachine.key) as Machine.
|
|
1358
|
+
get: () => currentMachines.get(stateMachine.key) as Machine.CurrentValue<M>,
|
|
1142
1359
|
is: (value) => currentMachines.get(stateMachine.key) === value
|
|
1143
1360
|
})
|
|
1144
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
|
+
|
|
1145
1378
|
const makeNextMachineWriteView = <M extends Machine.StateMachine.Any>(stateMachine: M): NextMachineWriteView<M> => ({
|
|
1146
1379
|
getPending: () => pendingMachines.get(stateMachine.key)?.value as Machine.StateMachine.Value<M> | undefined,
|
|
1147
1380
|
set(value) {
|
|
1148
|
-
|
|
1149
|
-
pendingMachines.set(stateMachine.key, { value, skipIfSame: false })
|
|
1381
|
+
return queueMachine(stateMachine, value, false)
|
|
1150
1382
|
},
|
|
1151
1383
|
setIfChanged(value) {
|
|
1152
|
-
|
|
1153
|
-
pendingMachines.set(stateMachine.key, { value, skipIfSame: true })
|
|
1384
|
+
return queueMachine(stateMachine, value, true)
|
|
1154
1385
|
},
|
|
1155
1386
|
reset() {
|
|
1156
1387
|
journalMachine(stateMachine.key)
|
|
@@ -1199,16 +1430,19 @@ const makeValidatedRuntime = <
|
|
|
1199
1430
|
* views the implementation is allowed to see. Views read live storage, so
|
|
1200
1431
|
* one context is built per system and reused for every run.
|
|
1201
1432
|
*/
|
|
1202
|
-
const makeContext = (system: SystemDefinition<any, any, any>, reader: ReaderState): SystemContext<any> => {
|
|
1433
|
+
const makeContext = (system: SystemDefinition<any, any, any>, reader: ReaderState, queue: Command.CommandQueue<S, Root>): SystemContext<any> => {
|
|
1203
1434
|
const spec = system.spec
|
|
1435
|
+
const lookup = makeLookup(reader)
|
|
1204
1436
|
return {
|
|
1205
1437
|
local: (spec.local as (() => unknown) | undefined)?.(),
|
|
1206
|
-
queries: mapRecord(spec.queries as Record<string, Query.Query.Any<Root>>, (query) => makeQueryHandle(query, reader)),
|
|
1207
|
-
lookup
|
|
1438
|
+
queries: mapRecord(spec.queries as Record<string, Query.Query.Any<Root>>, (query) => makeQueryHandle(query, reader, lookup)),
|
|
1439
|
+
lookup,
|
|
1208
1440
|
resources: mapRecord(spec.resources as Record<string, any>, (access) =>
|
|
1209
1441
|
access.mode === "read"
|
|
1210
1442
|
? Cells.storeRead(resources, access.descriptor.key)
|
|
1211
|
-
:
|
|
1443
|
+
: access.mode === "changed"
|
|
1444
|
+
? makeResourceChangedView(access.descriptor, reader)
|
|
1445
|
+
: makeResourceWriteView(access.descriptor, resources, writeResource)),
|
|
1212
1446
|
events: mapRecord(spec.events as Record<string, any>, (access) =>
|
|
1213
1447
|
access.mode === "read"
|
|
1214
1448
|
? makeEventReadView(access.descriptor.key, reader)
|
|
@@ -1222,12 +1456,15 @@ const makeValidatedRuntime = <
|
|
|
1222
1456
|
relationFailures: mapRecord(spec.relationFailures as Record<string, any>, (access) => makeRelationFailureReadView(access.relation, reader)),
|
|
1223
1457
|
services: mapRecord(spec.services as Record<string, any>, (access) =>
|
|
1224
1458
|
providedServices[access.descriptor.name as keyof Services]),
|
|
1225
|
-
|
|
1459
|
+
// The queue serves every declared permission set; the spec's types narrow it.
|
|
1460
|
+
commands: queue as unknown
|
|
1226
1461
|
} as SystemContext<any>
|
|
1227
1462
|
}
|
|
1228
1463
|
|
|
1229
1464
|
interface SystemSlot {
|
|
1230
1465
|
readonly context: SystemContext<any>
|
|
1466
|
+
/** The system's command queue (its context's `commands`, unchecked). */
|
|
1467
|
+
readonly commands: Command.CommandQueue<S, Root>
|
|
1231
1468
|
readonly reader: ReaderState
|
|
1232
1469
|
}
|
|
1233
1470
|
|
|
@@ -1316,7 +1553,7 @@ const makeValidatedRuntime = <
|
|
|
1316
1553
|
system: system.name,
|
|
1317
1554
|
outcome,
|
|
1318
1555
|
...(outcome === "ok" ? {} : { error }),
|
|
1319
|
-
ms: now()
|
|
1556
|
+
ms: HostClockModule.elapsed(started, now()),
|
|
1320
1557
|
writes,
|
|
1321
1558
|
resources: resourceWrites,
|
|
1322
1559
|
events: emitted,
|
|
@@ -1383,7 +1620,8 @@ const makeValidatedRuntime = <
|
|
|
1383
1620
|
let slot = slots.get(stateKey(system))
|
|
1384
1621
|
if (!slot) {
|
|
1385
1622
|
const reader: ReaderState = { since: 0, lastRun: 0, streamSince: 0, streamLastRun: 0, registeredAt: world.currentTick() }
|
|
1386
|
-
|
|
1623
|
+
const commands = Command.makeCommands<S, Root>(() => world.allocateEntity() as Entity.EntityId<S, Root>)
|
|
1624
|
+
slot = { context: makeContext(system, reader, commands), commands, reader }
|
|
1387
1625
|
slots.set(stateKey(system), slot)
|
|
1388
1626
|
if (holdsStreams) registerStreamReader(system, slot.reader)
|
|
1389
1627
|
const make = system.spec.local as (() => unknown) | undefined
|
|
@@ -1422,7 +1660,8 @@ const makeValidatedRuntime = <
|
|
|
1422
1660
|
return succeeded
|
|
1423
1661
|
}
|
|
1424
1662
|
}
|
|
1425
|
-
const { context, reader } = slotOf(system, true)
|
|
1663
|
+
const { context, commands, reader } = slotOf(system, true)
|
|
1664
|
+
if (debugEnabled) ranSystems.add(system.name)
|
|
1426
1665
|
// Changes stamped after the previous completed run are visible to this run.
|
|
1427
1666
|
reader.since = reader.lastRun
|
|
1428
1667
|
reader.streamSince = reader.streamLastRun
|
|
@@ -1431,23 +1670,21 @@ const makeValidatedRuntime = <
|
|
|
1431
1670
|
beginSystemTransaction()
|
|
1432
1671
|
let outcome: Result.Result<unknown, unknown>
|
|
1433
1672
|
try {
|
|
1434
|
-
// A system that returns nothing cannot fail
|
|
1435
|
-
|
|
1436
|
-
const effect = system.run(context)
|
|
1437
|
-
outcome = effect === undefined ? succeeded : effect.run(context.services)
|
|
1673
|
+
// A system that returns nothing cannot fail; otherwise it returned a `Result`.
|
|
1674
|
+
outcome = system.run(context) ?? succeeded
|
|
1438
1675
|
} catch (defect) {
|
|
1439
|
-
if (tracing) emit(traceSystem(system, reader, thisRun, started, "defect", defect,
|
|
1676
|
+
if (tracing) emit(traceSystem(system, reader, thisRun, started, "defect", defect, commands.flush()))
|
|
1440
1677
|
rollbackSystemTransaction()
|
|
1441
|
-
|
|
1678
|
+
commands.flush()
|
|
1442
1679
|
throw defect
|
|
1443
1680
|
}
|
|
1444
1681
|
if (!outcome.ok) {
|
|
1445
|
-
if (tracing) emit(traceSystem(system, reader, thisRun, started, "failed", outcome.error,
|
|
1682
|
+
if (tracing) emit(traceSystem(system, reader, thisRun, started, "failed", outcome.error, commands.flush()))
|
|
1446
1683
|
rollbackSystemTransaction()
|
|
1447
|
-
|
|
1684
|
+
commands.flush()
|
|
1448
1685
|
return Result.failure({ kind: "SystemFailure", system: system.name, error: outcome.error })
|
|
1449
1686
|
}
|
|
1450
|
-
const queued =
|
|
1687
|
+
const queued = commands.flush()
|
|
1451
1688
|
const traced = tracing ? traceSystem(system, reader, thisRun, started, "ok", undefined, queued) : undefined
|
|
1452
1689
|
commitSystemTransaction()
|
|
1453
1690
|
// A failed run leaves `lastRun` unchanged, so the next run sees the same changes again.
|
|
@@ -1469,8 +1706,15 @@ const makeValidatedRuntime = <
|
|
|
1469
1706
|
const origins = pendingOrigins.splice(0, pendingOrigins.length)
|
|
1470
1707
|
world.advanceTick()
|
|
1471
1708
|
if (!tracing) {
|
|
1472
|
-
|
|
1473
|
-
command
|
|
1709
|
+
if (!debugEnabled) {
|
|
1710
|
+
for (const command of commands) {
|
|
1711
|
+
command.apply(commandWorld)
|
|
1712
|
+
}
|
|
1713
|
+
return
|
|
1714
|
+
}
|
|
1715
|
+
for (let index = 0; index < commands.length; index++) {
|
|
1716
|
+
commandOrigin = origins[index] ?? "(unknown)"
|
|
1717
|
+
commands[index]!.apply(commandWorld)
|
|
1474
1718
|
}
|
|
1475
1719
|
return
|
|
1476
1720
|
}
|
|
@@ -1479,6 +1723,7 @@ const makeValidatedRuntime = <
|
|
|
1479
1723
|
const command = commands[index]!
|
|
1480
1724
|
const effects: Array<Debug.Effect> = []
|
|
1481
1725
|
commandEffects = effects
|
|
1726
|
+
commandOrigin = origins[index] ?? "(unknown)"
|
|
1482
1727
|
try {
|
|
1483
1728
|
command.apply(commandWorld)
|
|
1484
1729
|
} finally {
|
|
@@ -1503,7 +1748,7 @@ const makeValidatedRuntime = <
|
|
|
1503
1748
|
} finally {
|
|
1504
1749
|
schedulePath.pop()
|
|
1505
1750
|
}
|
|
1506
|
-
emit({ type: "schedule.end", frame: frameCount, schedule: scheduleLabel, ok: result.ok, ms: now()
|
|
1751
|
+
emit({ type: "schedule.end", frame: frameCount, schedule: scheduleLabel, ok: result.ok, ms: HostClockModule.elapsed(started, now()) })
|
|
1507
1752
|
return result
|
|
1508
1753
|
}
|
|
1509
1754
|
|
|
@@ -1541,6 +1786,36 @@ const makeValidatedRuntime = <
|
|
|
1541
1786
|
}
|
|
1542
1787
|
}
|
|
1543
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
|
+
|
|
1544
1819
|
/**
|
|
1545
1820
|
* Applies queued commands, then queued machine transitions, running the
|
|
1546
1821
|
* matching exit, transition, and enter schedules from the bundle.
|
|
@@ -1558,6 +1833,10 @@ const makeValidatedRuntime = <
|
|
|
1558
1833
|
|
|
1559
1834
|
const schedules = bundle?.entries ?? []
|
|
1560
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
|
+
}
|
|
1561
1840
|
pendingMachines.delete(machineKey)
|
|
1562
1841
|
const current = currentMachines.get(machineKey)
|
|
1563
1842
|
if (current === undefined) {
|
|
@@ -1568,12 +1847,25 @@ const makeValidatedRuntime = <
|
|
|
1568
1847
|
continue
|
|
1569
1848
|
}
|
|
1570
1849
|
|
|
1571
|
-
previousMachines.set(machineKey, current)
|
|
1572
1850
|
const snapshot = {
|
|
1573
1851
|
from: current as Machine.StateValue,
|
|
1574
1852
|
to: pending.value as Machine.StateValue
|
|
1575
1853
|
}
|
|
1576
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
|
+
|
|
1577
1869
|
for (const schedule of schedules) {
|
|
1578
1870
|
const transition = schedule.transition
|
|
1579
1871
|
if (transition.phase === "exit" && transition.machine.key === machineKey && transition.state === snapshot.from) {
|
|
@@ -1603,10 +1895,8 @@ const makeValidatedRuntime = <
|
|
|
1603
1895
|
}
|
|
1604
1896
|
}
|
|
1605
1897
|
|
|
1606
|
-
currentMachines.
|
|
1607
|
-
|
|
1608
|
-
transitionEvents.append(machineKey, world.advanceTick(), [snapshot])
|
|
1609
|
-
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)
|
|
1610
1900
|
|
|
1611
1901
|
for (const schedule of schedules) {
|
|
1612
1902
|
const transition = schedule.transition
|
|
@@ -1618,6 +1908,82 @@ const makeValidatedRuntime = <
|
|
|
1618
1908
|
}
|
|
1619
1909
|
}
|
|
1620
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
|
|
1621
1987
|
}
|
|
1622
1988
|
return succeeded
|
|
1623
1989
|
}
|
|
@@ -1661,7 +2027,7 @@ const makeValidatedRuntime = <
|
|
|
1661
2027
|
* Runs one schedule. A failing system stops the schedule; systems that
|
|
1662
2028
|
* already succeeded stay committed, and their pending work stays pending.
|
|
1663
2029
|
*/
|
|
1664
|
-
const runScheduleUnsafe = (schedule: ExecutableScheduleDefinition<S, any, any, any, any>): Result.Result<void, SystemFailure> => {
|
|
2030
|
+
const runScheduleUnsafe = (schedule: ExecutableScheduleDefinition<S, any, any, any, any, AnyScheduleOrder>): Result.Result<void, SystemFailure> => {
|
|
1665
2031
|
changedMachines = new Set()
|
|
1666
2032
|
return tracing
|
|
1667
2033
|
? traceSchedule(scheduleName(schedule), () => runSteps(schedule.steps))
|
|
@@ -1670,6 +2036,13 @@ const makeValidatedRuntime = <
|
|
|
1670
2036
|
|
|
1671
2037
|
const eventsByKey = new Set((Object.values(options.schema.events) as ReadonlyArray<Descriptor.Any>).map((descriptor) => descriptor.key))
|
|
1672
2038
|
const externallyWrittenEvents = new Set<string>()
|
|
2039
|
+
/** `debug.observeHost` listeners: ticks and sends, without tracing. Only a debug runtime registers any. */
|
|
2040
|
+
const hostListeners = new Set<(event: Debug.HostEvent<S, Root>) => void>()
|
|
2041
|
+
const emitHost = (event: Debug.HostEvent<S, Root>): void => {
|
|
2042
|
+
for (const listener of hostListeners) listener(event)
|
|
2043
|
+
}
|
|
2044
|
+
/** Events declared as sent by the host (`debug.declareHostEvents`). */
|
|
2045
|
+
let hostEvents = new Set<string>()
|
|
1673
2046
|
|
|
1674
2047
|
const send = (descriptor: Descriptor<"event", string, any>, value: unknown): void => {
|
|
1675
2048
|
if (ticking) throw new Error(`runtime.send(${descriptor.name}) was called while a schedule is running: emit from a system instead`)
|
|
@@ -1679,6 +2052,7 @@ const makeValidatedRuntime = <
|
|
|
1679
2052
|
// Bound host traffic even when it sends repeatedly without ticking.
|
|
1680
2053
|
events.trim(0)
|
|
1681
2054
|
externallyWrittenEvents.add(descriptor.name)
|
|
2055
|
+
if (hostListeners.size > 0) emitHost({ type: "send", event: descriptor.name, value })
|
|
1682
2056
|
if (tracing) emit({ type: "runtime.sent", frame: frameCount, tick: published, event: descriptor.name, value })
|
|
1683
2057
|
}
|
|
1684
2058
|
|
|
@@ -1691,21 +2065,91 @@ const makeValidatedRuntime = <
|
|
|
1691
2065
|
relationFailures.trim(boundary)
|
|
1692
2066
|
}
|
|
1693
2067
|
|
|
1694
|
-
|
|
2068
|
+
/**
|
|
2069
|
+
* With debug: the last failed tick result, until its `ok` is read. The
|
|
2070
|
+
* result handed back is a copy whose `ok` getter marks it read.
|
|
2071
|
+
*/
|
|
2072
|
+
let unread: { read: boolean; readonly event: Omit<Debug.FailureIgnoredEvent, "type" | "when"> } | undefined
|
|
2073
|
+
const reportUnread = (when: Debug.FailureIgnoredEvent["when"]): void => {
|
|
2074
|
+
const pending = unread
|
|
2075
|
+
unread = undefined
|
|
2076
|
+
if (pending === undefined || pending.read) return
|
|
2077
|
+
const event: Debug.FailureIgnoredEvent = { type: "failure.ignored", ...pending.event, when }
|
|
2078
|
+
emit(event)
|
|
2079
|
+
emitHost(event)
|
|
2080
|
+
}
|
|
2081
|
+
const watchResult = <R extends Result.Result<void, unknown>>(
|
|
2082
|
+
result: R,
|
|
2083
|
+
schedules: ReadonlyArray<ExecutableScheduleDefinition<S, any, any, any, any, AnyScheduleOrder>>
|
|
2084
|
+
): R => {
|
|
2085
|
+
if (!debugEnabled || result.ok) return result
|
|
2086
|
+
const error = result.error
|
|
2087
|
+
const system = typeof error === "object" && error !== null && (error as { readonly kind?: unknown }).kind === "SystemFailure"
|
|
2088
|
+
? (error as SystemFailure).system
|
|
2089
|
+
: undefined
|
|
2090
|
+
const names = schedules.flatMap((schedule) => schedule.steps.filter(Schedule.isSystemStep).map((step) => (step as SystemDefinition<any, any, any>).name))
|
|
2091
|
+
const at = system === undefined ? -1 : names.indexOf(system)
|
|
2092
|
+
const holder = system === undefined
|
|
2093
|
+
? schedules[0]
|
|
2094
|
+
: schedules.find((schedule) => schedule.steps.some((step) => Schedule.isSystemStep(step) && (step as SystemDefinition<any, any, any>).name === system)) ?? schedules[0]
|
|
2095
|
+
const entry = {
|
|
2096
|
+
read: false,
|
|
2097
|
+
event: {
|
|
2098
|
+
frame: frameCount,
|
|
2099
|
+
schedule: holder === undefined ? "(none)" : scheduleName(holder),
|
|
2100
|
+
system,
|
|
2101
|
+
error,
|
|
2102
|
+
skipped: [...new Set(names.slice(at + 1))]
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
unread = entry
|
|
2106
|
+
return {
|
|
2107
|
+
get ok() {
|
|
2108
|
+
entry.read = true
|
|
2109
|
+
return false
|
|
2110
|
+
},
|
|
2111
|
+
error
|
|
2112
|
+
} as unknown as R
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
const tickUnsafe = (schedules: ReadonlyArray<ExecutableScheduleDefinition<S, any, any, any, any, AnyScheduleOrder>>): Result.Result<void, SystemFailure> => {
|
|
1695
2116
|
if (ticking) throw new Error("runtime.tick cannot run inside another tick")
|
|
2117
|
+
if (debugEnabled) reportUnread("next-tick")
|
|
1696
2118
|
ticking = true
|
|
1697
2119
|
try {
|
|
1698
2120
|
advanceFrame()
|
|
1699
|
-
|
|
1700
|
-
const
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
2121
|
+
return watchResult(observedTick(schedules, () => {
|
|
2122
|
+
for (const schedule of schedules) {
|
|
2123
|
+
const result = runScheduleUnsafe(schedule)
|
|
2124
|
+
if (!result.ok) return result
|
|
2125
|
+
}
|
|
2126
|
+
return succeeded
|
|
2127
|
+
}), schedules)
|
|
1704
2128
|
} finally {
|
|
1705
2129
|
ticking = false
|
|
1706
2130
|
}
|
|
1707
2131
|
}
|
|
1708
2132
|
|
|
2133
|
+
/** Runs one tick's schedules, telling `observeHost` listeners when it starts and how it ends. */
|
|
2134
|
+
const observedTick = (
|
|
2135
|
+
schedules: ReadonlyArray<ExecutableScheduleDefinition<S, any, any, any, any, AnyScheduleOrder>>,
|
|
2136
|
+
run: () => Result.Result<void, SystemFailure>
|
|
2137
|
+
): Result.Result<void, SystemFailure> => {
|
|
2138
|
+
if (hostListeners.size === 0) return run()
|
|
2139
|
+
emitHost({ type: "tick.start", schedules, frame: frameCount })
|
|
2140
|
+
let result: Result.Result<void, SystemFailure>
|
|
2141
|
+
try {
|
|
2142
|
+
result = run()
|
|
2143
|
+
} catch (defect) {
|
|
2144
|
+
emitHost({ type: "tick.end", schedules, frame: frameCount, outcome: "defect", error: defect })
|
|
2145
|
+
throw defect
|
|
2146
|
+
}
|
|
2147
|
+
emitHost(result.ok
|
|
2148
|
+
? { type: "tick.end", schedules, frame: frameCount, outcome: "ok" }
|
|
2149
|
+
: { type: "tick.end", schedules, frame: frameCount, outcome: "failed", error: result.error })
|
|
2150
|
+
return result
|
|
2151
|
+
}
|
|
2152
|
+
|
|
1709
2153
|
const hasRequirement = (requirement: Requirement.RequirementValue): boolean => {
|
|
1710
2154
|
switch (requirement.kind) {
|
|
1711
2155
|
case "service":
|
|
@@ -1713,26 +2157,33 @@ const makeValidatedRuntime = <
|
|
|
1713
2157
|
case "resource":
|
|
1714
2158
|
return resources.has(requirement.key)
|
|
1715
2159
|
case "stateMachine":
|
|
1716
|
-
|
|
2160
|
+
// An inactive sub-state has no value but is provided.
|
|
2161
|
+
return providedMachineKeys.has(requirement.key) || currentMachines.has(requirement.key)
|
|
1717
2162
|
}
|
|
1718
2163
|
}
|
|
1719
2164
|
|
|
1720
|
-
const tryRunSchedule = <const Selected extends ExecutableScheduleDefinition<S, any, any, any, any>>(
|
|
2165
|
+
const tryRunSchedule = <const Selected extends ExecutableScheduleDefinition<S, any, any, any, any, AnyScheduleOrder>>(
|
|
1721
2166
|
schedule: Selected
|
|
1722
|
-
): Result.Result<void, MissingRuntimeRequirements | Schedule.FailureOf<Selected>> => {
|
|
2167
|
+
): Result.Result<void, MissingRuntimeRequirements | TickOrderViolation | Schedule.FailureOf<Selected>> => {
|
|
2168
|
+
if (debugEnabled) reportUnread("next-tick")
|
|
1723
2169
|
const missing = schedule.requirements
|
|
1724
2170
|
.filter((requirement) => !hasRequirement(requirement))
|
|
1725
2171
|
.map(({ kind, name }) => ({ kind, name }))
|
|
1726
2172
|
|
|
1727
2173
|
if (missing.length > 0) {
|
|
1728
|
-
return Result.failure({
|
|
1729
|
-
kind: "MissingRuntimeRequirements",
|
|
2174
|
+
return watchResult(Result.failure({
|
|
2175
|
+
kind: "MissingRuntimeRequirements" as const,
|
|
1730
2176
|
requirements: missing
|
|
1731
|
-
})
|
|
2177
|
+
}), [schedule])
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
const issues = OrderInternal.tickOrderIssues(schedule.steps)
|
|
2181
|
+
if (issues.length > 0) {
|
|
2182
|
+
return watchResult(Result.failure({ kind: "TickOrderViolation" as const, issues }), [schedule])
|
|
1732
2183
|
}
|
|
1733
2184
|
|
|
1734
2185
|
advanceFrame()
|
|
1735
|
-
return runScheduleUnsafe(schedule) as Result.Result<void, Schedule.FailureOf<Selected>>
|
|
2186
|
+
return watchResult(observedTick([schedule], () => runScheduleUnsafe(schedule)), [schedule]) as Result.Result<void, Schedule.FailureOf<Selected>>
|
|
1736
2187
|
}
|
|
1737
2188
|
|
|
1738
2189
|
/**
|
|
@@ -1823,6 +2274,11 @@ const makeValidatedRuntime = <
|
|
|
1823
2274
|
if (!value.ok) return Result.failure({ _tag: "InvalidComponent", entityId: entity.id, name, error: value.error })
|
|
1824
2275
|
components.push([descriptor, value.value])
|
|
1825
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
|
+
}
|
|
1826
2282
|
entities.push({ id: entity.id, components })
|
|
1827
2283
|
}
|
|
1828
2284
|
const validatedResources: Array<readonly [symbol, unknown]> = []
|
|
@@ -1859,12 +2315,29 @@ const makeValidatedRuntime = <
|
|
|
1859
2315
|
}
|
|
1860
2316
|
}
|
|
1861
2317
|
}
|
|
1862
|
-
const
|
|
2318
|
+
const restoredMachines = new Map(currentMachines)
|
|
1863
2319
|
for (const [name, value] of Object.entries(saved.machines)) {
|
|
1864
2320
|
const machine = machinesByName.get(name)
|
|
1865
2321
|
if (!machine) return Result.failure({ _tag: "UnknownMachine", name })
|
|
1866
2322
|
if (!machine.values.includes(value)) return Result.failure({ _tag: "InvalidMachineState", name, value })
|
|
1867
|
-
|
|
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
|
+
}
|
|
1868
2341
|
}
|
|
1869
2342
|
|
|
1870
2343
|
const scoped = new Set<number>()
|
|
@@ -1897,8 +2370,12 @@ const makeValidatedRuntime = <
|
|
|
1897
2370
|
world.tryRelate(source, relation, target)
|
|
1898
2371
|
}
|
|
1899
2372
|
for (const [id, key] of memberships) world.assignEntityScope(id, key)
|
|
1900
|
-
for (const [key, value] of validatedResources)
|
|
1901
|
-
|
|
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)
|
|
1902
2379
|
// Locals are not saved: each system starts over from its factory.
|
|
1903
2380
|
localSlots.forEach(({ slot }, index) => { (slot.context as { local: unknown }).local = rebuiltLocals[index] })
|
|
1904
2381
|
return succeeded
|
|
@@ -2041,12 +2518,21 @@ const makeValidatedRuntime = <
|
|
|
2041
2518
|
scheduleNames.clear()
|
|
2042
2519
|
for (const [name, schedule] of Object.entries(named)) scheduleNames.set(schedule, name)
|
|
2043
2520
|
},
|
|
2521
|
+
declareHostEvents(declared) {
|
|
2522
|
+
hostEvents = new Set(declared.map((descriptor) => descriptor.name))
|
|
2523
|
+
},
|
|
2044
2524
|
describe: () => DebugInternal.describe({
|
|
2045
2525
|
schema: options.schema,
|
|
2046
2526
|
machines: [...new Set(machinesByName.values())],
|
|
2047
2527
|
currentMachine: (machine) => currentMachines.get(machine.key) as Machine.StateValue | undefined,
|
|
2528
|
+
providesMachine: (machine) => providedMachineKeys.has(machine.key),
|
|
2529
|
+
scopes: options.scopeDefinitions,
|
|
2048
2530
|
providedServices: Object.keys(providedServices),
|
|
2049
|
-
externallyWrittenEvents,
|
|
2531
|
+
externallyWrittenEvents: new Set([...externallyWrittenEvents, ...hostEvents]),
|
|
2532
|
+
relationMutations,
|
|
2533
|
+
commandUses,
|
|
2534
|
+
ranSystems,
|
|
2535
|
+
requirementViolations: [...requirementViolations.values()],
|
|
2050
2536
|
hasResource: (descriptor) => resources.has(descriptor.key),
|
|
2051
2537
|
schedules: [...scheduleNames].map(([schedule, name]) => [name, schedule as ExecutableScheduleDefinition<S>] as const)
|
|
2052
2538
|
}),
|
|
@@ -2063,6 +2549,13 @@ const makeValidatedRuntime = <
|
|
|
2063
2549
|
}
|
|
2064
2550
|
}
|
|
2065
2551
|
},
|
|
2552
|
+
observeHost(listener) {
|
|
2553
|
+
hostListeners.add(listener)
|
|
2554
|
+
return () => {
|
|
2555
|
+
hostListeners.delete(listener)
|
|
2556
|
+
}
|
|
2557
|
+
},
|
|
2558
|
+
checkIgnoredFailures: () => reportUnread("session-end"),
|
|
2066
2559
|
streams,
|
|
2067
2560
|
population: () => {
|
|
2068
2561
|
const components: Record<string, number> = {}
|
|
@@ -2110,7 +2603,9 @@ export const make = <
|
|
|
2110
2603
|
readonly resources?: Resources
|
|
2111
2604
|
readonly machines?: ProvidedMachines
|
|
2112
2605
|
readonly machineDefinitions?: ReadonlyArray<Machine.StateMachine.Any>
|
|
2113
|
-
/**
|
|
2606
|
+
/** Scopes whose members a committed state change despawns; `Game.Runtime.make` passes every `Game.EntityScope`. */
|
|
2607
|
+
readonly scopeDefinitions?: ReadonlyArray<EntityScopeModule.EntityScope.Any>
|
|
2608
|
+
/** Attaches a `debug` handle to the runtime, timed by `hostClock`; see the `Debug` module. */
|
|
2114
2609
|
readonly debug?: Enabled
|
|
2115
2610
|
}): MakeRuntimeResult<S, Simplify<RuntimeServicesOf<ProvidedServices>>, Resources, Root, RuntimeMachinesOf<ProvidedMachines>, Enabled> => {
|
|
2116
2611
|
const fallible = Object.entries(options.schema.resources).some(([key, descriptor]) =>
|
|
@@ -2128,7 +2623,8 @@ export const make = <
|
|
|
2128
2623
|
resources: validated.value,
|
|
2129
2624
|
machines: options.machines,
|
|
2130
2625
|
machineDefinitions: options.machineDefinitions ?? [],
|
|
2131
|
-
|
|
2626
|
+
scopeDefinitions: options.scopeDefinitions ?? [],
|
|
2627
|
+
debug: options.debug
|
|
2132
2628
|
})
|
|
2133
2629
|
return (fallible ? Result.success(runtime) : runtime) as never
|
|
2134
2630
|
}
|