@typeonce/ritmo 0.6.0 → 0.7.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.
Files changed (125) hide show
  1. package/README.md +2 -0
  2. package/dist/Brand.d.ts +9 -4
  3. package/dist/Brand.d.ts.map +1 -1
  4. package/dist/Brand.js +9 -4
  5. package/dist/Brand.js.map +1 -1
  6. package/dist/Clock.d.ts +186 -0
  7. package/dist/Clock.d.ts.map +1 -0
  8. package/dist/Clock.js +111 -0
  9. package/dist/Clock.js.map +1 -0
  10. package/dist/Command.d.ts +157 -33
  11. package/dist/Command.d.ts.map +1 -1
  12. package/dist/Command.js +49 -15
  13. package/dist/Command.js.map +1 -1
  14. package/dist/Debug.d.ts +104 -12
  15. package/dist/Debug.d.ts.map +1 -1
  16. package/dist/Debug.js.map +1 -1
  17. package/dist/Decode.d.ts +1 -1
  18. package/dist/Decode.js +1 -1
  19. package/dist/Descriptor.d.ts +31 -21
  20. package/dist/Descriptor.d.ts.map +1 -1
  21. package/dist/Descriptor.js +21 -12
  22. package/dist/Descriptor.js.map +1 -1
  23. package/dist/Entity.d.ts +14 -5
  24. package/dist/Entity.d.ts.map +1 -1
  25. package/dist/Entity.js +2 -2
  26. package/dist/Entity.js.map +1 -1
  27. package/dist/EntityScope.d.ts +6 -0
  28. package/dist/EntityScope.d.ts.map +1 -1
  29. package/dist/EntityScope.js +6 -0
  30. package/dist/EntityScope.js.map +1 -1
  31. package/dist/HostClock.d.ts +49 -0
  32. package/dist/HostClock.d.ts.map +1 -0
  33. package/dist/HostClock.js +46 -0
  34. package/dist/HostClock.js.map +1 -0
  35. package/dist/Inspector.d.ts +7 -1
  36. package/dist/Inspector.d.ts.map +1 -1
  37. package/dist/Inspector.js.map +1 -1
  38. package/dist/Machine.d.ts +12 -3
  39. package/dist/Machine.d.ts.map +1 -1
  40. package/dist/Machine.js +4 -1
  41. package/dist/Machine.js.map +1 -1
  42. package/dist/Query.d.ts +28 -24
  43. package/dist/Query.d.ts.map +1 -1
  44. package/dist/Query.js +0 -24
  45. package/dist/Query.js.map +1 -1
  46. package/dist/Runtime.d.ts +36 -16
  47. package/dist/Runtime.d.ts.map +1 -1
  48. package/dist/Runtime.js +230 -67
  49. package/dist/Runtime.js.map +1 -1
  50. package/dist/Schedule.d.ts +217 -10
  51. package/dist/Schedule.d.ts.map +1 -1
  52. package/dist/Schedule.js +5 -0
  53. package/dist/Schedule.js.map +1 -1
  54. package/dist/Schema.d.ts +61 -25
  55. package/dist/Schema.d.ts.map +1 -1
  56. package/dist/Schema.js +0 -8
  57. package/dist/Schema.js.map +1 -1
  58. package/dist/System.d.ts +149 -52
  59. package/dist/System.d.ts.map +1 -1
  60. package/dist/System.js +52 -6
  61. package/dist/System.js.map +1 -1
  62. package/dist/index.d.ts +8 -7
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +8 -7
  65. package/dist/index.js.map +1 -1
  66. package/dist/internal/cells.d.ts +3 -3
  67. package/dist/internal/cells.d.ts.map +1 -1
  68. package/dist/internal/cells.js +9 -11
  69. package/dist/internal/cells.js.map +1 -1
  70. package/dist/internal/debug.d.ts +6 -0
  71. package/dist/internal/debug.d.ts.map +1 -1
  72. package/dist/internal/debug.js +95 -1
  73. package/dist/internal/debug.js.map +1 -1
  74. package/dist/internal/game.d.ts.map +1 -1
  75. package/dist/internal/game.js +9 -2
  76. package/dist/internal/game.js.map +1 -1
  77. package/dist/internal/order.d.ts +10 -0
  78. package/dist/internal/order.d.ts.map +1 -0
  79. package/dist/internal/order.js +120 -0
  80. package/dist/internal/order.js.map +1 -0
  81. package/dist/internal/queries.js +2 -2
  82. package/dist/internal/queries.js.map +1 -1
  83. package/package.json +1 -1
  84. package/src/Brand.ts +9 -4
  85. package/src/Clock.ts +311 -0
  86. package/src/Command.ts +271 -73
  87. package/src/Debug.ts +111 -10
  88. package/src/Decode.ts +1 -1
  89. package/src/Descriptor.ts +37 -27
  90. package/src/Entity.ts +15 -5
  91. package/src/EntityScope.ts +6 -0
  92. package/src/HostClock.ts +64 -0
  93. package/src/Inspector.ts +7 -2
  94. package/src/Machine.ts +13 -3
  95. package/src/Query.ts +41 -33
  96. package/src/Runtime.ts +273 -91
  97. package/src/Schedule.ts +380 -13
  98. package/src/Schema.ts +70 -41
  99. package/src/System.ts +174 -88
  100. package/src/index.ts +8 -7
  101. package/src/internal/cells.ts +10 -12
  102. package/src/internal/debug.ts +104 -4
  103. package/src/internal/game.ts +11 -3
  104. package/src/internal/order.ts +140 -0
  105. package/src/internal/queries.ts +3 -3
  106. package/dist/Definition.d.ts +0 -99
  107. package/dist/Definition.d.ts.map +0 -1
  108. package/dist/Definition.js +0 -68
  109. package/dist/Definition.js.map +0 -1
  110. package/dist/Feature.d.ts +0 -106
  111. package/dist/Feature.d.ts.map +0 -1
  112. package/dist/Feature.js +0 -87
  113. package/dist/Feature.js.map +0 -1
  114. package/dist/Fx.d.ts +0 -128
  115. package/dist/Fx.d.ts.map +0 -1
  116. package/dist/Fx.js +0 -112
  117. package/dist/Fx.js.map +0 -1
  118. package/dist/internal/definition.d.ts +0 -17
  119. package/dist/internal/definition.d.ts.map +0 -1
  120. package/dist/internal/definition.js +0 -25
  121. package/dist/internal/definition.js.map +0 -1
  122. package/src/Definition.ts +0 -106
  123. package/src/Feature.ts +0 -241
  124. package/src/Fx.ts +0 -176
  125. package/src/internal/definition.ts +0 -56
@@ -11,6 +11,7 @@ import type * as Machine from "../Machine.ts"
11
11
  import type * as Query from "../Query.ts"
12
12
  import type * as Relation from "../Relation.ts"
13
13
  import * as Schedule from "../Schedule.ts"
14
+ import * as OrderInternal from "./order.ts"
14
15
  import type { Schema } from "../Schema.ts"
15
16
  import type { SystemDefinition } from "../System.ts"
16
17
 
@@ -114,6 +115,30 @@ const describeQuery = (slot: string, query: Query.Query.Any<any>): Debug.QueryDe
114
115
  }
115
116
 
116
117
  type Mode = { readonly mode: "read" | "write"; readonly descriptor: Descriptor.Any }
118
+ type CommandAccess = { readonly permissions: ReadonlyArray<{ readonly permission: keyof Debug.CommandPermissions; readonly values: ReadonlyArray<{ readonly name: string }> }> }
119
+
120
+ const permissionsOf = (access: CommandAccess | undefined): Debug.CommandPermissions => {
121
+ const of = (kind: keyof Debug.CommandPermissions) =>
122
+ [...new Set((access?.permissions ?? []).filter((p) => p.permission === kind).flatMap((p) => p.values.map((value) => value.name)))]
123
+ return {
124
+ spawns: of("spawns"),
125
+ inserts: of("inserts"),
126
+ removes: of("removes"),
127
+ despawns: of("despawns"),
128
+ despawnsScope: of("despawnsScope"),
129
+ relates: of("relates")
130
+ }
131
+ }
132
+
133
+ const permissionVerb: Record<keyof Debug.CommandPermissions, string> = {
134
+ spawns: "spawn",
135
+ inserts: "insert",
136
+ removes: "remove",
137
+ despawns: "despawn",
138
+ despawnsScope: "despawn the scope",
139
+ relates: "relate"
140
+ }
141
+ type ThisTickAccess = Mode & { readonly thisTick?: true }
117
142
 
118
143
  export const describeSystem = (system: AnySystem, placements: ReadonlyArray<string>): Debug.SystemDescription => {
119
144
  const spec = system.spec
@@ -148,7 +173,16 @@ export const describeSystem = (system: AnySystem, placements: ReadonlyArray<stri
148
173
  removed: names(spec.removed as Record<string, { readonly descriptor: Descriptor.Any }>, (access) => access.descriptor.name),
149
174
  despawned: Object.keys(spec.despawned).length > 0,
150
175
  relationFailures: names(spec.relationFailures as Record<string, { readonly relation: Relation.Relation.Any }>, (access) => access.relation.name),
176
+ thisTick: [
177
+ ...[...Object.values(spec.events as Record<string, ThisTickAccess>), ...Object.values(spec.resources as Record<string, ThisTickAccess>)]
178
+ .filter((access) => access.thisTick === true)
179
+ .map((access) => access.descriptor.name),
180
+ ...Object.entries(spec.queries as Record<string, { readonly thisTick?: true }>)
181
+ .filter(([, query]) => query.thisTick === true)
182
+ .map(([slot]) => `queries.${slot}`)
183
+ ],
151
184
  services: names(spec.services as Record<string, { readonly descriptor: Descriptor.Any }>, (access) => access.descriptor.name),
185
+ commands: permissionsOf(spec.commands as CommandAccess | undefined),
152
186
  when: (spec.when as ReadonlyArray<Machine.Condition>).map(renderCondition),
153
187
  pauseWhen: (spec.pauseWhen as ReadonlyArray<Machine.Condition>).map(renderCondition),
154
188
  local: spec.local !== undefined
@@ -228,6 +262,12 @@ export interface DescribeInput {
228
262
  readonly currentMachine: (machine: Machine.StateMachine.Any) => Machine.StateValue | undefined
229
263
  readonly providedServices: ReadonlyArray<string>
230
264
  readonly externallyWrittenEvents: ReadonlySet<string>
265
+ /** Relations commands have tried to relate or reorder, by the queuing systems. */
266
+ readonly relationMutations: ReadonlyMap<string, ReadonlySet<string>>
267
+ /** What each system's applied commands did, as `kind:name`; see `Runtime`. */
268
+ readonly commandUses: ReadonlyMap<string, ReadonlySet<string>>
269
+ /** Systems that ran in this session. */
270
+ readonly ranSystems: ReadonlySet<string>
231
271
  readonly hasResource: (descriptor: Descriptor.Any) => boolean
232
272
  readonly schedules: ReadonlyArray<readonly [string, AnySchedule]>
233
273
  }
@@ -269,6 +309,7 @@ export const describe = (input: DescribeInput): Debug.Description => {
269
309
  }
270
310
  }
271
311
  for (const name of system.removed) components.read(name, system.name)
312
+ for (const name of [...system.commands.spawns, ...system.commands.inserts, ...system.commands.removes]) components.write(name, system.name)
272
313
  for (const name of system.resources.reads) resources.read(name, system.name)
273
314
  for (const name of system.resources.writes) resources.write(name, system.name)
274
315
  for (const name of system.events.reads) events.read(name, system.name)
@@ -283,7 +324,7 @@ export const describe = (input: DescribeInput): Debug.Description => {
283
324
  const appliesTransitions = schedules.some(([, schedule]) =>
284
325
  schedule.steps.some((step) => !Schedule.isSystemStep(step) && step.kind === "applyStateTransitions"))
285
326
 
286
- const lints: Array<Debug.Lint> = []
327
+ const lints: Array<Omit<Debug.Lint, "fix">> = []
287
328
  if (schedules.length > 0) {
288
329
  for (const entry of events.list()) {
289
330
  if (entry.writers.length > 0 && entry.readers.length === 0) {
@@ -313,7 +354,44 @@ export const describe = (input: DescribeInput): Debug.Description => {
313
354
  })
314
355
  }
315
356
  }
357
+ const failureReaders = new Set(systems.flatMap((system) => system.relationFailures))
358
+ // Declared `relates` permissions, and relations that commands were seen to relate (drafts relate without one).
359
+ const relaters = new Map<string, Set<string>>()
360
+ for (const system of systems) {
361
+ for (const name of system.commands.relates) relaters.set(name, new Set([...(relaters.get(name) ?? []), system.name]))
362
+ }
363
+ for (const [name, queuers] of input.relationMutations) relaters.set(name, new Set([...(relaters.get(name) ?? []), ...queuers]))
364
+ for (const [name, queuers] of relaters) {
365
+ if (failureReaders.has(name)) continue
366
+ lints.push({
367
+ severity: "warning",
368
+ code: "relation-failure-never-read",
369
+ subject: name,
370
+ 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
+ })
372
+ }
316
373
  lints.push(...readBeforeWriteLints(schedules, described, input.externallyWrittenEvents))
374
+ // Each named schedule is checked as one tick; `tick` checks this at compile time.
375
+ for (const [name, schedule] of input.schedules) {
376
+ for (const issue of OrderInternal.tickOrderIssues(schedule.steps)) {
377
+ lints.push({ severity: "warning", code: "this-tick-order", subject: issue.data, message: `in ${name}, ${issue.message}` })
378
+ }
379
+ }
380
+ // Observed with debug: only systems that ran, so an unplayed session reports nothing.
381
+ for (const system of systems) {
382
+ if (!input.ranSystems.has(system.name)) continue
383
+ const uses = input.commandUses.get(system.name) ?? new Set<string>()
384
+ for (const kind of Object.keys(permissionVerb) as Array<keyof Debug.CommandPermissions>) {
385
+ const unused = system.commands[kind].filter((name) => !uses.has(`${kind}:${name}`))
386
+ if (unused.length === 0) continue
387
+ lints.push({
388
+ severity: "info",
389
+ code: "command-permission-unused",
390
+ subject: system.name,
391
+ message: `${system.name} may ${permissionVerb[kind]} ${unused.join(", ")} but never did in this session`
392
+ })
393
+ }
394
+ }
317
395
  for (const entry of components.list()) {
318
396
  if (entry.writers.length > 0 && entry.readers.every((reader) => entry.writers.includes(reader))) {
319
397
  lints.push({
@@ -355,7 +433,29 @@ export const describe = (input: DescribeInput): Debug.Description => {
355
433
  resources: resources.list(),
356
434
  events: events.list()
357
435
  },
358
- lints
436
+ lints: lints.map((lint) => ({ ...lint, fix: fixFor(lint.code, lint.subject) }))
437
+ }
438
+ }
439
+
440
+ /** The fix each lint names, by code. */
441
+ const fixFor = (code: Debug.Lint["code"], subject: string): string => {
442
+ switch (code) {
443
+ case "event-never-read":
444
+ return `read ${subject} in a system with Game.System.readEvent, or stop emitting it; if only a render schedule reads it, pass that schedule as describe-only`
445
+ case "event-never-written":
446
+ 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
+ case "next-state-never-applied":
448
+ return `add Game.Schedule.applyStateTransitions(...) to a schedule that runs after the system that queues ${subject}`
449
+ case "relation-failure-never-read":
450
+ return `read the failures with Game.System.readRelationFailures(${subject}) in a system and handle them`
451
+ case "this-tick-order":
452
+ return "move the writer before the reader in the schedule (the message names both), or read without Game.System.thisTick"
453
+ case "component-never-read":
454
+ return `fine if ${subject} is read outside the described schedules (rendering, inspectors); otherwise stop writing it`
455
+ case "command-permission-unused":
456
+ return `fine if the session did not reach the case where ${subject} uses it; otherwise drop the permission from its Game.System.commands(...)`
457
+ case "read-before-write":
458
+ return "fine when a one-run lag is intended (input before movement); otherwise move the reader after the writer, or read with Game.System.thisTick to make it an error"
359
459
  }
360
460
  }
361
461
 
@@ -380,8 +480,8 @@ const readBeforeWriteLints = (
380
480
  schedules: ReadonlyArray<readonly [string, AnySchedule]>,
381
481
  described: ReadonlyMap<AnySystem, Debug.SystemDescription>,
382
482
  externallyWrittenEvents: ReadonlySet<string>
383
- ): Array<Debug.Lint> => {
384
- const lints: Array<Debug.Lint> = []
483
+ ): Array<Omit<Debug.Lint, "fix">> => {
484
+ const lints: Array<Omit<Debug.Lint, "fix">> = []
385
485
  for (const [name, schedule] of schedules) {
386
486
  const steps = schedule.steps.flatMap((step) =>
387
487
  Schedule.isSystemStep(step) ? [{ system: described.get(step as AnySystem)!, accesses: accessesOf(described.get(step as AnySystem)!) }] : [])
@@ -5,6 +5,7 @@
5
5
  * module functions; the precise public types live on `Schema.Game`.
6
6
  */
7
7
  import * as Command from "../Command.ts"
8
+ import type * as Debug from "../Debug.ts"
8
9
  import * as Condition from "../Condition.ts"
9
10
  import * as Entity from "../Entity.ts"
10
11
  import * as EntityScope from "../EntityScope.ts"
@@ -57,9 +58,8 @@ export const makeGame = <S extends Schema.Any, Root>(schema: S, _root: Root): Sc
57
58
  }),
58
59
  Command: {
59
60
  spawn: Command.spawn,
60
- insert: Command.insert,
61
+ extend: Command.extend,
61
62
  entry: Command.entry,
62
- entryResult: Command.entryResult,
63
63
  entryRaw: Command.entryRaw,
64
64
  relate: Command.relate
65
65
  },
@@ -81,6 +81,14 @@ export const makeGame = <S extends Schema.Any, Root>(schema: S, _root: Root): Sc
81
81
  writeResource: System.writeResource,
82
82
  readEvent: System.readEvent,
83
83
  writeEvent: System.writeEvent,
84
+ thisTick: System.thisTick,
85
+ commands: Command.commands,
86
+ spawns: Command.spawns,
87
+ inserts: Command.inserts,
88
+ removes: Command.removes,
89
+ despawns: Command.despawns,
90
+ despawnsScope: Command.despawnsScope,
91
+ relates: Command.relates,
84
92
  service: System.service,
85
93
  machine: System.machine,
86
94
  nextState: System.nextState,
@@ -117,7 +125,7 @@ export const makeGame = <S extends Schema.Any, Root>(schema: S, _root: Root): Sc
117
125
  readonly services: Runtime.RuntimeServices<any>
118
126
  readonly resources?: object
119
127
  readonly machines?: Runtime.RuntimeMachines<any>
120
- readonly debug?: true
128
+ readonly debug?: Debug.Option
121
129
  }) => Runtime.make({ schema, ...options, machineDefinitions: definedMachines } as never),
122
130
  service: Runtime.service,
123
131
  services: Runtime.services,
@@ -0,0 +1,140 @@
1
+ /**
2
+ * The runtime form of the `Game.System.thisTick` check, for erased schedules
3
+ * (`tryTick`) and the `this-tick-order` lint. The compile-time form lives in
4
+ * `Schedule.ts` (`TickOrderErrors`) and produces the same messages.
5
+ */
6
+ import type { Descriptor } from "../Descriptor.ts"
7
+ import * as Schedule from "../Schedule.ts"
8
+ import type { SystemDefinition } from "../System.ts"
9
+
10
+ type AnySystem = SystemDefinition<any, any, any, any, any>
11
+ type OrderIssue = Schedule.OrderIssue
12
+ type Access = { readonly mode: "read" | "write"; readonly descriptor: Descriptor.Any; readonly thisTick?: true }
13
+ type QueryAccess = {
14
+ readonly thisTick?: true
15
+ readonly selection: Record<string, { readonly mode?: string; readonly descriptor?: Descriptor.Any }>
16
+ readonly with: ReadonlyArray<Descriptor.Any>
17
+ readonly without: ReadonlyArray<Descriptor.Any>
18
+ readonly filters: ReadonlyArray<{ readonly descriptor?: Descriptor.Any }>
19
+ }
20
+ type Permission = { readonly permission: string; readonly values: ReadonlyArray<{ readonly name: string }> }
21
+
22
+ /** One position of a tick: a system, or a point where queued commands are applied. */
23
+ type Item = { readonly kind: "system"; readonly system: AnySystem } | { readonly kind: "flush" }
24
+
25
+ const accesses = (system: AnySystem): ReadonlyArray<Access> => [
26
+ ...Object.values(system.spec.events as Record<string, Access>),
27
+ ...Object.values(system.spec.resources as Record<string, Access>)
28
+ ]
29
+
30
+ const sameData = (left: Descriptor.Any, right: Descriptor.Any): boolean =>
31
+ left.kind === right.kind && left.name === right.name
32
+
33
+ const sameSystem = (left: AnySystem, right: AnySystem): boolean => (left.base ?? left) === (right.base ?? right)
34
+
35
+ /**
36
+ * The positions of a tick, flattened: an `applyStateTransitions(...)` step
37
+ * applies queued commands, then runs every schedule of its bundle (as if
38
+ * each may run), markers inside them included.
39
+ */
40
+ const flatten = (steps: ReadonlyArray<Schedule.ScheduleStep>): Array<Item> =>
41
+ steps.flatMap((step): Array<Item> => {
42
+ if (Schedule.isSystemStep(step)) return [{ kind: "system", system: step as AnySystem }]
43
+ if (step.kind === "applyDeferred") return [{ kind: "flush" }]
44
+ const bundle = step.bundle as { readonly entries: ReadonlyArray<{ readonly steps: ReadonlyArray<Schedule.ScheduleStep> }> } | undefined
45
+ return [{ kind: "flush" }, ...(bundle?.entries ?? []).flatMap((entry) => flatten(entry.steps))]
46
+ })
47
+
48
+ const permissionNames = (system: AnySystem, kind: string): Set<string> => {
49
+ const commands = system.spec.commands as { readonly permissions: ReadonlyArray<Permission> } | undefined
50
+ return new Set((commands?.permissions ?? []).filter((p) => p.permission === kind).flatMap((p) => p.values.map((value) => value.name)))
51
+ }
52
+
53
+ /**
54
+ * Whether a system's declared commands can change what a query sees: spawns
55
+ * covering every component it requires, or inserts / removes of any
56
+ * component it names. Despawns never count.
57
+ */
58
+ const changesQuery = (system: AnySystem, query: QueryAccess): boolean => {
59
+ const slots = Object.values(query.selection)
60
+ const required = [
61
+ ...slots.filter((slot) => slot.mode === "read" || slot.mode === "write").flatMap((slot) => slot.descriptor ? [slot.descriptor.name] : []),
62
+ ...query.with.map((d) => d.name)
63
+ ]
64
+ const named = new Set([
65
+ ...slots.flatMap((slot) => slot.descriptor ? [slot.descriptor.name] : []),
66
+ ...query.with.map((d) => d.name),
67
+ ...query.without.map((d) => d.name),
68
+ ...query.filters.flatMap((filter) => filter.descriptor ? [filter.descriptor.name] : [])
69
+ ])
70
+ const changes = [...permissionNames(system, "inserts"), ...permissionNames(system, "removes")]
71
+ if (changes.some((name) => named.has(name))) return true
72
+ const spawns = permissionNames(system, "spawns")
73
+ return spawns.size > 0 && required.every((name) => spawns.has(name))
74
+ }
75
+
76
+ /**
77
+ * Checks the systems of one tick, in the order they run. A read with no
78
+ * writer in the tick is fine (a reused reader); `event-never-written` covers
79
+ * a writer missing everywhere.
80
+ */
81
+ export const tickOrderIssues = (steps: ReadonlyArray<Schedule.ScheduleStep>): ReadonlyArray<OrderIssue> => {
82
+ // A schedule's steps never change, so `tryTick` checks each schedule once.
83
+ const cached = checked.get(steps)
84
+ if (cached !== undefined) return cached
85
+ const issues = computeIssues(steps)
86
+ checked.set(steps, issues)
87
+ return issues
88
+ }
89
+
90
+ const checked = new WeakMap<ReadonlyArray<Schedule.ScheduleStep>, ReadonlyArray<OrderIssue>>()
91
+
92
+ const computeIssues = (steps: ReadonlyArray<Schedule.ScheduleStep>): ReadonlyArray<OrderIssue> => {
93
+ const items = flatten(steps)
94
+ const issues: Array<OrderIssue> = []
95
+ const systemsAt = items.flatMap((item, at) => item.kind === "system" ? [{ system: item.system, at }] : [])
96
+ for (const { system: reader, at: index } of systemsAt) {
97
+ for (const read of accesses(reader)) {
98
+ if (read.mode !== "read" || read.thisTick !== true) continue
99
+ const data = read.descriptor.name
100
+ for (const { system, at } of systemsAt) {
101
+ if (at <= index || sameSystem(system, reader)) continue
102
+ if (!accesses(system).some((access) => access.mode === "write" && sameData(access.descriptor, read.descriptor))) continue
103
+ issues.push({
104
+ kind: "lateWriter",
105
+ reader: reader.name,
106
+ data,
107
+ writer: system.name,
108
+ message: `${reader.name} needs ${data} from this tick, but its writer ${system.name} runs after it: move ${system.name} before ${reader.name}, or read ${data} without thisTick`
109
+ })
110
+ }
111
+ }
112
+ for (const [slot, query] of Object.entries(reader.spec.queries as Record<string, QueryAccess>)) {
113
+ if (query.thisTick !== true) continue
114
+ const data = `queries.${slot}`
115
+ let lastFlush = -1
116
+ for (let at = 0; at < index; at++) if (items[at]!.kind === "flush") lastFlush = at
117
+ for (const { system, at } of systemsAt) {
118
+ if (sameSystem(system, reader) || !changesQuery(system, query)) continue
119
+ if (at > index) {
120
+ issues.push({
121
+ kind: "lateWriter",
122
+ reader: reader.name,
123
+ data,
124
+ writer: system.name,
125
+ message: `${system.name} queues commands that ${reader.name} needs this tick (${data}), but it runs after ${reader.name}: move ${system.name} before the applyDeferred() that precedes ${reader.name}, or read ${data} without thisTick`
126
+ })
127
+ } else if (at > lastFlush && at < index) {
128
+ issues.push({
129
+ kind: "unflushedWriter",
130
+ reader: reader.name,
131
+ data,
132
+ writer: system.name,
133
+ message: `${reader.name} needs this tick's commands for ${data}, but ${system.name} queues them with no applyDeferred() in between: add Game.Schedule.applyDeferred() after ${system.name}`
134
+ })
135
+ }
136
+ }
137
+ }
138
+ }
139
+ return issues
140
+ }
@@ -33,7 +33,7 @@ type SlotPlan =
33
33
  readonly mode: "write"
34
34
  readonly ordinal: number
35
35
  readonly constructor: DescriptorModule.ResultConstructor<unknown, unknown, unknown> | undefined
36
- readonly state: string | undefined
36
+ readonly entityState: string | undefined
37
37
  }
38
38
  | {
39
39
  readonly slot: string
@@ -143,7 +143,7 @@ export const makeQueryEngine = (world: World) => {
143
143
  mode: "write",
144
144
  ordinal,
145
145
  constructor: DescriptorModule.constructorOf(access.descriptor),
146
- state: DescriptorModule.isState(access.descriptor) ? access.descriptor.name : undefined
146
+ entityState: DescriptorModule.isEntityState(access.descriptor) ? access.descriptor.name : undefined
147
147
  })
148
148
  } else {
149
149
  slots.push({ slot, mode: "read", ordinal })
@@ -255,7 +255,7 @@ export const makeQueryEngine = (world: World) => {
255
255
  case "read":
256
256
  return Cells.componentRead(record, plan.ordinal)
257
257
  case "write":
258
- return Cells.componentWrite(record, plan.ordinal, world, plan.constructor, plan.state)
258
+ return Cells.componentWrite(record, plan.ordinal, world, plan.constructor, plan.entityState)
259
259
  case "optional":
260
260
  return Cells.componentOptional(record, plan.ordinal)
261
261
  case "readRelation":
@@ -1,99 +0,0 @@
1
- /**
2
- * Helpers for validating reusable authored values once and reusing them later.
3
- *
4
- * `Definition` is intended for static or rarely changing authored data such as
5
- * spawn points, collider sizes, arena bounds, or other constants that should
6
- * cross the validation boundary once and then remain branded afterward.
7
- *
8
- * In game code this is the "content setup" companion to runtime/world setup:
9
- * validate authored constants early, keep them in carried form, and then reuse
10
- * them from setup systems, reset systems, or content modules without
11
- * revalidating on every frame.
12
- *
13
- * @example
14
- * ```ts
15
- * // Validate authored constants once when defining the content bundle.
16
- * const player = Definition.all({
17
- * spawn: Definition.entry(Vector2, { x: 32, y: 48 }),
18
- * size: Definition.entry(Size2, { width: 16, height: 16 })
19
- * })
20
- * ```
21
- *
22
- * @module Definition
23
- * @docGroup core
24
- *
25
- * @categoryDescription Definition Types
26
- * Named reusable shapes used to validate and retain authored values.
27
- *
28
- * @categoryDescription Definition Builders
29
- * Helpers that validate one or several authored values through existing constructors.
30
- */
31
- import * as internal from "./internal/definition.ts";
32
- import type * as Result from "./Result.ts";
33
- /**
34
- * One reusable validated definition entry.
35
- *
36
- * @category Definition Types
37
- */
38
- export type Entry<Value, Error> = internal.Entry<Value, Error>;
39
- /**
40
- * Record of named definition entries accepted by {@link all}.
41
- *
42
- * @category Definition Types
43
- */
44
- export type Entries = internal.Entries;
45
- /**
46
- * Minimal constructor contract accepted by {@link entry}.
47
- *
48
- * @category Definition Types
49
- */
50
- export type ResultConstructor<Value, Raw, Error> = internal.ResultConstructor<Value, Raw, Error>;
51
- /**
52
- * Success shape produced by {@link all}.
53
- *
54
- * @category Definition Types
55
- */
56
- export type SuccessOf<Input extends Entries> = internal.SuccessOf<Input>;
57
- /**
58
- * Error shape produced by {@link all}.
59
- *
60
- * @category Definition Types
61
- */
62
- export type ErrorOf<Input extends Entries> = internal.ErrorOf<Input>;
63
- /**
64
- * Validates one reusable authored value through an existing constructor.
65
- *
66
- * Use this for one named authored constant that should cross a constructor
67
- * boundary now and remain in carried form afterward.
68
- *
69
- * @example
70
- * ```ts
71
- * // Validate a single authored position once during content setup.
72
- * const playerSpawn = Definition.entry(Vector2, { x: 32, y: 48 })
73
- * ```
74
- *
75
- * @category Definition Builders
76
- */
77
- export declare const entry: <Value, Raw, Error>(constructor: ResultConstructor<Value, Raw, Error>, raw: Raw) => Entry<Value, Error>;
78
- /**
79
- * Validates a named record of reusable authored values at once.
80
- *
81
- * Successful entries are returned under the same keys. Failed entries remain
82
- * keyed so authored-data setup can report exactly which constant was invalid.
83
- *
84
- * Use this when one content bundle or level definition should either validate
85
- * as a whole or report a structured set of authoring failures.
86
- *
87
- * @example
88
- * ```ts
89
- * // Validate one authored content bundle and keep failures keyed by field.
90
- * const definitions = Definition.all({
91
- * spawn: Definition.entry(Vector2, { x: 32, y: 48 }),
92
- * collider: Definition.entry(Size2, { width: 16, height: 16 })
93
- * })
94
- * ```
95
- *
96
- * @category Definition Builders
97
- */
98
- export declare const all: <Input extends Entries>(input: Input) => Result.Result<SuccessOf<Input>, ErrorOf<Input>>;
99
- //# sourceMappingURL=Definition.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Definition.d.ts","sourceRoot":"","sources":["../src/Definition.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,KAAK,QAAQ,MAAM,0BAA0B,CAAA;AACpD,OAAO,KAAK,KAAK,MAAM,MAAM,aAAa,CAAA;AAE1C;;;;GAIG;AACH,MAAM,MAAM,KAAK,CAAC,KAAK,EAAE,KAAK,IAAI,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;AAC9D;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAA;AACtC;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,IAAI,QAAQ,CAAC,iBAAiB,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,CAAA;AAChG;;;;GAIG;AACH,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,OAAO,IAAI,QAAQ,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;AACxE;;;;GAIG;AACH,MAAM,MAAM,OAAO,CAAC,KAAK,SAAS,OAAO,IAAI,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;AAEpE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,KAAK,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,EACpC,WAAW,EAAE,iBAAiB,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,EACjD,GAAG,EAAE,GAAG,KACL,KAAK,CAAC,KAAK,EAAE,KAAK,CAAkB,CAAA;AAEzC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,GAAG,EAAE,CAAC,KAAK,SAAS,OAAO,EACtC,KAAK,EAAE,KAAK,KACT,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAgB,CAAA"}
@@ -1,68 +0,0 @@
1
- /**
2
- * Helpers for validating reusable authored values once and reusing them later.
3
- *
4
- * `Definition` is intended for static or rarely changing authored data such as
5
- * spawn points, collider sizes, arena bounds, or other constants that should
6
- * cross the validation boundary once and then remain branded afterward.
7
- *
8
- * In game code this is the "content setup" companion to runtime/world setup:
9
- * validate authored constants early, keep them in carried form, and then reuse
10
- * them from setup systems, reset systems, or content modules without
11
- * revalidating on every frame.
12
- *
13
- * @example
14
- * ```ts
15
- * // Validate authored constants once when defining the content bundle.
16
- * const player = Definition.all({
17
- * spawn: Definition.entry(Vector2, { x: 32, y: 48 }),
18
- * size: Definition.entry(Size2, { width: 16, height: 16 })
19
- * })
20
- * ```
21
- *
22
- * @module Definition
23
- * @docGroup core
24
- *
25
- * @categoryDescription Definition Types
26
- * Named reusable shapes used to validate and retain authored values.
27
- *
28
- * @categoryDescription Definition Builders
29
- * Helpers that validate one or several authored values through existing constructors.
30
- */
31
- import * as internal from "./internal/definition.js";
32
- /**
33
- * Validates one reusable authored value through an existing constructor.
34
- *
35
- * Use this for one named authored constant that should cross a constructor
36
- * boundary now and remain in carried form afterward.
37
- *
38
- * @example
39
- * ```ts
40
- * // Validate a single authored position once during content setup.
41
- * const playerSpawn = Definition.entry(Vector2, { x: 32, y: 48 })
42
- * ```
43
- *
44
- * @category Definition Builders
45
- */
46
- export const entry = internal.entry;
47
- /**
48
- * Validates a named record of reusable authored values at once.
49
- *
50
- * Successful entries are returned under the same keys. Failed entries remain
51
- * keyed so authored-data setup can report exactly which constant was invalid.
52
- *
53
- * Use this when one content bundle or level definition should either validate
54
- * as a whole or report a structured set of authoring failures.
55
- *
56
- * @example
57
- * ```ts
58
- * // Validate one authored content bundle and keep failures keyed by field.
59
- * const definitions = Definition.all({
60
- * spawn: Definition.entry(Vector2, { x: 32, y: 48 }),
61
- * collider: Definition.entry(Size2, { width: 16, height: 16 })
62
- * })
63
- * ```
64
- *
65
- * @category Definition Builders
66
- */
67
- export const all = internal.all;
68
- //# sourceMappingURL=Definition.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Definition.js","sourceRoot":"","sources":["../src/Definition.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,KAAK,QAAQ,MAAM,0BAA0B,CAAA;AAkCpD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,KAAK,GAGS,QAAQ,CAAC,KAAK,CAAA;AAEzC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,GAAG,GAEuC,QAAQ,CAAC,GAAG,CAAA"}
package/dist/Feature.d.ts DELETED
@@ -1,106 +0,0 @@
1
- import type * as Schedule from "./Schedule.ts";
2
- import type { MergeSchemaDefinitions, Schema, SchemaDefinition, ValidateFragments } from "./Schema.ts";
3
- type EmptySchema = SchemaDefinition<{}, {}, {}, {}>;
4
- type AnyFeature = {
5
- readonly kind: "feature";
6
- readonly name: string;
7
- readonly schema: Schema.Any;
8
- readonly requires: ReadonlyArray<AnyFeature>;
9
- readonly build: unknown;
10
- readonly output: FeatureBuildOutput;
11
- };
12
- type AnySchedule = Schedule.ScheduleDefinition<any, any, any, any, any>;
13
- /**
14
- * What a feature builder returns: bootstrap and update schedules, plus any
15
- * other values the feature wants to expose on the composed project.
16
- */
17
- export type FeatureBuildOutput = {
18
- readonly bootstrap?: ReadonlyArray<AnySchedule>;
19
- readonly update?: ReadonlyArray<AnySchedule>;
20
- };
21
- /**
22
- * The bound authoring surface a feature builder receives. It is the normal
23
- * `Game` API restricted to the descriptors the feature can see; runtimes are
24
- * built from the composed project instead.
25
- */
26
- export type FeatureBuildGame<Accessible extends Schema.Any, Root = unknown> = Omit<Schema.Game<Accessible, Root>, "Runtime">;
27
- type FeatureBuildFunction<Accessible extends Schema.Any, Output extends FeatureBuildOutput = FeatureBuildOutput> = (Game: FeatureBuildGame<Accessible>) => Output;
28
- export interface FeatureDefinition<Name extends string = string, FeatureSchema extends Schema.Any = Schema.Any, Requires extends ReadonlyArray<AnyFeature> = ReadonlyArray<AnyFeature>, Output extends FeatureBuildOutput = FeatureBuildOutput> {
29
- readonly kind: "feature";
30
- readonly name: Name;
31
- readonly schema: FeatureSchema;
32
- readonly requires: Requires;
33
- readonly build: FeatureBuildFunction<MergeSchemaDefinitions<FeatureSchema, MergeFeatureSchemas<Requires>>, Output>;
34
- readonly output: Output;
35
- }
36
- type MergeFeatureSchemas<Features extends ReadonlyArray<AnyFeature>> = Features extends readonly [infer Head extends AnyFeature, ...infer Tail extends Array<AnyFeature>] ? MergeSchemaDefinitions<FeatureClosureSchema<Head>, MergeFeatureSchemas<Tail>> : EmptySchema;
37
- type FeatureClosureSchema<F extends AnyFeature> = F extends FeatureDefinition<any, infer FeatureSchema extends Schema.Any, infer Requires extends ReadonlyArray<AnyFeature>, any> ? MergeSchemaDefinitions<FeatureSchema, MergeFeatureSchemas<Requires>> : EmptySchema;
38
- type FeatureNames<Features extends ReadonlyArray<AnyFeature>> = Features[number]["name"];
39
- type DuplicateFeatureNames<Features extends ReadonlyArray<AnyFeature>, Seen extends string = never> = Features extends readonly [infer Head extends AnyFeature, ...infer Tail extends Array<AnyFeature>] ? Head["name"] extends Seen ? Head["name"] | DuplicateFeatureNames<Tail, Seen> : DuplicateFeatureNames<Tail, Seen | Head["name"]> : never;
40
- type MissingFeatureDependencies<Features extends ReadonlyArray<AnyFeature>> = Exclude<Features[number]["requires"][number]["name"], FeatureNames<Features>>;
41
- type FeatureSchemas<Features extends ReadonlyArray<AnyFeature>> = {
42
- readonly [K in keyof Features]: Features[K]["schema"];
43
- };
44
- /**
45
- * Rejects duplicate feature names, missing required features, and schema
46
- * fragments whose keys or descriptor names collide.
47
- */
48
- type ValidateFeatureSelection<Features extends ReadonlyArray<AnyFeature>> = [
49
- DuplicateFeatureNames<Features>
50
- ] extends [never] ? [MissingFeatureDependencies<Features>] extends [never] ? ValidateFragments<FeatureSchemas<Features>> : {
51
- readonly __fixFeatureDependencies__: MissingFeatureDependencies<Features>;
52
- } : {
53
- readonly __fixFeatureSelection__: DuplicateFeatureNames<Features>;
54
- };
55
- type RebindFeatureSchedule<ScheduleValue, S extends Schema.Any, Root> = ScheduleValue extends Schedule.ScheduleDefinition<any, infer Requirements, any, infer Carried, infer Failure> ? Schedule.ScheduleDefinition<S, Requirements, Root, Carried, Failure> : never;
56
- type FeatureScheduleArray<Output, Key extends "bootstrap" | "update"> = Key extends keyof Output ? Extract<Output[Key], ReadonlyArray<AnySchedule>> : readonly [];
57
- type NormalizeFeatureOutput<Output extends object, S extends Schema.Any, Root> = {
58
- readonly [K in Exclude<keyof Output, "bootstrap" | "update">]: Output[K];
59
- } & {
60
- readonly bootstrap: ReadonlyArray<RebindFeatureSchedule<FeatureScheduleArray<Output, "bootstrap">[number], S, Root>>;
61
- readonly update: ReadonlyArray<RebindFeatureSchedule<FeatureScheduleArray<Output, "update">[number], S, Root>>;
62
- };
63
- type FeatureOutputRecord<Features extends ReadonlyArray<AnyFeature>, S extends Schema.Any, Root> = {
64
- readonly [K in FeatureNames<Features>]: NormalizeFeatureOutput<Extract<Features[number], {
65
- readonly name: K;
66
- }>["output"], S, Root>;
67
- };
68
- /**
69
- * The result of composing features: the merged schema, the bound `Game`,
70
- * each feature's normalized output, and the aggregated schedules in feature
71
- * order.
72
- */
73
- export interface ComposedFeatureProject<Features extends ReadonlyArray<AnyFeature>, Root = unknown, S extends Schema.Any = MergeFeatureSchemas<Features>> {
74
- readonly schema: S;
75
- readonly Game: Schema.Game<S, Root>;
76
- readonly features: FeatureOutputRecord<Features, S, Root>;
77
- readonly schedules: {
78
- readonly bootstrap: ReadonlyArray<FeatureOutputRecord<Features, S, Root>[FeatureNames<Features>]["bootstrap"][number]>;
79
- readonly update: ReadonlyArray<FeatureOutputRecord<Features, S, Root>[FeatureNames<Features>]["update"][number]>;
80
- };
81
- }
82
- /**
83
- * Defines one pre-bind feature.
84
- */
85
- export declare const define: <const Name extends string, FeatureSchema extends Schema.Any, const Requires extends ReadonlyArray<AnyFeature> = [], Output extends FeatureBuildOutput = FeatureBuildOutput>(name: Name, options: {
86
- readonly schema: FeatureSchema;
87
- readonly requires?: Requires;
88
- readonly build: FeatureBuildFunction<MergeSchemaDefinitions<FeatureSchema, MergeFeatureSchemas<Requires>>, Output>;
89
- }) => FeatureDefinition<Name, FeatureSchema, Requires, Output>;
90
- /**
91
- * Composes features into one merged schema and one bound `Game`, then builds
92
- * every feature against it.
93
- *
94
- * Aggregated schedule order is the selected feature order, not dependency
95
- * order. Run them with `runtime.tick(...project.schedules.update)`.
96
- */
97
- export declare const compose: <Root, const Features extends readonly [AnyFeature, ...Array<AnyFeature>]>(options: {
98
- readonly root: Root;
99
- readonly features: Features;
100
- } & ValidateFeatureSelection<Features>) => ComposedFeatureProject<Features, Root>;
101
- export declare const Feature: {
102
- define: typeof define;
103
- compose: typeof compose;
104
- };
105
- export {};
106
- //# sourceMappingURL=Feature.d.ts.map