@typeonce/ritmo 0.7.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 +181 -40
- package/dist/Command.d.ts.map +1 -1
- package/dist/Command.js +31 -11
- 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 +127 -13
- package/dist/Debug.d.ts.map +1 -1
- package/dist/Descriptor.d.ts +61 -10
- package/dist/Descriptor.d.ts.map +1 -1
- package/dist/Descriptor.js +22 -10
- package/dist/Descriptor.js.map +1 -1
- package/dist/Entity.d.ts +22 -7
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js.map +1 -1
- package/dist/EntityScope.d.ts +20 -1
- package/dist/EntityScope.d.ts.map +1 -1
- package/dist/EntityScope.js +8 -2
- package/dist/EntityScope.js.map +1 -1
- package/dist/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 +59 -34
- 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 +100 -14
- package/dist/Query.d.ts.map +1 -1
- package/dist/Query.js +34 -5
- 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 +61 -15
- package/dist/Runtime.d.ts.map +1 -1
- package/dist/Runtime.js +594 -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 +233 -48
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +33 -7
- package/dist/Schema.js.map +1 -1
- package/dist/Snapshot.d.ts +90 -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 +132 -31
- package/dist/System.d.ts.map +1 -1
- package/dist/System.js +32 -0
- 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 +10 -0
- package/dist/internal/debug.d.ts.map +1 -1
- package/dist/internal/debug.js +171 -12
- 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 +2 -6
- package/dist/internal/fragments.d.ts.map +1 -1
- package/dist/internal/fragments.js +36 -3
- 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 +48 -7
- package/dist/internal/game.js.map +1 -1
- package/dist/internal/order.d.ts.map +1 -1
- package/dist/internal/order.js +4 -2
- package/dist/internal/order.js.map +1 -1
- package/dist/internal/queries.d.ts.map +1 -1
- package/dist/internal/queries.js +57 -26
- 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/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/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 +281 -67
- package/src/Condition.ts +17 -1
- package/src/Debug.ts +124 -15
- package/src/Descriptor.ts +92 -21
- package/src/Entity.ts +23 -7
- package/src/EntityScope.ts +21 -2
- package/src/FixedLoop.ts +218 -0
- package/src/HostClock.ts +2 -1
- package/src/Machine.ts +72 -35
- package/src/Query.ts +118 -17
- package/src/Relation.ts +5 -67
- package/src/Runtime.ts +659 -85
- package/src/Schedule.ts +31 -19
- package/src/Schema.ts +297 -57
- package/src/Snapshot.ts +94 -2
- package/src/System.ts +151 -29
- package/src/index.ts +4 -0
- package/src/internal/cells.ts +42 -10
- package/src/internal/debug.ts +179 -14
- package/src/internal/equal.ts +31 -0
- package/src/internal/fragments.ts +35 -5
- package/src/internal/game.ts +69 -7
- package/src/internal/order.ts +8 -4
- package/src/internal/queries.ts +62 -12
- package/src/internal/relations.ts +52 -0
- package/src/internal/requirements.ts +71 -0
- package/src/internal/streams.ts +11 -0
- package/src/internal/world.ts +60 -4
package/src/internal/debug.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type * as Debug from "../Debug.ts"
|
|
7
7
|
import * as DescriptorModule from "../Descriptor.ts"
|
|
8
8
|
import type { Descriptor } from "../Descriptor.ts"
|
|
9
|
+
import type * as EntityScope from "../EntityScope.ts"
|
|
9
10
|
import type * as Inspector from "../Inspector.ts"
|
|
10
11
|
import type * as Machine from "../Machine.ts"
|
|
11
12
|
import type * as Query from "../Query.ts"
|
|
@@ -72,6 +73,9 @@ export const transitionScheduleName = (schedule: TransitionSchedule): string =>
|
|
|
72
73
|
const names = <A>(record: Record<string, A>, name: (value: A) => string): Array<string> =>
|
|
73
74
|
Object.values(record).map(name)
|
|
74
75
|
|
|
76
|
+
const lifecycleNames = (filters: ReadonlyArray<Query.Filter<any>>, kind: "added" | "changed"): Array<string> =>
|
|
77
|
+
filters.flatMap((filter) => filter.kind === kind ? [filter.descriptor.name] : [])
|
|
78
|
+
|
|
75
79
|
const describeQuery = (slot: string, query: Query.Query.Any<any>): Debug.QueryDescription => {
|
|
76
80
|
const reads: Array<string> = []
|
|
77
81
|
const writes: Array<string> = []
|
|
@@ -102,8 +106,11 @@ const describeQuery = (slot: string, query: Query.Query.Any<any>): Debug.QueryDe
|
|
|
102
106
|
optional,
|
|
103
107
|
with: query.with.map((descriptor) => descriptor.name),
|
|
104
108
|
without: query.without.map((descriptor) => descriptor.name),
|
|
105
|
-
added: query.filters
|
|
106
|
-
changed: query.filters
|
|
109
|
+
added: lifecycleNames(query.filters, "added"),
|
|
110
|
+
changed: lifecycleNames(query.filters, "changed"),
|
|
111
|
+
anyOf: query.filters.flatMap((filter) => filter.kind === "anyOf"
|
|
112
|
+
? [{ added: lifecycleNames(filter.filters, "added"), changed: lifecycleNames(filter.filters, "changed") }]
|
|
113
|
+
: []),
|
|
107
114
|
relations: [
|
|
108
115
|
...relations,
|
|
109
116
|
...relationNames(query.withRelations, "with"),
|
|
@@ -114,7 +121,7 @@ const describeQuery = (slot: string, query: Query.Query.Any<any>): Debug.QueryDe
|
|
|
114
121
|
}
|
|
115
122
|
}
|
|
116
123
|
|
|
117
|
-
type Mode = { readonly mode: "read" | "write"; readonly descriptor: Descriptor.Any }
|
|
124
|
+
type Mode = { readonly mode: "read" | "changed" | "write"; readonly descriptor: Descriptor.Any }
|
|
118
125
|
type CommandAccess = { readonly permissions: ReadonlyArray<{ readonly permission: keyof Debug.CommandPermissions; readonly values: ReadonlyArray<{ readonly name: string }> }> }
|
|
119
126
|
|
|
120
127
|
const permissionsOf = (access: CommandAccess | undefined): Debug.CommandPermissions => {
|
|
@@ -142,7 +149,7 @@ type ThisTickAccess = Mode & { readonly thisTick?: true }
|
|
|
142
149
|
|
|
143
150
|
export const describeSystem = (system: AnySystem, placements: ReadonlyArray<string>): Debug.SystemDescription => {
|
|
144
151
|
const spec = system.spec
|
|
145
|
-
const byMode = (record: Record<string, Mode>, mode: "
|
|
152
|
+
const byMode = (record: Record<string, Mode>, mode: Mode["mode"]) =>
|
|
146
153
|
Object.values(record).filter((access) => access.mode === mode).map((access) => access.descriptor.name)
|
|
147
154
|
const machineName = (access: { readonly machine: Machine.StateMachine.Any }) => access.machine.name
|
|
148
155
|
// A system also reads whatever its `check` conditions read.
|
|
@@ -160,7 +167,8 @@ export const describeSystem = (system: AnySystem, placements: ReadonlyArray<stri
|
|
|
160
167
|
Object.entries(check.spec.queries as Record<string, Query.Query.Any<any>>).map(([slot, query]) => describeQuery(`check(${check.name}).${slot}`, query)))
|
|
161
168
|
],
|
|
162
169
|
resources: {
|
|
163
|
-
reads: unique([...byMode(spec.resources, "read"), ...checks.flatMap((check) => byMode(check.spec.resources, "read"))]),
|
|
170
|
+
reads: unique([...byMode(spec.resources, "read"), ...byMode(spec.resources, "changed"), ...checks.flatMap((check) => byMode(check.spec.resources, "read"))]),
|
|
171
|
+
changed: byMode(spec.resources, "changed"),
|
|
164
172
|
writes: byMode(spec.resources, "write")
|
|
165
173
|
},
|
|
166
174
|
events: { reads: byMode(spec.events, "read"), writes: byMode(spec.events, "write") },
|
|
@@ -260,6 +268,10 @@ export interface DescribeInput {
|
|
|
260
268
|
readonly schema: Schema.Any
|
|
261
269
|
readonly machines: ReadonlyArray<Machine.StateMachine.Any>
|
|
262
270
|
readonly currentMachine: (machine: Machine.StateMachine.Any) => Machine.StateValue | undefined
|
|
271
|
+
/** Whether the runtime was given an initial value for the machine. */
|
|
272
|
+
readonly providesMachine: (machine: Machine.StateMachine.Any) => boolean
|
|
273
|
+
/** Every scope defined through the game, with its despawn triggers. */
|
|
274
|
+
readonly scopes: ReadonlyArray<EntityScope.EntityScope.Any>
|
|
263
275
|
readonly providedServices: ReadonlyArray<string>
|
|
264
276
|
readonly externallyWrittenEvents: ReadonlySet<string>
|
|
265
277
|
/** Relations commands have tried to relate or reorder, by the queuing systems. */
|
|
@@ -268,6 +280,11 @@ export interface DescribeInput {
|
|
|
268
280
|
readonly commandUses: ReadonlyMap<string, ReadonlySet<string>>
|
|
269
281
|
/** Systems that ran in this session. */
|
|
270
282
|
readonly ranSystems: ReadonlySet<string>
|
|
283
|
+
/** Commands skipped because they would have broken a `Schema.requires` requirement. */
|
|
284
|
+
readonly requirementViolations: ReadonlyArray<Debug.RequirementViolation>
|
|
285
|
+
readonly nextStateReplacements: ReadonlyArray<Debug.NextStateReplacement>
|
|
286
|
+
readonly staleTargets: ReadonlyArray<Debug.StaleTarget>
|
|
287
|
+
readonly streamLosses: ReadonlyArray<Debug.StreamLoss>
|
|
271
288
|
readonly hasResource: (descriptor: Descriptor.Any) => boolean
|
|
272
289
|
readonly schedules: ReadonlyArray<readonly [string, AnySchedule]>
|
|
273
290
|
}
|
|
@@ -279,19 +296,38 @@ export const describe = (input: DescribeInput): Debug.Description => {
|
|
|
279
296
|
const relations = Object.values(input.schema.relations) as ReadonlyArray<Relation.Relation.Any>
|
|
280
297
|
|
|
281
298
|
const schedules = collectSchedules(input.schedules)
|
|
299
|
+
// A system gated by Schedule.when / pauseWhen is a copy pointing at its
|
|
300
|
+
// base: both are one system, described once with every placement.
|
|
301
|
+
const baseOf = (system: AnySystem): AnySystem => (system as { readonly base?: AnySystem }).base ?? system
|
|
282
302
|
const placements = new Map<AnySystem, Array<string>>()
|
|
303
|
+
const steps = new Set<AnySystem>()
|
|
283
304
|
for (const [name, schedule] of schedules) {
|
|
284
305
|
schedule.steps.forEach((step, index) => {
|
|
285
306
|
if (!Schedule.isSystemStep(step)) return
|
|
286
|
-
const system = step as AnySystem
|
|
307
|
+
const system = baseOf(step as AnySystem)
|
|
308
|
+
steps.add(step as AnySystem)
|
|
287
309
|
const list = placements.get(system)
|
|
288
310
|
const placement = `${name}#${index}`
|
|
289
311
|
if (list) list.push(placement)
|
|
290
312
|
else placements.set(system, [placement])
|
|
291
313
|
})
|
|
292
314
|
}
|
|
293
|
-
|
|
294
|
-
const
|
|
315
|
+
// Gates differ per placement: the description lists the conditions of all of them.
|
|
316
|
+
const gatesOf = new Map<AnySystem, { when: Set<unknown>; pauseWhen: Set<unknown> }>()
|
|
317
|
+
for (const step of steps) {
|
|
318
|
+
const base = baseOf(step)
|
|
319
|
+
const gates = gatesOf.get(base) ?? { when: new Set(), pauseWhen: new Set() }
|
|
320
|
+
for (const condition of step.spec.when as ReadonlyArray<unknown>) gates.when.add(condition)
|
|
321
|
+
for (const condition of step.spec.pauseWhen as ReadonlyArray<unknown>) gates.pauseWhen.add(condition)
|
|
322
|
+
gatesOf.set(base, gates)
|
|
323
|
+
}
|
|
324
|
+
const byBase = new Map([...placements].map(([system, where]) => {
|
|
325
|
+
const gates = gatesOf.get(system)!
|
|
326
|
+
const merged = { ...system, spec: { ...system.spec, when: [...gates.when], pauseWhen: [...gates.pauseWhen] } } as AnySystem
|
|
327
|
+
return [system, describeSystem(merged, where)] as const
|
|
328
|
+
}))
|
|
329
|
+
const described = new Map<AnySystem, Debug.SystemDescription>([...steps].map((step) => [step, byBase.get(baseOf(step))!] as const))
|
|
330
|
+
const systems = [...byBase.values()]
|
|
295
331
|
|
|
296
332
|
const components = new Index(componentDescriptors.map((descriptor) => descriptor.name))
|
|
297
333
|
const resources = new Index(resourceDescriptors.map((descriptor) => descriptor.name))
|
|
@@ -300,7 +336,7 @@ export const describe = (input: DescribeInput): Debug.Description => {
|
|
|
300
336
|
for (const name of input.externallyWrittenEvents) events.write(name, "runtime.send")
|
|
301
337
|
for (const system of systems) {
|
|
302
338
|
for (const query of system.queries) {
|
|
303
|
-
for (const name of [...query.reads, ...query.optional, ...query.with, ...query.added, ...query.changed]) {
|
|
339
|
+
for (const name of [...query.reads, ...query.optional, ...query.with, ...query.added, ...query.changed, ...anyOfNames(query)]) {
|
|
304
340
|
components.read(name, system.name)
|
|
305
341
|
}
|
|
306
342
|
for (const name of query.writes) {
|
|
@@ -325,6 +361,20 @@ export const describe = (input: DescribeInput): Debug.Description => {
|
|
|
325
361
|
schedule.steps.some((step) => !Schedule.isSystemStep(step) && step.kind === "applyStateTransitions"))
|
|
326
362
|
|
|
327
363
|
const lints: Array<Omit<Debug.Lint, "fix">> = []
|
|
364
|
+
const triggerName = (trigger: EntityScope.Trigger) =>
|
|
365
|
+
`${trigger.phase === "exit" ? "onExit" : "onEnter"}(${trigger.machine.name}=${String(trigger.state)})`
|
|
366
|
+
for (const scope of input.scopes) {
|
|
367
|
+
for (const trigger of scope.despawnOn) {
|
|
368
|
+
const machine = input.machines.find((candidate) => candidate.key === trigger.machine.key)
|
|
369
|
+
if (machine !== undefined && input.providesMachine(machine)) continue
|
|
370
|
+
lints.push({
|
|
371
|
+
severity: "warning",
|
|
372
|
+
code: "scope-trigger-never-applied",
|
|
373
|
+
subject: scope.name,
|
|
374
|
+
message: `${scope.name} is despawned on ${triggerName(trigger)}, but the runtime does not provide ${trigger.machine.name}, so its state never changes and the scope is never despawned`
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
}
|
|
328
378
|
if (schedules.length > 0) {
|
|
329
379
|
for (const entry of events.list()) {
|
|
330
380
|
if (entry.writers.length > 0 && entry.readers.length === 0) {
|
|
@@ -354,8 +404,26 @@ export const describe = (input: DescribeInput): Debug.Description => {
|
|
|
354
404
|
})
|
|
355
405
|
}
|
|
356
406
|
}
|
|
407
|
+
for (const scope of input.scopes) {
|
|
408
|
+
for (const trigger of scope.despawnOn) {
|
|
409
|
+
const machine = input.machines.find((candidate) => candidate.key === trigger.machine.key)
|
|
410
|
+
if (machine === undefined || !input.providesMachine(machine)) continue
|
|
411
|
+
const reason = !appliesTransitions
|
|
412
|
+
? "no described schedule has an applyStateTransitions() marker"
|
|
413
|
+
: !queuedWithParents(machine, nextStates)
|
|
414
|
+
? `no described system queues a next state of ${machine.name}`
|
|
415
|
+
: undefined
|
|
416
|
+
if (reason === undefined) continue
|
|
417
|
+
lints.push({
|
|
418
|
+
severity: "warning",
|
|
419
|
+
code: "scope-trigger-never-applied",
|
|
420
|
+
subject: scope.name,
|
|
421
|
+
message: `${scope.name} is despawned on ${triggerName(trigger)}, but ${reason}, so the scope is never despawned`
|
|
422
|
+
})
|
|
423
|
+
}
|
|
424
|
+
}
|
|
357
425
|
const failureReaders = new Set(systems.flatMap((system) => system.relationFailures))
|
|
358
|
-
// Declared `relates` permissions, and relations that commands were seen to relate (
|
|
426
|
+
// Declared `relates` permissions, and relations that commands were seen to relate (erased types can relate without one).
|
|
359
427
|
const relaters = new Map<string, Set<string>>()
|
|
360
428
|
for (const system of systems) {
|
|
361
429
|
for (const name of system.commands.relates) relaters.set(name, new Set([...(relaters.get(name) ?? []), system.name]))
|
|
@@ -370,6 +438,20 @@ export const describe = (input: DescribeInput): Debug.Description => {
|
|
|
370
438
|
message: `${name} is related by commands from ${[...queuers].sort().join(", ")}, but no described system reads its failures (Game.System.readRelationFailures(${name})), so a failed relate or reorder is dropped silently`
|
|
371
439
|
})
|
|
372
440
|
}
|
|
441
|
+
const resourceWriters = new Set(systems.flatMap((system) => system.resources.writes))
|
|
442
|
+
const changedReaders = new Map<string, Array<string>>()
|
|
443
|
+
for (const system of systems) {
|
|
444
|
+
for (const name of system.resources.changed) changedReaders.set(name, [...(changedReaders.get(name) ?? []), system.name])
|
|
445
|
+
}
|
|
446
|
+
for (const [name, readers] of changedReaders) {
|
|
447
|
+
if (resourceWriters.has(name)) continue
|
|
448
|
+
lints.push({
|
|
449
|
+
severity: "warning",
|
|
450
|
+
code: "resource-changed-never-written",
|
|
451
|
+
subject: name,
|
|
452
|
+
message: `${name} is read with changedResource by ${readers.join(", ")}, but no described system writes it, so changed() is true only on the first run`
|
|
453
|
+
})
|
|
454
|
+
}
|
|
373
455
|
lints.push(...readBeforeWriteLints(schedules, described, input.externallyWrittenEvents))
|
|
374
456
|
// Each named schedule is checked as one tick; `tick` checks this at compile time.
|
|
375
457
|
for (const [name, schedule] of input.schedules) {
|
|
@@ -403,10 +485,63 @@ export const describe = (input: DescribeInput): Debug.Description => {
|
|
|
403
485
|
}
|
|
404
486
|
}
|
|
405
487
|
}
|
|
488
|
+
// Observed with debug, whether or not schedules are named.
|
|
489
|
+
for (const violation of input.requirementViolations) {
|
|
490
|
+
const times = violation.count === 1 ? "once" : `${violation.count} times`
|
|
491
|
+
lints.push({
|
|
492
|
+
severity: "warning",
|
|
493
|
+
code: "requirement-violated",
|
|
494
|
+
subject: violation.system,
|
|
495
|
+
message: `${violation.system} queued ${violation.command === "insert" ? "an" : "a"} ${violation.command} that would have left ${violation.component} without ${violation.missing} (last on entity ${violation.entity}), so it was skipped ${times}`
|
|
496
|
+
})
|
|
497
|
+
}
|
|
498
|
+
for (const replacement of input.nextStateReplacements) {
|
|
499
|
+
const times = replacement.count === 1 ? "" : `, ${replacement.count} times`
|
|
500
|
+
lints.push({
|
|
501
|
+
severity: "warning",
|
|
502
|
+
code: "next-state-replaced",
|
|
503
|
+
subject: replacement.machine,
|
|
504
|
+
message: `${replacement.by.system} queued ${replacement.machine}=${String(replacement.by.value)}, replacing ${String(replacement.replaced.value)} queued by ${replacement.replaced.system} before the same applyStateTransitions()${times}: the last value wins`
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
for (const stale of input.staleTargets) {
|
|
508
|
+
const times = stale.count === 1 ? "once" : `${stale.count} times`
|
|
509
|
+
lints.push({
|
|
510
|
+
severity: "warning",
|
|
511
|
+
code: "stale-target",
|
|
512
|
+
subject: stale.system,
|
|
513
|
+
message: `${stale.system} passed ${stale.command} an id spawned in an earlier run (last entity ${stale.entity}), so it was skipped ${times}`
|
|
514
|
+
})
|
|
515
|
+
}
|
|
516
|
+
for (const loss of input.streamLosses) {
|
|
517
|
+
if (loss.checksLagged) continue
|
|
518
|
+
if (loss.lostRuns > 0) {
|
|
519
|
+
lints.push({
|
|
520
|
+
severity: "warning",
|
|
521
|
+
code: "stream-loss-unread",
|
|
522
|
+
subject: loss.system,
|
|
523
|
+
message: `${loss.system} read ${loss.stream} after entries were dropped before it read them (${loss.lostRuns === 1 ? "once" : `${loss.lostRuns} runs`}), and never checks lagged()`
|
|
524
|
+
})
|
|
525
|
+
} else if (loss.paused) {
|
|
526
|
+
lints.push({
|
|
527
|
+
severity: "info",
|
|
528
|
+
code: "stream-loss-unread",
|
|
529
|
+
subject: loss.system,
|
|
530
|
+
message: `${loss.system} reads ${loss.stream} under pauseWhen and never checks lagged(): a long pause drops entries past the stream's capacity`
|
|
531
|
+
})
|
|
532
|
+
}
|
|
533
|
+
}
|
|
406
534
|
|
|
407
535
|
return {
|
|
408
536
|
version: 1,
|
|
409
|
-
components: componentDescriptors.map((descriptor) => ({
|
|
537
|
+
components: componentDescriptors.map((descriptor) => ({
|
|
538
|
+
name: descriptor.name,
|
|
539
|
+
storage: storageOf(descriptor),
|
|
540
|
+
immutable: DescriptorModule.isImmutable(descriptor),
|
|
541
|
+
requires: [...new Set(input.schema.requires
|
|
542
|
+
.filter((requirement) => requirement.component.name === descriptor.name)
|
|
543
|
+
.flatMap((requirement) => requirement.required.map((required) => required.name)))]
|
|
544
|
+
})),
|
|
410
545
|
resources: resourceDescriptors.map((descriptor) => ({
|
|
411
546
|
name: descriptor.name,
|
|
412
547
|
storage: storageOf(descriptor),
|
|
@@ -423,8 +558,11 @@ export const describe = (input: DescribeInput): Debug.Description => {
|
|
|
423
558
|
machines: input.machines.map((machine) => ({
|
|
424
559
|
name: machine.name,
|
|
425
560
|
states: machine.values,
|
|
426
|
-
current: input.currentMachine(machine)
|
|
561
|
+
current: input.currentMachine(machine),
|
|
562
|
+
provided: input.providesMachine(machine),
|
|
563
|
+
within: machine.within === undefined ? undefined : `${machine.within.machine.name}=${String(machine.within.state)}`
|
|
427
564
|
})),
|
|
565
|
+
scopes: input.scopes.map((scope) => ({ name: scope.name, despawnOn: scope.despawnOn.map(triggerName) })),
|
|
428
566
|
services: describeServices(input.providedServices, systems),
|
|
429
567
|
schedules: schedules.map(([name, schedule]) => ({ name, steps: schedule.steps.map(describeStep) })),
|
|
430
568
|
systems,
|
|
@@ -437,6 +575,17 @@ export const describe = (input: DescribeInput): Debug.Description => {
|
|
|
437
575
|
}
|
|
438
576
|
}
|
|
439
577
|
|
|
578
|
+
/**
|
|
579
|
+
* Whether a described system queues a next state of `machine`, or of a
|
|
580
|
+
* machine it exists within: a parent leaving its state changes a sub-state too.
|
|
581
|
+
*/
|
|
582
|
+
const queuedWithParents = (machine: Machine.StateMachine.Any, nextStates: ReadonlyMap<string, unknown>): boolean => {
|
|
583
|
+
for (let current: Machine.StateMachine.Any | undefined = machine; current !== undefined; current = current.within?.machine) {
|
|
584
|
+
if (nextStates.has(current.name)) return true
|
|
585
|
+
}
|
|
586
|
+
return false
|
|
587
|
+
}
|
|
588
|
+
|
|
440
589
|
/** The fix each lint names, by code. */
|
|
441
590
|
const fixFor = (code: Debug.Lint["code"], subject: string): string => {
|
|
442
591
|
switch (code) {
|
|
@@ -446,12 +595,24 @@ const fixFor = (code: Debug.Lint["code"], subject: string): string => {
|
|
|
446
595
|
return `emit ${subject} from a system with Game.System.writeEvent, or stop reading it; if the host sends it with runtime.send, declare it (debug.declareHostEvents, or hostEvents in a Session or lint target)`
|
|
447
596
|
case "next-state-never-applied":
|
|
448
597
|
return `add Game.Schedule.applyStateTransitions(...) to a schedule that runs after the system that queues ${subject}`
|
|
598
|
+
case "resource-changed-never-written":
|
|
599
|
+
return `write ${subject} from a system with Game.System.writeResource, or read it with Game.System.readResource; if only a render schedule writes it, pass that schedule as describe-only`
|
|
600
|
+
case "scope-trigger-never-applied":
|
|
601
|
+
return `provide the machine of ${subject}'s trigger in Game.Runtime.make({ machines }), queue its next state from a system, and apply it with Game.Schedule.applyStateTransitions(...); or despawn ${subject} with commands.despawnScope(...) instead`
|
|
449
602
|
case "relation-failure-never-read":
|
|
450
603
|
return `read the failures with Game.System.readRelationFailures(${subject}) in a system and handle them`
|
|
451
604
|
case "this-tick-order":
|
|
452
605
|
return "move the writer before the reader in the schedule (the message names both), or read without Game.System.thisTick"
|
|
453
606
|
case "component-never-read":
|
|
454
607
|
return `fine if ${subject} is read outside the described schedules (rendering, inspectors); otherwise stop writing it`
|
|
608
|
+
case "requirement-violated":
|
|
609
|
+
return `keep what ${subject} spawns or inserts complete (every component its components require), and remove a required component only together with its requirers; a query proof can go stale when another system's command is applied first in the same applyDeferred()`
|
|
610
|
+
case "stream-loss-unread":
|
|
611
|
+
return `check lagged() in ${subject} where a missed entry matters (a node never destroyed, a count off), and rebuild from queries when it is true; fine if a missed entry is harmless`
|
|
612
|
+
case "stale-target":
|
|
613
|
+
return `an id spawn returns is valid only in the run that spawned it; in ${subject}, keep the entity id or a handle and resolve it again with a query (queries.x.get(id) / lookup.getHandle(handle, query)) and pass match.entity`
|
|
614
|
+
case "next-state-replaced":
|
|
615
|
+
return "decide between the values in one system, or check whether a next state is already queued (nextMachines.x.getPending()) before setting another"
|
|
455
616
|
case "command-permission-unused":
|
|
456
617
|
return `fine if the session did not reach the case where ${subject} uses it; otherwise drop the permission from its Game.System.commands(...)`
|
|
457
618
|
case "read-before-write":
|
|
@@ -462,12 +623,16 @@ const fixFor = (code: Debug.Lint["code"], subject: string): string => {
|
|
|
462
623
|
type Accesses = { readonly reads: ReadonlyArray<string>; readonly writes: ReadonlyArray<string> }
|
|
463
624
|
|
|
464
625
|
/** What a system reads and writes, per kind. `with`/`without` filters test presence and are not reads. */
|
|
626
|
+
/** The components named by a query's `anyOf` filters. */
|
|
627
|
+
const anyOfNames = (query: Debug.QueryDescription): ReadonlyArray<string> =>
|
|
628
|
+
query.anyOf.flatMap((filter) => [...filter.added, ...filter.changed])
|
|
629
|
+
|
|
465
630
|
const accessesOf = (system: Debug.SystemDescription): Record<"component" | "resource" | "event", Accesses> => ({
|
|
466
631
|
component: {
|
|
467
|
-
reads: system.queries.flatMap((query) => [...query.reads, ...query.optional, ...query.added, ...query.changed]),
|
|
632
|
+
reads: system.queries.flatMap((query) => [...query.reads, ...query.optional, ...query.added, ...query.changed, ...anyOfNames(query)]),
|
|
468
633
|
writes: system.queries.flatMap((query) => query.writes)
|
|
469
634
|
},
|
|
470
|
-
resource: system.resources,
|
|
635
|
+
resource: { reads: system.resources.reads, writes: system.resources.writes },
|
|
471
636
|
event: system.events
|
|
472
637
|
})
|
|
473
638
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural equality for plain data: primitives by `Object.is`, arrays and
|
|
3
|
+
* plain objects by their entries. Other objects (class instances, maps,
|
|
4
|
+
* sets) compare by reference. Used to decide whether a resource write
|
|
5
|
+
* changed the value (`Game.System.changedResource`).
|
|
6
|
+
*/
|
|
7
|
+
export const sameValue = (left: unknown, right: unknown): boolean => {
|
|
8
|
+
if (Object.is(left, right)) return true
|
|
9
|
+
if (typeof left !== "object" || typeof right !== "object" || left === null || right === null) return false
|
|
10
|
+
if (Array.isArray(left)) {
|
|
11
|
+
if (!Array.isArray(right) || left.length !== right.length) return false
|
|
12
|
+
for (let index = 0; index < left.length; index++) {
|
|
13
|
+
if (!sameValue(left[index], right[index])) return false
|
|
14
|
+
}
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(right) || !isPlain(left) || !isPlain(right)) return false
|
|
18
|
+
const leftKeys = Object.keys(left)
|
|
19
|
+
if (leftKeys.length !== Object.keys(right).length) return false
|
|
20
|
+
const leftRecord = left as Record<string, unknown>
|
|
21
|
+
const rightRecord = right as Record<string, unknown>
|
|
22
|
+
for (const key of leftKeys) {
|
|
23
|
+
if (!(key in rightRecord) || !sameValue(leftRecord[key], rightRecord[key])) return false
|
|
24
|
+
}
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const isPlain = (value: object): boolean => {
|
|
29
|
+
const prototype = Object.getPrototypeOf(value) as object | null
|
|
30
|
+
return prototype === null || prototype === Object.prototype
|
|
31
|
+
}
|
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
* The compile-time counterpart is `ValidateFragments` in `Schema.ts`; these
|
|
5
5
|
* guards cover schemas assembled from erased types.
|
|
6
6
|
*/
|
|
7
|
+
import { isTransient } from "../Descriptor.ts"
|
|
7
8
|
import type { Schema, SchemaDefinition } from "../Schema.ts"
|
|
8
9
|
|
|
9
|
-
type NamedRegistry = Record<string, { readonly name: string }>
|
|
10
|
+
type NamedRegistry = Record<string, { readonly name: string; readonly kind?: string }>
|
|
11
|
+
|
|
12
|
+
/** The descriptor kind each registry holds (relations carry their own shape). */
|
|
13
|
+
const expectedKind: Partial<Record<(typeof kinds)[number], string>> = { components: "component", resources: "resource", events: "event" }
|
|
14
|
+
const article = (kind: string) => kind === "event" ? "an" : "a"
|
|
10
15
|
|
|
11
16
|
const kinds = ["components", "resources", "events", "relations"] as const
|
|
12
17
|
|
|
@@ -14,15 +19,22 @@ export const emptySchema = (): SchemaDefinition => ({
|
|
|
14
19
|
components: {},
|
|
15
20
|
resources: {},
|
|
16
21
|
events: {},
|
|
17
|
-
relations: {}
|
|
22
|
+
relations: {},
|
|
23
|
+
requires: []
|
|
18
24
|
})
|
|
19
25
|
|
|
20
26
|
/**
|
|
21
27
|
* Merges two registries after checking for duplicate keys and descriptor names.
|
|
22
28
|
*/
|
|
23
|
-
const mergeRegistry = (left: NamedRegistry, right: NamedRegistry): NamedRegistry => {
|
|
29
|
+
const mergeRegistry = (left: NamedRegistry, right: NamedRegistry, expected: string | undefined): NamedRegistry => {
|
|
24
30
|
const names = new Set(Object.values(left).map((entry) => entry.name))
|
|
25
31
|
for (const [key, entry] of Object.entries(right)) {
|
|
32
|
+
if (expected !== undefined && entry.kind !== expected) {
|
|
33
|
+
const actual = String(entry.kind)
|
|
34
|
+
throw new Error(actual === "service"
|
|
35
|
+
? `${key} is a service, not ${article(expected)} ${expected}: provide services to Game.Runtime.make({ services }) instead of registering them`
|
|
36
|
+
: `${key} is ${article(actual)} ${actual}, not ${article(expected)} ${expected}: move it to ${actual}s: { ... }`)
|
|
37
|
+
}
|
|
26
38
|
if (key in left) {
|
|
27
39
|
throw new Error(`Duplicate schema key: ${key}`)
|
|
28
40
|
}
|
|
@@ -35,10 +47,11 @@ const mergeRegistry = (left: NamedRegistry, right: NamedRegistry): NamedRegistry
|
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
export const mergeSchemas = (left: Schema.Any, right: Schema.Any): Schema.Any => {
|
|
38
|
-
const merged: Record<string,
|
|
50
|
+
const merged: Record<string, unknown> = {}
|
|
39
51
|
for (const kind of kinds) {
|
|
40
|
-
merged[kind] = mergeRegistry(left[kind], right[kind])
|
|
52
|
+
merged[kind] = mergeRegistry(left[kind], right[kind], expectedKind[kind])
|
|
41
53
|
}
|
|
54
|
+
merged["requires"] = [...left.requires, ...right.requires]
|
|
42
55
|
return merged as unknown as Schema.Any
|
|
43
56
|
}
|
|
44
57
|
|
|
@@ -47,5 +60,22 @@ export const buildSchema = (fragments: ReadonlyArray<Schema.Any>): Schema.Any =>
|
|
|
47
60
|
for (const fragment of fragments) {
|
|
48
61
|
current = mergeSchemas(current, fragment)
|
|
49
62
|
}
|
|
63
|
+
const registered = new Set(Object.values(current.components).map((descriptor) => descriptor.name))
|
|
64
|
+
for (const requirement of current.requires) {
|
|
65
|
+
for (const descriptor of [requirement.component, ...requirement.required]) {
|
|
66
|
+
if (!registered.has(descriptor.name)) {
|
|
67
|
+
throw new Error(`${descriptor.name} is named in Schema.requires(...) but no fragment registers it: add it to a fragment's components`)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const required of requirement.required) {
|
|
71
|
+
if (isTransient(required) && !isTransient(requirement.component)) {
|
|
72
|
+
throw new Error(transientRequirementMessage(requirement.component.name, required.name))
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
50
76
|
return current
|
|
51
77
|
}
|
|
78
|
+
|
|
79
|
+
/** Why a saved component cannot require a transient one: a restore would bring it back without it. */
|
|
80
|
+
export const transientRequirementMessage = (requirer: string, required: string): string =>
|
|
81
|
+
`${requirer} requires ${required}, which is transient and not saved: make ${requirer} transient too, or give ${required} a constructor`
|
package/src/internal/game.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as Command from "../Command.ts"
|
|
8
8
|
import type * as Debug from "../Debug.ts"
|
|
9
|
+
import type * as Snapshot from "../Snapshot.ts"
|
|
10
|
+
import type { Descriptor } from "../Descriptor.ts"
|
|
9
11
|
import * as Condition from "../Condition.ts"
|
|
10
12
|
import * as Entity from "../Entity.ts"
|
|
11
13
|
import * as EntityScope from "../EntityScope.ts"
|
|
@@ -14,36 +16,90 @@ import * as Machine from "../Machine.ts"
|
|
|
14
16
|
import * as QueryModule from "../Query.ts"
|
|
15
17
|
import * as Relation from "../Relation.ts"
|
|
16
18
|
import * as Runtime from "../Runtime.ts"
|
|
19
|
+
import * as Requirement from "../Requirement.ts"
|
|
17
20
|
import * as Schedule from "../Schedule.ts"
|
|
18
21
|
import type { Schema } from "../Schema.ts"
|
|
19
22
|
import * as System from "../System.ts"
|
|
23
|
+
import { requirementIndex, withRequired } from "./requirements.ts"
|
|
24
|
+
|
|
25
|
+
/** Why an exit phase (`onExit`, `onTransition`) cannot contain `applyDeferred()`. */
|
|
26
|
+
export const exitApplyDeferredMessage =
|
|
27
|
+
"onExit and onTransition schedules cannot contain applyDeferred(): the exit phase rolls back as one when a system in it fails, so its commands are applied at the next applyDeferred() after the marker" as const
|
|
20
28
|
|
|
21
29
|
export const makeGame = <S extends Schema.Any, Root>(schema: S, _root: Root): Schema.Game<S, Root> => {
|
|
30
|
+
const requirements = requirementIndex(schema)
|
|
22
31
|
const definedMachines: Array<Machine.StateMachine.Any> = []
|
|
23
32
|
const definedMachineNames = new Set<string>()
|
|
24
33
|
|
|
25
|
-
const defineMachine = (
|
|
34
|
+
const defineMachine = (
|
|
35
|
+
name: string,
|
|
36
|
+
values: readonly [Machine.StateValue, ...Machine.StateValue[]],
|
|
37
|
+
options?: { readonly within: readonly [Machine.StateMachine.Any, Machine.StateValue] }
|
|
38
|
+
) => {
|
|
26
39
|
if (definedMachineNames.has(name)) {
|
|
27
40
|
throw new Error(`Duplicate state machine name: ${name}`)
|
|
28
41
|
}
|
|
29
|
-
const machine = Machine.StateMachine(
|
|
42
|
+
const machine = Machine.StateMachine(
|
|
43
|
+
name,
|
|
44
|
+
values,
|
|
45
|
+
options === undefined ? undefined : { machine: options.within[0], state: options.within[1] }
|
|
46
|
+
)
|
|
30
47
|
definedMachines.push(machine)
|
|
31
48
|
definedMachineNames.add(name)
|
|
32
49
|
return machine
|
|
33
50
|
}
|
|
34
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Every scope defined through this game, in definition order. The runtime
|
|
54
|
+
* reads their triggers at `Runtime.make`, so a scope only ever used with
|
|
55
|
+
* `spawnIn` is still despawned by its state change.
|
|
56
|
+
*/
|
|
57
|
+
const definedScopes: Array<EntityScope.EntityScope.Any> = []
|
|
58
|
+
const definedScopeNames = new Set<string>()
|
|
59
|
+
|
|
60
|
+
const defineScope = (
|
|
61
|
+
name: string,
|
|
62
|
+
options?: {
|
|
63
|
+
readonly despawnOnEnter?: readonly [Machine.StateMachine.Any, Machine.StateValue]
|
|
64
|
+
readonly despawnOnExit?: readonly [Machine.StateMachine.Any, Machine.StateValue]
|
|
65
|
+
}
|
|
66
|
+
) => {
|
|
67
|
+
if (definedScopeNames.has(name)) {
|
|
68
|
+
throw new Error(`Duplicate entity scope name: ${name}`)
|
|
69
|
+
}
|
|
70
|
+
const triggers: Array<EntityScope.Trigger> = []
|
|
71
|
+
if (options?.despawnOnExit !== undefined) {
|
|
72
|
+
triggers.push({ phase: "exit", machine: options.despawnOnExit[0], state: options.despawnOnExit[1] })
|
|
73
|
+
}
|
|
74
|
+
if (options?.despawnOnEnter !== undefined) {
|
|
75
|
+
triggers.push({ phase: "enter", machine: options.despawnOnEnter[0], state: options.despawnOnEnter[1] })
|
|
76
|
+
}
|
|
77
|
+
const scope = EntityScope.make(name, triggers)
|
|
78
|
+
definedScopes.push(scope)
|
|
79
|
+
definedScopeNames.add(name)
|
|
80
|
+
return scope
|
|
81
|
+
}
|
|
82
|
+
|
|
35
83
|
const transitionSchedule = (
|
|
36
84
|
transition: Machine.TransitionScheduleDefinition["transition"],
|
|
37
85
|
plan: ReadonlyArray<Schedule.ScheduleEntry>
|
|
38
|
-
) =>
|
|
86
|
+
) => {
|
|
87
|
+
const schedule = Schedule.make(schema, plan)
|
|
88
|
+
if (transition.phase !== "enter" && schedule.steps.some((step) => !Schedule.isSystemStep(step) && step.kind === "applyDeferred")) {
|
|
89
|
+
throw new Error(exitApplyDeferredMessage)
|
|
90
|
+
}
|
|
91
|
+
// The machine is a requirement: a runtime without it could never run the schedule.
|
|
92
|
+
return { ...schedule, requirements: Requirement.collect([...schedule.requirements, transition.machine]), transition }
|
|
93
|
+
}
|
|
39
94
|
|
|
40
95
|
const game = {
|
|
41
96
|
schema,
|
|
42
|
-
EntityScope:
|
|
97
|
+
EntityScope: defineScope,
|
|
43
98
|
Inspector: (name: string, spec: object, read: (context: any) => unknown) =>
|
|
44
99
|
Inspector.make(name, { schema, ...spec } as never, read),
|
|
45
100
|
Entity: {
|
|
46
|
-
handle: Entity.handle
|
|
101
|
+
handle: Entity.handle,
|
|
102
|
+
id: (handle: number) => Entity.makeEntityId(handle)
|
|
47
103
|
},
|
|
48
104
|
Query: Object.assign((spec: never) => QueryModule.Query(spec), {
|
|
49
105
|
read: QueryModule.read,
|
|
@@ -51,6 +107,7 @@ export const makeGame = <S extends Schema.Any, Root>(schema: S, _root: Root): Sc
|
|
|
51
107
|
optional: QueryModule.optional,
|
|
52
108
|
added: QueryModule.added,
|
|
53
109
|
changed: QueryModule.changed,
|
|
110
|
+
anyOf: QueryModule.anyOf,
|
|
54
111
|
readRelation: Relation.read,
|
|
55
112
|
optionalRelation: Relation.optional,
|
|
56
113
|
readRelated: Relation.readRelated,
|
|
@@ -79,11 +136,15 @@ export const makeGame = <S extends Schema.Any, Root>(schema: S, _root: Root): Sc
|
|
|
79
136
|
{
|
|
80
137
|
readResource: System.readResource,
|
|
81
138
|
writeResource: System.writeResource,
|
|
139
|
+
changedResource: System.changedResource,
|
|
82
140
|
readEvent: System.readEvent,
|
|
83
141
|
writeEvent: System.writeEvent,
|
|
84
142
|
thisTick: System.thisTick,
|
|
85
143
|
commands: Command.commands,
|
|
86
|
-
spawns
|
|
144
|
+
// `spawns(Player)` also allows what `Player` requires (`Schema.requires`), matching its type.
|
|
145
|
+
spawns: (...components: ReadonlyArray<Descriptor<"component", string, any>>) =>
|
|
146
|
+
// Internally erased: the bound signature already requires at least one component.
|
|
147
|
+
Command.spawns(...(withRequired(requirements, components) as unknown as readonly [Descriptor<"component", string, any>])),
|
|
87
148
|
inserts: Command.inserts,
|
|
88
149
|
removes: Command.removes,
|
|
89
150
|
despawns: Command.despawns,
|
|
@@ -126,7 +187,8 @@ export const makeGame = <S extends Schema.Any, Root>(schema: S, _root: Root): Sc
|
|
|
126
187
|
readonly resources?: object
|
|
127
188
|
readonly machines?: Runtime.RuntimeMachines<any>
|
|
128
189
|
readonly debug?: Debug.Option
|
|
129
|
-
|
|
190
|
+
readonly saves?: Snapshot.Migrations
|
|
191
|
+
}) => Runtime.make({ schema, ...options, machineDefinitions: definedMachines, scopeDefinitions: definedScopes } as never),
|
|
130
192
|
service: Runtime.service,
|
|
131
193
|
services: Runtime.services,
|
|
132
194
|
machine: Runtime.machine,
|
package/src/internal/order.ts
CHANGED
|
@@ -4,19 +4,23 @@
|
|
|
4
4
|
* `Schedule.ts` (`TickOrderErrors`) and produces the same messages.
|
|
5
5
|
*/
|
|
6
6
|
import type { Descriptor } from "../Descriptor.ts"
|
|
7
|
+
import type * as Query from "../Query.ts"
|
|
7
8
|
import * as Schedule from "../Schedule.ts"
|
|
8
9
|
import type { SystemDefinition } from "../System.ts"
|
|
9
10
|
|
|
10
11
|
type AnySystem = SystemDefinition<any, any, any, any, any>
|
|
11
12
|
type OrderIssue = Schedule.OrderIssue
|
|
12
|
-
type Access = { readonly mode: "read" | "write"; readonly descriptor: Descriptor.Any; readonly thisTick?: true }
|
|
13
|
+
type Access = { readonly mode: "read" | "changed" | "write"; readonly descriptor: Descriptor.Any; readonly thisTick?: true }
|
|
13
14
|
type QueryAccess = {
|
|
14
15
|
readonly thisTick?: true
|
|
15
16
|
readonly selection: Record<string, { readonly mode?: string; readonly descriptor?: Descriptor.Any }>
|
|
16
17
|
readonly with: ReadonlyArray<Descriptor.Any>
|
|
17
18
|
readonly without: ReadonlyArray<Descriptor.Any>
|
|
18
|
-
readonly filters: ReadonlyArray<
|
|
19
|
+
readonly filters: ReadonlyArray<Query.Filter<any>>
|
|
19
20
|
}
|
|
21
|
+
/** Every component a filter names: the one of `added` / `changed`, or each member of `anyOf`. */
|
|
22
|
+
const filterNames = (filter: Query.Filter<any>): ReadonlyArray<string> =>
|
|
23
|
+
filter.kind === "anyOf" ? filter.filters.map((member) => member.descriptor.name) : [filter.descriptor.name]
|
|
20
24
|
type Permission = { readonly permission: string; readonly values: ReadonlyArray<{ readonly name: string }> }
|
|
21
25
|
|
|
22
26
|
/** One position of a tick: a system, or a point where queued commands are applied. */
|
|
@@ -65,7 +69,7 @@ const changesQuery = (system: AnySystem, query: QueryAccess): boolean => {
|
|
|
65
69
|
...slots.flatMap((slot) => slot.descriptor ? [slot.descriptor.name] : []),
|
|
66
70
|
...query.with.map((d) => d.name),
|
|
67
71
|
...query.without.map((d) => d.name),
|
|
68
|
-
...query.filters.flatMap((filter) => filter
|
|
72
|
+
...query.filters.flatMap((filter) => filterNames(filter))
|
|
69
73
|
])
|
|
70
74
|
const changes = [...permissionNames(system, "inserts"), ...permissionNames(system, "removes")]
|
|
71
75
|
if (changes.some((name) => named.has(name))) return true
|
|
@@ -95,7 +99,7 @@ const computeIssues = (steps: ReadonlyArray<Schedule.ScheduleStep>): ReadonlyArr
|
|
|
95
99
|
const systemsAt = items.flatMap((item, at) => item.kind === "system" ? [{ system: item.system, at }] : [])
|
|
96
100
|
for (const { system: reader, at: index } of systemsAt) {
|
|
97
101
|
for (const read of accesses(reader)) {
|
|
98
|
-
if (read.mode !== "read" || read.thisTick !== true) continue
|
|
102
|
+
if ((read.mode !== "read" && read.mode !== "changed") || read.thisTick !== true) continue
|
|
99
103
|
const data = read.descriptor.name
|
|
100
104
|
for (const { system, at } of systemsAt) {
|
|
101
105
|
if (at <= index || sameSystem(system, reader)) continue
|