@typeonce/ritmo 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +3 -2
  2. package/dist/Clock.d.ts +19 -4
  3. package/dist/Clock.d.ts.map +1 -1
  4. package/dist/Clock.js +18 -2
  5. package/dist/Clock.js.map +1 -1
  6. package/dist/Command.d.ts +101 -45
  7. package/dist/Command.d.ts.map +1 -1
  8. package/dist/Command.js +22 -4
  9. package/dist/Command.js.map +1 -1
  10. package/dist/Debug.d.ts +66 -8
  11. package/dist/Debug.d.ts.map +1 -1
  12. package/dist/Descriptor.d.ts +27 -9
  13. package/dist/Descriptor.d.ts.map +1 -1
  14. package/dist/Descriptor.js +13 -9
  15. package/dist/Descriptor.js.map +1 -1
  16. package/dist/Entity.d.ts +15 -8
  17. package/dist/Entity.d.ts.map +1 -1
  18. package/dist/Entity.js.map +1 -1
  19. package/dist/FixedLoop.d.ts +106 -0
  20. package/dist/FixedLoop.d.ts.map +1 -0
  21. package/dist/FixedLoop.js +97 -0
  22. package/dist/FixedLoop.js.map +1 -0
  23. package/dist/HostClock.d.ts +1 -1
  24. package/dist/HostClock.d.ts.map +1 -1
  25. package/dist/HostClock.js +2 -1
  26. package/dist/HostClock.js.map +1 -1
  27. package/dist/Machine.d.ts +4 -1
  28. package/dist/Machine.d.ts.map +1 -1
  29. package/dist/Machine.js +4 -1
  30. package/dist/Machine.js.map +1 -1
  31. package/dist/Query.d.ts +52 -10
  32. package/dist/Query.d.ts.map +1 -1
  33. package/dist/Query.js +2 -1
  34. package/dist/Query.js.map +1 -1
  35. package/dist/Relation.d.ts +5 -14
  36. package/dist/Relation.d.ts.map +1 -1
  37. package/dist/Relation.js +0 -32
  38. package/dist/Relation.js.map +1 -1
  39. package/dist/Runtime.d.ts +58 -15
  40. package/dist/Runtime.d.ts.map +1 -1
  41. package/dist/Runtime.js +288 -70
  42. package/dist/Runtime.js.map +1 -1
  43. package/dist/Schedule.d.ts +24 -15
  44. package/dist/Schedule.d.ts.map +1 -1
  45. package/dist/Schedule.js +2 -1
  46. package/dist/Schedule.js.map +1 -1
  47. package/dist/Schema.d.ts +98 -29
  48. package/dist/Schema.d.ts.map +1 -1
  49. package/dist/Schema.js +4 -4
  50. package/dist/Schema.js.map +1 -1
  51. package/dist/Snapshot.d.ts +76 -1
  52. package/dist/Snapshot.d.ts.map +1 -1
  53. package/dist/Snapshot.js +42 -1
  54. package/dist/Snapshot.js.map +1 -1
  55. package/dist/System.d.ts +68 -24
  56. package/dist/System.d.ts.map +1 -1
  57. package/dist/System.js.map +1 -1
  58. package/dist/index.d.ts +4 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +4 -0
  61. package/dist/index.js.map +1 -1
  62. package/dist/internal/cells.d.ts +2 -0
  63. package/dist/internal/cells.d.ts.map +1 -1
  64. package/dist/internal/cells.js +34 -8
  65. package/dist/internal/cells.js.map +1 -1
  66. package/dist/internal/debug.d.ts +3 -0
  67. package/dist/internal/debug.d.ts.map +1 -1
  68. package/dist/internal/debug.js +83 -15
  69. package/dist/internal/debug.js.map +1 -1
  70. package/dist/internal/fragments.d.ts +2 -6
  71. package/dist/internal/fragments.d.ts.map +1 -1
  72. package/dist/internal/fragments.js +25 -2
  73. package/dist/internal/fragments.js.map +1 -1
  74. package/dist/internal/game.d.ts +2 -0
  75. package/dist/internal/game.d.ts.map +1 -1
  76. package/dist/internal/game.js +16 -3
  77. package/dist/internal/game.js.map +1 -1
  78. package/dist/internal/queries.d.ts.map +1 -1
  79. package/dist/internal/queries.js +27 -18
  80. package/dist/internal/queries.js.map +1 -1
  81. package/dist/internal/relations.d.ts +10 -0
  82. package/dist/internal/relations.d.ts.map +1 -0
  83. package/dist/internal/relations.js +32 -0
  84. package/dist/internal/relations.js.map +1 -0
  85. package/dist/internal/streams.d.ts +2 -0
  86. package/dist/internal/streams.d.ts.map +1 -1
  87. package/dist/internal/streams.js +10 -0
  88. package/dist/internal/streams.js.map +1 -1
  89. package/dist/internal/world.d.ts +7 -1
  90. package/dist/internal/world.d.ts.map +1 -1
  91. package/dist/internal/world.js +53 -3
  92. package/dist/internal/world.js.map +1 -1
  93. package/package.json +1 -1
  94. package/src/Clock.ts +29 -4
  95. package/src/Command.ts +161 -67
  96. package/src/Debug.ts +61 -9
  97. package/src/Descriptor.ts +43 -17
  98. package/src/Entity.ts +16 -9
  99. package/src/FixedLoop.ts +218 -0
  100. package/src/HostClock.ts +2 -1
  101. package/src/Machine.ts +4 -1
  102. package/src/Query.ts +47 -10
  103. package/src/Relation.ts +5 -67
  104. package/src/Runtime.ts +345 -85
  105. package/src/Schedule.ts +31 -19
  106. package/src/Schema.ts +118 -35
  107. package/src/Snapshot.ts +90 -2
  108. package/src/System.ts +76 -23
  109. package/src/index.ts +4 -0
  110. package/src/internal/cells.ts +42 -10
  111. package/src/internal/debug.ts +84 -15
  112. package/src/internal/fragments.ts +23 -3
  113. package/src/internal/game.ts +19 -3
  114. package/src/internal/queries.ts +28 -2
  115. package/src/internal/relations.ts +52 -0
  116. package/src/internal/streams.ts +11 -0
  117. package/src/internal/world.ts +60 -4
package/src/Command.ts CHANGED
@@ -65,6 +65,7 @@ import * as Entity from "./Entity.ts"
65
65
  import type * as EntityScope from "./EntityScope.ts"
66
66
  import type * as Relation from "./Relation.ts"
67
67
  import * as Result from "./Result.ts"
68
+ import type { ReadonlyValue } from "./Query.ts"
68
69
  import type { RequirementPair, Schema } from "./Schema.ts"
69
70
 
70
71
  /**
@@ -102,7 +103,7 @@ export namespace Draft {
102
103
  /**
103
104
  * A typed descriptor/value pair used by the flat command authoring APIs.
104
105
  */
105
- export type Entry<D extends Descriptor<"component", string, any>> = readonly [D, Descriptor.Value<D>]
106
+ export type Entry<D extends Descriptor<"component", string, any>> = readonly [D, ReadonlyValue<Descriptor.Value<D>>]
106
107
 
107
108
  /**
108
109
  * Extracts the component-descriptor union from a schema.
@@ -142,15 +143,21 @@ type EntryValue<T> =
142
143
  export type FoldEntries<
143
144
  Entries extends ReadonlyArray<SpawnEntry<any>>,
144
145
  P extends Entity.ComponentProof = {}
145
- > = number extends Entries["length"]
146
- // A widened array (a spread of entries built at runtime): each entry may or may not be there.
147
- ? P & MaybeEntries<EntryValue<Entries[number]>>
146
+ > = Entries extends readonly [
147
+ infer Head extends SpawnEntry<any>,
148
+ ...infer Tail extends ReadonlyArray<SpawnEntry<any>>
149
+ ]
150
+ ? FoldEntries<Tail, Draft.InsertEntry<P, EntryValue<Head>>>
151
+ // Fixed entries after a spread are there too.
148
152
  : Entries extends readonly [
149
- infer Head extends SpawnEntry<any>,
150
- ...infer Tail extends Array<SpawnEntry<any>>
153
+ ...infer Init extends ReadonlyArray<SpawnEntry<any>>,
154
+ infer Last extends SpawnEntry<any>
151
155
  ]
152
- ? FoldEntries<Tail, Draft.InsertEntry<P, EntryValue<Head>>>
153
- : P
156
+ ? FoldEntries<Init, Draft.InsertEntry<P, EntryValue<Last>>>
157
+ : number extends Entries["length"]
158
+ // A widened array (a spread of entries built at runtime): each entry may or may not be there.
159
+ ? P & MaybeEntries<EntryValue<Entries[number]>>
160
+ : P
154
161
 
155
162
  /** The proof of entries that may each be present: every component they can hold, optional. */
156
163
  type MaybeEntries<Entry> = {
@@ -175,10 +182,11 @@ export type DraftFor<
175
182
  S extends Schema.Any,
176
183
  Entries extends ReadonlyArray<SpawnEntry<S>>,
177
184
  P extends Entity.ComponentProof,
178
- Root
185
+ Root,
186
+ Relates = never
179
187
  > = [Extract<Entries[number], { readonly ok: boolean }>] extends [never]
180
- ? Entity.EntityDraft<S, FoldEntries<Entries, P>, Root>
181
- : Result.Result<Entity.EntityDraft<S, FoldEntries<Entries, P>, Root>, EntryErrors<Entries>>
188
+ ? Entity.EntityDraft<S, FoldEntries<Entries, P>, Root, Relates>
189
+ : Result.Result<Entity.EntityDraft<S, FoldEntries<Entries, P>, Root, Relates>, EntryErrors<Entries>>
182
190
 
183
191
  /**
184
192
  * A deferred world mutation.
@@ -235,6 +243,12 @@ export interface InternalWorld<S extends Schema.Any> {
235
243
  * written when an inserted component would miss one it requires.
236
244
  */
237
245
  readonly insertComponents: (id: Entity.EntityId<S, any>, entries: ReadonlyArray<Entity.StagedComponent>) => void
246
+ /**
247
+ * Reports a `despawn` or `remove` given a numeric id this run did not
248
+ * spawn: an id kept from an earlier run, or a raw id that erased types let
249
+ * through. The command is skipped.
250
+ */
251
+ readonly reportStaleTarget: (command: "despawn" | "remove", id: Entity.EntityId<S, any>) => void
238
252
  /**
239
253
  * Attempts to attach one relation edge between two live entities.
240
254
  */
@@ -270,12 +284,13 @@ const unspawnedId = Entity.makeEntityId<any, any>(-1)
270
284
  */
271
285
  export const entry = <D extends Descriptor<"component", string, any>>(
272
286
  descriptor: D,
273
- value: Descriptor.Value<D>
287
+ value: ReadonlyValue<Descriptor.Value<D>>
274
288
  ): Entry<D> => [descriptor, value]
275
289
 
276
290
  /**
277
291
  * Validates raw input through a constructed descriptor's own validator and
278
- * returns an entry result for `spawn(...)` / `insert(...)`.
292
+ * returns an entry result for `Game.Command.spawn(...)` / `extend(...)`.
293
+ * `commands.insert` takes valid entries only: check the result first.
279
294
  *
280
295
  * This is the one way to put unvalidated data on an entity: the descriptor
281
296
  * owns its validator (`Descriptor.ConstructedComponent(...)`, built with
@@ -316,12 +331,13 @@ export const extend = <
316
331
  S extends Schema.Any,
317
332
  P extends Entity.ComponentProof,
318
333
  Root,
319
- const Entries extends ReadonlyArray<SpawnEntry<S>>
334
+ const Entries extends ReadonlyArray<SpawnEntry<S>>,
335
+ Relates = never
320
336
  >(
321
- draft: Entity.EntityDraft<S, P, Root>,
337
+ draft: Entity.EntityDraft<S, P, Root, Relates>,
322
338
  ...entries: Entries
323
- ): DraftFor<S, Entries, P, Root> =>
324
- build(draft.id, { ...draft.proof }, [...draft.components], draft.relations, entries) as DraftFor<S, Entries, P, Root>
339
+ ): DraftFor<S, Entries, P, Root, Relates> =>
340
+ build(draft.id, { ...draft.proof }, [...draft.components], draft.relations, entries) as DraftFor<S, Entries, P, Root, Relates>
325
341
 
326
342
  /**
327
343
  * Shared draft builder. Plain entries take a fast path; the first `Result`
@@ -367,7 +383,7 @@ const build = (
367
383
  *
368
384
  * Drafts are plain values: build them anywhere (including small factories),
369
385
  * then queue them with `commands.spawn(...)`. Entries may be `[Descriptor,
370
- * value]` pairs or results from `entryRaw(...)`; see `insert(...)` for how
386
+ * value]` pairs or results from `entryRaw(...)`; see `extend(...)` for how
371
387
  * failures are reported.
372
388
  *
373
389
  * @example
@@ -399,12 +415,13 @@ export const relate = <
399
415
  S extends Schema.Any,
400
416
  P extends Entity.ComponentProof,
401
417
  R extends Relation.Relation.Any,
402
- Root = unknown
418
+ Root = unknown,
419
+ Relates = never
403
420
  >(
404
- draft: Entity.EntityDraft<S, P, Root>,
421
+ draft: Entity.EntityDraft<S, P, Root, Relates>,
405
422
  relation: R,
406
423
  target: Entity.EntityId<S, Root>
407
- ): Entity.EntityDraft<S, P, Root> =>
424
+ ): Entity.EntityDraft<S, P, Root, Relates | R> =>
408
425
  Entity.draft(draft.id, draft.proof, draft.components, [
409
426
  ...draft.relations,
410
427
  {
@@ -427,7 +444,7 @@ type ComponentDescriptor = Descriptor<"component", string, any>
427
444
  * Allows `spawn` / `spawnIn` of drafts whose components are all among `components`.
428
445
  * Use it for entities the system creates; use `extend` on a draft to build one.
429
446
  */
430
- export const spawns = <const Components extends ReadonlyArray<ComponentDescriptor>>(...components: Components): Permission<"spawns", Components[number]> =>
447
+ export const spawns = <const Components extends readonly [ComponentDescriptor, ...ReadonlyArray<ComponentDescriptor>]>(...components: Components): Permission<"spawns", Components[number]> =>
431
448
  ({ permission: "spawns", values: components })
432
449
 
433
450
  /**
@@ -435,11 +452,11 @@ export const spawns = <const Components extends ReadonlyArray<ComponentDescripto
435
452
  * replace at the next `applyDeferred()`. To change a value now, on an entity
436
453
  * you matched, write through a query (`Game.Query.write`) instead.
437
454
  */
438
- export const inserts = <const Components extends ReadonlyArray<ComponentDescriptor>>(...components: Components): Permission<"inserts", Components[number]> =>
455
+ export const inserts = <const Components extends readonly [ComponentDescriptor, ...ReadonlyArray<ComponentDescriptor>]>(...components: Components): Permission<"inserts", Components[number]> =>
439
456
  ({ permission: "inserts", values: components })
440
457
 
441
458
  /** Allows `remove(entity, X)` with `X` among `components`. */
442
- export const removes = <const Components extends ReadonlyArray<ComponentDescriptor>>(...components: Components): Permission<"removes", Components[number]> =>
459
+ export const removes = <const Components extends readonly [ComponentDescriptor, ...ReadonlyArray<ComponentDescriptor>]>(...components: Components): Permission<"removes", Components[number]> =>
443
460
  ({ permission: "removes", values: components })
444
461
 
445
462
  /**
@@ -447,7 +464,7 @@ export const removes = <const Components extends ReadonlyArray<ComponentDescript
447
464
  * of `components` (`match.entity`), never a raw id: the system can only
448
465
  * despawn what it has proven is, for example, an `Enemy`.
449
466
  */
450
- export const despawns = <const Components extends ReadonlyArray<ComponentDescriptor>>(...components: Components): Permission<"despawns", Components[number]> =>
467
+ export const despawns = <const Components extends readonly [ComponentDescriptor, ...ReadonlyArray<ComponentDescriptor>]>(...components: Components): Permission<"despawns", Components[number]> =>
451
468
  ({ permission: "despawns", values: components })
452
469
 
453
470
  /** Allows `despawnScope(scope)` for this scope. */
@@ -455,7 +472,7 @@ export const despawnsScope = <const Scope extends EntityScope.EntityScope.Any>(s
455
472
  ({ permission: "despawnsScope", values: [scope] })
456
473
 
457
474
  /** Allows `relate`, `unrelate`, and `reorderChildren` for these relations. */
458
- export const relates = <const Relations extends ReadonlyArray<Relation.Relation.Any>>(...relations: Relations): Permission<"relates", Relations[number]> =>
475
+ export const relates = <const Relations extends readonly [Relation.Relation.Any, ...ReadonlyArray<Relation.Relation.Any>]>(...relations: Relations): Permission<"relates", Relations[number]> =>
459
476
  ({ permission: "relates", values: relations })
460
477
 
461
478
  type PermissionValue<Permissions, Kind extends PermissionKind> =
@@ -510,6 +527,12 @@ type Names<D> = D extends { readonly name: infer Name extends string } ? Name :
510
527
  /** Every key of every member of a union of proofs (a draft with a conditional entry is a union). */
511
528
  type AllKeys<P> = P extends unknown ? keyof P & string : never
512
529
 
530
+ /** Relations staged on a draft need `relates`, like `commands.relate`. */
531
+ type DraftRelationsAllowed<Relates, Declared> =
532
+ [Exclude<Relates, Declared>] extends [never]
533
+ ? unknown
534
+ : Fix<`${Names<Exclude<Relates, Declared>>} is related by this draft but not declared in this system's commands: add it to Game.System.relates(...)`>
535
+
513
536
  type DraftAllowed<P, Spawns> =
514
537
  [Exclude<AllKeys<P>, Names<Spawns>>] extends [never]
515
538
  ? unknown
@@ -523,16 +546,24 @@ type EntriesAllowed<Entries extends ReadonlyArray<readonly [ComponentDescriptor,
523
546
  type RemoveAllowed<D, Removes> =
524
547
  [D] extends [Removes] ? unknown : Fix<`${Names<D>} is not declared in this system's commands: add it to Game.System.removes(...)`>
525
548
 
526
- type DespawnAllowed<E, Despawns> =
527
- E extends { readonly kind: "EntityRef" | "EntityMut"; readonly __proves?: (_: never) => infer Proves }
549
+ /**
550
+ * Why each possible target of a despawn is not allowed (`never` when it is).
551
+ * Computed per member, so a target chosen from several queries
552
+ * (`cond ? a.entity : b.entity`) must pass for every one of them.
553
+ */
554
+ type DespawnRejections<E, Despawns> =
555
+ E extends { readonly kind: "EntityRef" | "EntityMut"; readonly __proves?: (_: infer Proves) => void }
528
556
  ? [Extract<Proves, Despawns>] extends [never]
529
- ? Fix<`despawn needs an entity from a query proving ${Names<Despawns>} (declared in despawns)`>
530
- : unknown
557
+ ? `despawn needs an entity from a query proving ${Names<Despawns>} (declared in despawns)`
558
+ : never
531
559
  : E extends { readonly [spawnedTypeId]: (_: never) => infer Spawned }
532
560
  ? [Extract<Spawned, Names<Despawns>>] extends [never]
533
- ? Fix<`despawn needs an entity spawned with ${Names<Despawns>} (declared in despawns)`>
534
- : unknown
535
- : Fix<`despawn needs an entity from a query proving ${Names<Despawns>} (declared in despawns), not a raw id: pass match.entity`>
561
+ ? `despawn needs an entity spawned with ${Names<Despawns>} (declared in despawns)`
562
+ : never
563
+ : `despawn needs an entity from a query proving ${Names<Despawns>} (declared in despawns), not a raw id: pass match.entity`
564
+
565
+ type DespawnAllowed<E, Despawns> =
566
+ [DespawnRejections<E, Despawns>] extends [never] ? unknown : Fix<DespawnRejections<E, Despawns>>
536
567
 
537
568
  declare const spawnedTypeId: unique symbol
538
569
 
@@ -540,53 +571,95 @@ declare const spawnedTypeId: unique symbol
540
571
  * The id `spawn` / `spawnIn` return: an `EntityId` that also records the
541
572
  * components its draft holds in every case, so the system that queued it can
542
573
  * `despawn` it before the next `applyDeferred()` (no query can match it yet).
574
+ * It is valid only in the run that spawned it: kept in a closure or a
575
+ * resource and passed to `despawn` / `remove` in a later run, the command is
576
+ * skipped and reported (the `stale-target` lint). Keep the entity id or a
577
+ * handle and resolve it again through a query instead.
543
578
  * Stored as a plain `EntityId`, it is a raw id again.
544
579
  */
545
580
  export type SpawnedId<S extends Schema.Any, Root, Components extends string> =
546
581
  Entity.EntityId<S, Root> & { readonly [spawnedTypeId]: (_: never) => Components }
547
582
 
548
- /** The requirements (`[requirer, required]`) of the components in `Held` that `Held` lacks. */
549
- type MissingPairs<S extends Schema.Any, Held extends string> =
550
- Extract<Schema.Requires<S>, readonly [Held, string]> extends infer Pair
583
+ /** One message per missing requirement: `"Player requires Velocity"`. */
584
+ type MissingMessages<Pairs> = Pairs extends readonly [infer From extends string, infer To extends string] ? `${From} requires ${To}` : never
585
+
586
+ /** The requirements (`[requirer, required]`) of the components in `From` that `Held` lacks. */
587
+ type MissingPairsFrom<S extends Schema.Any, From extends string, Held extends string> =
588
+ Extract<Schema.Requires<S>, readonly [From, string]> extends infer Pair
551
589
  ? Pair extends readonly [string, infer To extends string]
552
590
  ? To extends Held ? never : Pair
553
591
  : never
554
592
  : never
555
593
 
556
- /** One message per missing requirement: `"Player requires Velocity"`. */
557
- type MissingMessages<Pairs> = Pairs extends readonly [infer From extends string, infer To extends string] ? `${From} requires ${To}` : never
558
-
559
594
  /**
560
- * Accepts a draft holding every requirement of every component it holds.
561
- * Direct requirements are enough: each required component must be held
562
- * too, with its own requirements.
595
+ * The requirements a draft may break. A draft's proof is a union of
596
+ * alternatives when an entry is conditional, and has optional keys for
597
+ * spread entries: in each alternative, every component it may hold needs
598
+ * every component it requires surely held. Direct requirements are enough:
599
+ * each required component must be held too, with its own requirements.
563
600
  */
564
- type DraftRequirementsMet<S extends Schema.Any, Held extends string> =
565
- [MissingPairs<S, Held>] extends [never]
601
+ type DraftViolations<S extends Schema.Any, P> =
602
+ P extends unknown ? MissingPairsFrom<S, keyof P & string, RequiredKeys<P>> : never
603
+
604
+ type DraftRequirementsMet<S extends Schema.Any, P> =
605
+ [DraftViolations<S, P>] extends [never]
566
606
  ? unknown
567
- : Fix<`${MissingMessages<MissingPairs<S, Held>>}: add the missing entries to the draft`>
607
+ : Fix<`${MissingMessages<DraftViolations<S, P>>}: add the missing entries to the draft`>
568
608
 
569
- /** What an insert or remove target is known to hold: a query match's proof, or a `SpawnedId`'s draft. A raw id proves nothing. */
570
- type TargetHolds<E> =
571
- E extends { readonly kind: "EntityRef" | "EntityMut"; readonly __proves?: (_: never) => infer Proves }
609
+ /** What one possible target holds: a query match's proof, or a `SpawnedId`'s draft. A raw id proves nothing. */
610
+ type HeldBy<E> =
611
+ E extends { readonly kind: "EntityRef" | "EntityMut"; readonly __proves?: (_: infer Proves) => void }
572
612
  ? Names<Proves>
573
613
  : E extends { readonly [spawnedTypeId]: (_: never) => infer Spawned extends string }
574
614
  ? Spawned
575
615
  : never
576
616
 
577
- /** What a remove target is known to lack: its query's `without`. */
578
- type TargetLacks<E> =
579
- E extends { readonly kind: "EntityRef" | "EntityMut"; readonly __lacks?: (_: never) => infer Lacks }
617
+ /** What one possible target's query proves absent: its `without`. */
618
+ type LackedBy<E> =
619
+ E extends { readonly kind: "EntityRef" | "EntityMut"; readonly __lacks?: (_: infer Lacks) => void }
580
620
  ? Names<Lacks>
581
621
  : never
582
622
 
623
+ /** The names in `All` that some member of `E` does not hold (or lack). */
624
+ type MissingFromHeld<E, All> = E extends unknown ? Exclude<All, HeldBy<E>> : never
625
+ type MissingFromLacked<E, All> = E extends unknown ? Exclude<All, LackedBy<E>> : never
626
+
627
+ /**
628
+ * What an insert or remove target is known to hold. For a target chosen
629
+ * from several queries (`cond ? a.entity : b.entity`), only what every one
630
+ * of them proves.
631
+ */
632
+ type TargetHolds<E> = Exclude<HeldBy<E>, MissingFromHeld<E, HeldBy<E>>>
633
+
634
+ /** What a remove target is known to lack: its query's `without` (for several queries, what all of them exclude). */
635
+ type TargetLacks<E> = Exclude<LackedBy<E>, MissingFromLacked<E, LackedBy<E>>>
636
+
583
637
  type EntryNames<Entries extends ReadonlyArray<readonly [ComponentDescriptor, unknown]>> = Names<Entries[number][0]>
584
638
 
585
- /** Inserted components with requirements neither inserted alongside nor proven held by the target. */
639
+ /** The names one entry surely holds: none when it is conditional between descriptors. */
640
+ type SureEntryNames<Entry, All extends string = EntryNamesOf<Entry>> =
641
+ Exclude<All, Entry extends unknown ? Exclude<All, EntryNamesOf<Entry>> : never>
642
+ type EntryNamesOf<Entry> = Entry extends readonly [infer D, unknown] ? Names<D> : never
643
+
644
+ /** The names surely inserted: fixed entries, not spread or conditional ones. */
645
+ type SurelyInserted<Entries> =
646
+ Entries extends readonly [infer Head, ...infer Tail]
647
+ ? SureEntryNames<Head> | SurelyInserted<Tail>
648
+ : Entries extends readonly [...infer Init, infer Last]
649
+ ? SureEntryNames<Last> | SurelyInserted<Init>
650
+ : never
651
+
652
+ /**
653
+ * Inserted components (any entry may be one) with requirements neither
654
+ * surely inserted alongside nor proven held by the target.
655
+ */
656
+ type InsertViolations<S extends Schema.Any, E, Entries extends ReadonlyArray<readonly [ComponentDescriptor, unknown]>> =
657
+ MissingPairsFrom<S, EntryNames<Entries>, SurelyInserted<Entries> | TargetHolds<E>>
658
+
586
659
  type InsertRequirementsMet<S extends Schema.Any, E, Entries extends ReadonlyArray<readonly [ComponentDescriptor, unknown]>> =
587
- [MissingPairs<S, EntryNames<Entries> | TargetHolds<E>> & readonly [EntryNames<Entries>, string]] extends [never]
660
+ [InsertViolations<S, E, Entries>] extends [never]
588
661
  ? unknown
589
- : Fix<`${MissingMessages<MissingPairs<S, EntryNames<Entries> | TargetHolds<E>> & readonly [EntryNames<Entries>, string]>}: insert it in the same call, or pass match.entity from a query that proves it`>
662
+ : Fix<`${MissingMessages<InsertViolations<S, E, Entries>>}: insert it in the same call, or pass match.entity from a query that proves it`>
590
663
 
591
664
  /**
592
665
  * Removing `Removed` is sound when every component that requires one of
@@ -606,13 +679,16 @@ type RemoveViolations<S extends Schema.Any, E, Removed extends string> =
606
679
  type RemoveRequirementsMet<S extends Schema.Any, E, Removed extends string> =
607
680
  [RemoveViolations<S, E, Removed>] extends [never] ? unknown : Fix<RemoveViolations<S, E, Removed>>
608
681
 
609
- /** A remove target: `match.entity`, or the id `spawn` returned in this run; never a raw id. */
610
- type RemoveTargetAllowed<S extends Schema.Any, Root, E> =
682
+ /** A remove target: `match.entity`, or the id `spawn` returned in this run; never a raw id, not even as one of several. */
683
+ type RemoveTargetRejections<S extends Schema.Any, Root, E> =
611
684
  E extends { readonly kind: "EntityRef" | "EntityMut"; readonly id: Entity.EntityId<S, Root> }
612
- ? unknown
685
+ ? never
613
686
  : E extends SpawnedId<S, Root, string>
614
- ? unknown
615
- : Fix<"remove needs an entity from a query (pass match.entity) or the id spawn returned in this run, not a raw id">
687
+ ? never
688
+ : "remove needs an entity from a query (pass match.entity) or the id spawn returned in this run, not a raw id"
689
+
690
+ type RemoveTargetAllowed<S extends Schema.Any, Root, E> =
691
+ [RemoveTargetRejections<S, Root, E>] extends [never] ? unknown : Fix<RemoveTargetRejections<S, Root, E>>
616
692
 
617
693
  /** The transitive closure of the names `N` require (cycles stop). */
618
694
  type RequiredClosure<Pairs extends RequirementPair, N extends string, Seen extends string = never> =
@@ -657,7 +733,8 @@ type HeldNames<P> = Exclude<AllKeys<P>, MissingSomewhere<P, AllKeys<P>>>
657
733
  * `spawn` needs `Game.System.spawns(...)` listing every component of the
658
734
  * draft, `insert` needs `inserts`, `remove` needs `removes`, `despawn` needs
659
735
  * `despawns` and an entity from a query proving one of them, `despawnScope`
660
- * needs `despawnsScope`, and relation edits need `relates`.
736
+ * needs `despawnsScope`, and relation edits need `relates`, including
737
+ * relations a spawned draft stages with `Game.Command.relate(...)`.
661
738
  *
662
739
  * Resource and event writes are not commands: they go through the
663
740
  * `writeResource(...)` / `writeEvent(...)` access a system declares.
@@ -678,19 +755,23 @@ export interface CommandsApi<
678
755
  * The draft must hold every component its components require
679
756
  * (`Schema.requires`); drafts are checked here, so a partial draft built
680
757
  * with `Game.Command.spawn(...)` and completed with `extend(...)` is fine.
758
+ * A component the draft may hold (a conditional or spread entry) needs
759
+ * what it requires surely held.
681
760
  */
682
761
  readonly spawn: [Spawns] extends [never]
683
762
  ? MethodNotDeclared<"spawn", "spawns">
684
- : <P extends Entity.ComponentProof>(draft: Entity.EntityDraft<S, P, Root> & DraftAllowed<P, Spawns> & DraftRequirementsMet<S, HeldNames<P>>) => SpawnedId<S, Root, HeldNames<P>>
763
+ : <P extends Entity.ComponentProof, R = never>(
764
+ draft: Entity.EntityDraft<S, P, Root, R> & DraftAllowed<P, Spawns> & DraftRequirementsMet<S, P> & DraftRelationsAllowed<R, Relates>
765
+ ) => SpawnedId<S, Root, HeldNames<P>>
685
766
  /**
686
767
  * Queues an entity spawn owned by one lifetime scope (removed together
687
768
  * with `despawnScope(...)`). The draft is checked like `spawn(...)`.
688
769
  */
689
770
  readonly spawnIn: [Spawns] extends [never]
690
771
  ? MethodNotDeclared<"spawnIn", "spawns">
691
- : <P extends Entity.ComponentProof>(
772
+ : <P extends Entity.ComponentProof, R = never>(
692
773
  scope: EntityScope.EntityScope<string, Root>,
693
- draft: Entity.EntityDraft<S, P, Root> & DraftAllowed<P, Spawns> & DraftRequirementsMet<S, HeldNames<P>>
774
+ draft: Entity.EntityDraft<S, P, Root, R> & DraftAllowed<P, Spawns> & DraftRequirementsMet<S, P> & DraftRelationsAllowed<R, Relates>
694
775
  ) => SpawnedId<S, Root, HeldNames<P>>
695
776
  /**
696
777
  * Queues component inserts (or replacements) on an existing entity, applied
@@ -782,8 +863,8 @@ export interface CommandsApi<
782
863
 
783
864
  /** The command queue the runtime gives each system: every command, unchecked, and `flush`. */
784
865
  export interface CommandQueue<S extends Schema.Any, Root = unknown> {
785
- readonly spawn: (draft: Entity.EntityDraft<S, any, Root>) => Entity.EntityId<S, Root>
786
- readonly spawnIn: (scope: EntityScope.EntityScope<string, Root>, draft: Entity.EntityDraft<S, any, Root>) => Entity.EntityId<S, Root>
866
+ readonly spawn: (draft: Entity.EntityDraft<S, any, Root, any>) => Entity.EntityId<S, Root>
867
+ readonly spawnIn: (scope: EntityScope.EntityScope<string, Root>, draft: Entity.EntityDraft<S, any, Root, any>) => Entity.EntityId<S, Root>
787
868
  readonly insert: (entity: Entity.EntityId<S, Root> | { readonly id: Entity.EntityId<S, Root> }, ...entries: ReadonlyArray<SchemaEntry<S>>) => Entity.EntityId<S, Root>
788
869
  readonly despawn: (entity: Entity.EntityId<S, Root> | { readonly id: Entity.EntityId<S, Root> }) => void
789
870
  readonly despawnScope: (scope: EntityScope.EntityScope<string, Root>) => void
@@ -813,10 +894,17 @@ export const makeCommands = <S extends Schema.Any, Root = unknown>(
813
894
  * the system effect completes.
814
895
  */
815
896
  const queue: Array<DeferredCommand<S>> = []
897
+ /**
898
+ * Ids this run spawned: the only numeric targets `despawn` and `remove`
899
+ * accept. Their types reject raw ids, so any other number is an id kept
900
+ * from an earlier run (or erased types), and proves nothing now.
901
+ */
902
+ const spawnedThisRun = new Set<number>()
816
903
 
817
904
  return {
818
905
  spawn(draft) {
819
906
  const id = allocateId()
907
+ spawnedThisRun.add(id)
820
908
  queue.push({
821
909
  tag: "spawn",
822
910
  apply(world) {
@@ -830,6 +918,7 @@ export const makeCommands = <S extends Schema.Any, Root = unknown>(
830
918
  },
831
919
  spawnIn(scope, draft) {
832
920
  const id = allocateId()
921
+ spawnedThisRun.add(id)
833
922
  queue.push({
834
923
  tag: "spawnIn",
835
924
  apply(world) {
@@ -854,10 +943,12 @@ export const makeCommands = <S extends Schema.Any, Root = unknown>(
854
943
  },
855
944
  despawn(entity) {
856
945
  const id = typeof entity === "object" ? entity.id : entity
946
+ const stale = typeof entity !== "object" && !spawnedThisRun.has(id)
857
947
  queue.push({
858
948
  tag: "despawn",
859
949
  apply(world) {
860
- world.destroyEntity(id)
950
+ if (stale) world.reportStaleTarget("despawn", id)
951
+ else world.destroyEntity(id)
861
952
  }
862
953
  })
863
954
  },
@@ -871,10 +962,12 @@ export const makeCommands = <S extends Schema.Any, Root = unknown>(
871
962
  },
872
963
  remove(target, ...descriptors) {
873
964
  const entity = typeof target === "object" ? target.id : target
965
+ const stale = typeof target !== "object" && !spawnedThisRun.has(entity)
874
966
  queue.push({
875
967
  tag: "remove",
876
968
  apply(world) {
877
- world.removeComponents(entity, descriptors)
969
+ if (stale) world.reportStaleTarget("remove", entity)
970
+ else world.removeComponents(entity, descriptors)
878
971
  }
879
972
  })
880
973
  return entity
@@ -904,6 +997,7 @@ export const makeCommands = <S extends Schema.Any, Root = unknown>(
904
997
  })
905
998
  },
906
999
  flush() {
1000
+ spawnedThisRun.clear()
907
1001
  return queue.splice(0, queue.length)
908
1002
  }
909
1003
  }
package/src/Debug.ts CHANGED
@@ -35,7 +35,7 @@
35
35
  *
36
36
  * @example
37
37
  * ```ts
38
- * const runtime = Game.Runtime.make({ services, resources, debug: { hostClock: HostClock.standard } })
38
+ * const runtime = Game.Runtime.make({ services, resources, debug: { hostClock: HostClock.standard } }).value
39
39
  * runtime.debug.nameSchedules({ setup: setupSchedule, update: updateSchedule })
40
40
  *
41
41
  * const stop = runtime.debug.observe((event) => {
@@ -206,7 +206,11 @@ export interface SystemDescription {
206
206
  * names for despawnsScope, relation names for relates.
207
207
  */
208
208
  readonly commands: CommandPermissions
209
- /** Run conditions, rendered, for example `stateChanged(Game/Flow)`. */
209
+ /**
210
+ * Run conditions, rendered, for example `stateChanged(Game/Flow)`. A system
211
+ * placed under several `Schedule.when` / `pauseWhen` groups is described
212
+ * once, with the conditions of every placement.
213
+ */
210
214
  readonly when: ReadonlyArray<string>
211
215
  /** Pause conditions, rendered: while one fails the system waits and holds its stream entries. */
212
216
  readonly pauseWhen: ReadonlyArray<string>
@@ -243,7 +247,12 @@ export interface ScheduleDescription {
243
247
  * `relation-failure-never-read` fires for a relation that applied commands
244
248
  * have tried to relate or reorder (which can fail) while no described system
245
249
  * reads its failures, and `requirement-violated` lists commands skipped
246
- * because they would have broken a `Schema.requires` requirement, so
250
+ * because they would have broken a `Schema.requires` requirement,
251
+ * `next-state-replaced` lists next states one system queued and another
252
+ * replaced with a different value before the marker (the last wins),
253
+ * `stale-target` lists `despawn` / `remove` commands given a `SpawnedId`
254
+ * kept from an earlier run (skipped), `stream-loss-unread` lists stream
255
+ * readers that never check `lagged()` and lost entries (or are paused), so
247
256
  * describe after the game has run for a while. `scope-trigger-never-applied`
248
257
  * also reads which machines the runtime provides.
249
258
  */
@@ -260,6 +269,9 @@ export interface Lint {
260
269
  | "this-tick-order"
261
270
  | "command-permission-unused"
262
271
  | "requirement-violated"
272
+ | "next-state-replaced"
273
+ | "stale-target"
274
+ | "stream-loss-unread"
263
275
  | "scope-trigger-never-applied"
264
276
  readonly message: string
265
277
  readonly subject: string
@@ -430,6 +442,21 @@ export type Effect =
430
442
  readonly component: string
431
443
  readonly missing: string
432
444
  }
445
+ /** A `despawn` / `remove` skipped because it was given a `SpawnedId` from an earlier run. */
446
+ | { readonly kind: "staleTarget"; readonly entity: number; readonly command: StaleTarget["command"] }
447
+
448
+ /**
449
+ * A next state one system queued and another replaced with a different value
450
+ * before the same `applyStateTransitions(...)`: the `next-state-replaced`
451
+ * lint. Only the last value is committed.
452
+ */
453
+ export interface NextStateReplacement {
454
+ readonly machine: string
455
+ readonly replaced: { readonly system: string; readonly value: Machine.StateValue }
456
+ readonly by: { readonly system: string; readonly value: Machine.StateValue }
457
+ /** How many times it happened. */
458
+ readonly count: number
459
+ }
433
460
 
434
461
  /**
435
462
  * Commands of one system skipped because applying them would have left a
@@ -449,6 +476,37 @@ export interface RequirementViolation {
449
476
  readonly count: number
450
477
  }
451
478
 
479
+ /**
480
+ * Commands of one system skipped because they were given a `SpawnedId` kept
481
+ * from an earlier run, whose entity may since have been despawned and its id
482
+ * reused: the `stale-target` lint.
483
+ */
484
+ export interface StaleTarget {
485
+ readonly system: string
486
+ readonly command: "despawn" | "remove"
487
+ /** The last entity id it happened to. */
488
+ readonly entity: number
489
+ /** How many commands were skipped this way. */
490
+ readonly count: number
491
+ }
492
+
493
+ /**
494
+ * One system's reads of one stream (events, transition events, removed,
495
+ * despawned, relation failures) on a debug runtime: the
496
+ * `stream-loss-unread` lint.
497
+ */
498
+ export interface StreamLoss {
499
+ readonly system: string
500
+ /** The stream, as `events Hit`, `removed Sprite`, `despawned`, ... */
501
+ readonly stream: string
502
+ /** Runs that read it after entries were dropped before they were read. */
503
+ readonly lostRuns: number
504
+ /** Whether the system called `lagged()` on it. */
505
+ readonly checksLagged: boolean
506
+ /** Whether the system has `pauseWhen` conditions (a long pause can drop entries). */
507
+ readonly paused: boolean
508
+ }
509
+
452
510
  export interface AppliedCommand {
453
511
  readonly tag: string
454
512
  readonly system: string
@@ -629,9 +687,3 @@ export interface Option {
629
687
  export interface Member<S extends Schema.Any, Root> {
630
688
  readonly debug: Handle<S, Root>
631
689
  }
632
-
633
- /** Resolves the extra runtime member for a `debug` option type. */
634
- export type MemberFor<Enabled, S extends Schema.Any, Root> =
635
- [Enabled] extends [true] ? Member<S, Root>
636
- : [Enabled] extends [false | undefined] ? {}
637
- : Partial<Member<S, Root>>