@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/Schedule.ts CHANGED
@@ -147,10 +147,12 @@ export type CompositionOrder<Entries extends ReadonlyArray<unknown>> = ScheduleO
147
147
  // walks back from the reader to the nearest marker, which is usually close.
148
148
 
149
149
  /** An event or resource read. */
150
- interface DataRead<Kind, Key, Reader> {
150
+ interface DataRead<Kind, Key, Reader, ReaderSystem> {
151
151
  readonly kind: Kind
152
152
  readonly key: Key
153
153
  readonly reader: Reader
154
+ /** The reader itself: systems may share a display name, so writers are told apart by type. */
155
+ readonly readerSystem: ReaderSystem
154
156
  }
155
157
 
156
158
  /**
@@ -158,19 +160,24 @@ interface DataRead<Kind, Key, Reader> {
158
160
  * component it names (`Named`: selection slots, optional ones included,
159
161
  * `with`, `without`, and filters).
160
162
  */
161
- interface QueryRead<Slot, Required, Named, Reader> {
163
+ interface QueryRead<Slot, Required, Named, Reader, ReaderSystem> {
162
164
  readonly kind: "query"
163
165
  readonly slot: Slot
164
166
  readonly required: Required
165
167
  readonly named: Named
166
168
  readonly reader: Reader
169
+ readonly readerSystem: ReaderSystem
167
170
  }
168
171
 
169
172
  type DescriptorKind<D> = D extends { readonly kind: infer Kind } ? Kind : never
170
173
 
171
174
  type Names<D> = D extends { readonly name: infer Name extends string } ? Name : never
172
175
 
173
- type DescriptorsOf<Access> = Access extends { readonly descriptor: infer D } ? D : never
176
+ type DescriptorsOf<Access> =
177
+ Access extends { readonly descriptor: infer D } ? D
178
+ // A Query.anyOf filter names the components of its member filters.
179
+ : Access extends { readonly kind: "anyOf"; readonly filters: ReadonlyArray<infer Member> } ? DescriptorsOf<Member>
180
+ : never
174
181
 
175
182
  /** Every system an entry runs, nested schedules and transition bundles included, as a union. */
176
183
  type SystemsIn<Entry> =
@@ -179,13 +186,14 @@ type SystemsIn<Entry> =
179
186
  ? [Order] extends [never] ? never : SystemsIn<Order["entries"][number]>
180
187
  : never
181
188
 
182
- type QueryReadOf<Slot, Q, Reader> =
189
+ type QueryReadOf<Slot, Q, Reader, ReaderSystem> =
183
190
  Q extends { readonly selection: infer Selection; readonly with: infer With extends ReadonlyArray<unknown>; readonly without: infer Without extends ReadonlyArray<unknown>; readonly filters: infer Filters extends ReadonlyArray<unknown> }
184
191
  ? QueryRead<
185
192
  Slot,
186
193
  Names<{ readonly [K in keyof Selection]: Selection[K] extends { readonly mode: "read" | "write"; readonly descriptor: infer D } ? D : never }[keyof Selection] | With[number]>,
187
194
  Names<DescriptorsOf<Selection[keyof Selection]> | With[number] | Without[number] | DescriptorsOf<Filters[number]>>,
188
- Reader
195
+ Reader,
196
+ ReaderSystem
189
197
  >
190
198
  : never
191
199
 
@@ -195,19 +203,22 @@ type SystemReads<Sys> =
195
203
  :
196
204
  | (Extract<SpecAccessValues<Sys["spec"]>, ThisTick> extends infer Access
197
205
  ? Access extends { readonly descriptor: infer D extends { readonly name: string } }
198
- ? DataRead<DescriptorKind<D>, D["name"], Sys["name"]>
206
+ ? DataRead<DescriptorKind<D>, D["name"], Sys["name"], Sys>
199
207
  : never
200
208
  : never)
201
209
  | (Sys["spec"] extends { readonly queries: infer Queries }
202
- ? { readonly [K in keyof Queries]: Queries[K] extends ThisTick ? QueryReadOf<K, Queries[K], Sys["name"]> : never }[keyof Queries]
210
+ ? { readonly [K in keyof Queries]: Queries[K] extends ThisTick ? QueryReadOf<K, Queries[K], Sys["name"], Sys> : never }[keyof Queries]
203
211
  : never)
204
212
  : never
205
213
 
206
- /** Names of the systems among `Systems` that write `Key` (other than `Reader`). */
207
- type WritersOf<Systems, Kind, Key, Reader> =
214
+ /** Whether two system types are the same system (mutually assignable): a shared display name is not enough. */
215
+ type SameSystem<A, B> = [A] extends [B] ? [B] extends [A] ? true : false : false
216
+
217
+ /** Names of the systems among `Systems` that write `Key` (other than the reader itself). */
218
+ type WritersOf<Systems, Kind, Key, ReaderSystem> =
208
219
  Systems extends AnySystem
209
220
  ? IsErased<Systems["spec"]> extends true ? never
210
- : Systems["name"] extends Reader ? never
221
+ : SameSystem<Systems, ReaderSystem> extends true ? never
211
222
  : [Extract<SpecAccessValues<Systems["spec"]>, { readonly mode: "write"; readonly descriptor: { readonly kind: Kind; readonly name: Key } }>] extends [never]
212
223
  ? never
213
224
  : Systems["name"]
@@ -233,18 +244,18 @@ type CommandNames<Commands> =
233
244
  : readonly [never, never]
234
245
 
235
246
  /** Names of the systems among `Systems` whose commands count for a query read (other than its reader). */
236
- type CommandWritersOf<Systems, Required, Named, Reader> =
247
+ type CommandWritersOf<Systems, Required, Named, ReaderSystem> =
237
248
  Systems extends AnySystem
238
249
  ? IsErased<Systems["spec"]> extends true ? never
239
- : Systems["name"] extends Reader ? never
250
+ : SameSystem<Systems, ReaderSystem> extends true ? never
240
251
  : Systems["spec"] extends { readonly commands: infer Commands }
241
252
  ? CommandsCount<Commands, Required, Named> extends true ? Systems["name"] : never
242
253
  : never
243
254
  : never
244
255
 
245
256
  type WritersFor<Systems, R> =
246
- R extends QueryRead<any, infer Required, infer Named, infer Reader> ? CommandWritersOf<Systems, Required, Named, Reader>
247
- : R extends DataRead<infer Kind, infer Key, infer Reader> ? WritersOf<Systems, Kind, Key, Reader>
257
+ R extends QueryRead<any, infer Required, infer Named, any, infer ReaderSystem> ? CommandWritersOf<Systems, Required, Named, ReaderSystem>
258
+ : R extends DataRead<infer Kind, infer Key, any, infer ReaderSystem> ? WritersOf<Systems, Kind, Key, ReaderSystem>
248
259
  : never
249
260
 
250
261
  /**
@@ -288,16 +299,16 @@ type ReaderIndices<Entries extends ReadonlyArray<unknown>, Reader> = {
288
299
 
289
300
  type LateWriterError<R, Writer> =
290
301
  Writer extends string
291
- ? R extends QueryRead<infer Slot extends string, any, any, infer Reader extends string>
302
+ ? R extends QueryRead<infer Slot extends string, any, any, infer Reader extends string, any>
292
303
  ? `${Writer} queues commands that ${Reader} needs this tick (queries.${Slot}), but it runs after ${Reader}: move ${Writer} before the applyDeferred() that precedes ${Reader}, or read queries.${Slot} without thisTick`
293
- : R extends DataRead<any, infer Key extends string, infer Reader extends string>
304
+ : R extends DataRead<any, infer Key extends string, infer Reader extends string, any>
294
305
  ? `${Reader} needs ${Key} from this tick, but its writer ${Writer} runs after it: move ${Writer} before ${Reader}, or read ${Key} without thisTick`
295
306
  : never
296
307
  : never
297
308
 
298
309
  type UnflushedWriterError<R, Writer> =
299
310
  Writer extends string
300
- ? R extends QueryRead<infer Slot extends string, any, any, infer Reader extends string>
311
+ ? R extends QueryRead<infer Slot extends string, any, any, infer Reader extends string, any>
301
312
  ? `${Reader} needs this tick's commands for queries.${Slot}, but ${Writer} queues them with no applyDeferred() in between: add Game.Schedule.applyDeferred() after ${Writer}`
302
313
  : never
303
314
  : never
@@ -371,7 +382,7 @@ type TailSegment<Entries, Acc = never> =
371
382
  : readonly [Acc, false]
372
383
 
373
384
  type UnflushedWriters<Entries, R, Reader extends string> =
374
- R extends QueryRead<any, any, any, any>
385
+ R extends QueryRead<any, any, any, any, any>
375
386
  ? Segment<Entries, Reader> extends infer Found
376
387
  ? Found extends readonly [infer Systems, any] ? UnflushedWriterError<R, WritersFor<Systems, R>> : never
377
388
  : never
@@ -656,7 +667,8 @@ export const make = <S extends Schema.Any, const Entries extends ReadonlyArray<S
656
667
  *
657
668
  * Conditions gate every system in `entries`, nested schedules included.
658
669
  * Marker steps (`applyDeferred`, `applyStateTransitions`) in the group still
659
- * run. A gated system is the same system as far as the runtime is concerned:
670
+ * run, and so do the transition schedules an `applyStateTransitions(bundle)`
671
+ * in the group runs: gate those systems in their own specs. A gated system is the same system as far as the runtime is concerned:
660
672
  * it shares change detection and event cursors with the original and with
661
673
  * any other gated copy, so using one system in two groups of one schedule is
662
674
  * a duplicate like any other.
package/src/Schema.ts CHANGED
@@ -54,7 +54,7 @@ import type { ConstructedDescriptor, Descriptor } from "./Descriptor.ts"
54
54
  import type * as Entity from "./Entity.ts"
55
55
  import type * as EntityScope from "./EntityScope.ts"
56
56
  import { buildSchema, emptySchema, mergeSchemas } from "./internal/fragments.ts"
57
- import { makeGame } from "./internal/game.ts"
57
+ import { type exitApplyDeferredMessage, makeGame } from "./internal/game.ts"
58
58
  import type * as Condition from "./Condition.ts"
59
59
  import type * as Inspector from "./Inspector.ts"
60
60
  import type * as Machine from "./Machine.ts"
@@ -66,6 +66,7 @@ import type * as Result from "./Result.ts"
66
66
  import type * as Debug from "./Debug.ts"
67
67
  import type * as Runtime from "./Runtime.ts"
68
68
  import type * as Schedule from "./Schedule.ts"
69
+ import type * as Snapshot from "./Snapshot.ts"
69
70
  import type * as System from "./System.ts"
70
71
 
71
72
  /**
@@ -73,6 +74,27 @@ import type * as System from "./System.ts"
73
74
  */
74
75
  export type Registry = Record<string, Descriptor.Any>
75
76
 
77
+ /** At least one: a permission that names nothing allows nothing. */
78
+ type NonEmpty<T> = readonly [T, ...ReadonlyArray<T>]
79
+
80
+ type Article<Kind extends string> = Kind extends "event" ? "an" : "a"
81
+
82
+ /**
83
+ * Rejects descriptors of another kind in a fragment's `components`,
84
+ * `resources`, or `events`, naming where each belongs.
85
+ */
86
+ type RegistryOfKind<R, Expected extends string> = {
87
+ readonly [K in keyof R]: R[K] extends { readonly kind: Expected }
88
+ ? unknown
89
+ : R[K] extends { readonly kind: infer Actual extends string }
90
+ ? {
91
+ readonly __fixSchema__: Actual extends "service"
92
+ ? `${K & string} is a service, not ${Article<Expected>} ${Expected}: provide services to Game.Runtime.make({ services }) instead of registering them`
93
+ : `${K & string} is ${Article<Actual>} ${Actual}, not ${Article<Expected>} ${Expected}: move it to ${Actual}s: { ... }`
94
+ }
95
+ : unknown
96
+ }
97
+
76
98
  /**
77
99
  * A closed schema: the components, resources, events, and relations a world
78
100
  * may contain, and the component requirements (`Schema.requires`) every
@@ -133,6 +155,10 @@ type PairsOf<R> = R extends ComponentRequirement<infer Pairs> ? Pairs : never
133
155
  * Requirements chain (`A` requires `B`, `B` requires `C`) and may name
134
156
  * components of other fragments of the same `Schema.bind`.
135
157
  *
158
+ * A saved component cannot require a transient one (`Schema.bind` rejects
159
+ * it): a restore would bring the requirer back without it. A transient
160
+ * requirer may require a transient component.
161
+ *
136
162
  * @example
137
163
  * ```ts
138
164
  * const Gameplay = Schema.fragment({
@@ -250,17 +276,33 @@ export type ValidateFragments<Fragments extends ReadonlyArray<Schema.Any>> =
250
276
  type UnregisteredRequirements<S extends Schema.Any> =
251
277
  Exclude<Schema.Requires<S>[0] | Schema.Requires<S>[1], Schema.ComponentDescriptor<S>["name"]>
252
278
 
279
+ type TransientComponentName<S extends Schema.Any> = Extract<Schema.ComponentDescriptor<S>, { readonly transient: true }>["name"]
280
+
281
+ /** One message per saved requirer that requires a transient component. */
282
+ type TransientRequirements<S extends Schema.Any, Pairs = Schema.Requires<S>> =
283
+ Pairs extends readonly [infer From extends string, infer To extends string]
284
+ ? To extends TransientComponentName<S>
285
+ ? From extends TransientComponentName<S> ? never
286
+ : `${From} requires ${To}, which is transient and not saved: make ${From} transient too, or give ${To} a constructor`
287
+ : never
288
+ : never
289
+
253
290
  /**
254
291
  * Rejects requirements naming a component the bound schema does not
255
292
  * register. Checked at `bind`, so a fragment may require a component another
256
- * fragment registers. Erased schemas (`Schema.Any`) are checked by `bind` at
257
- * runtime instead.
293
+ * fragment registers. Also rejects a saved component requiring a transient
294
+ * one (a restore would bring the requirer back without it). Erased schemas
295
+ * (`Schema.Any`) are checked by `bind` at runtime instead.
258
296
  */
259
297
  export type ValidateRequires<S extends Schema.Any> =
260
- [UnregisteredRequirements<S>] extends [never]
261
- ? unknown
262
- : string extends Schema.Requires<S>[1]
298
+ string extends Schema.Requires<S>[1]
263
299
  ? unknown
300
+ : [UnregisteredRequirements<S>] extends [never]
301
+ ? [TransientRequirements<S>] extends [never]
302
+ ? unknown
303
+ : {
304
+ readonly __schemaRequires__: TransientRequirements<S>
305
+ }
264
306
  : {
265
307
  readonly __schemaRequires__: `${UnregisteredRequirements<S>} is named in Schema.requires(...) but no fragment registers it: add it to a fragment's components`
266
308
  }
@@ -288,11 +330,6 @@ export const defineRoot = <const Name extends string>(name: Name): RootToken<Nam
288
330
  }
289
331
  }) as RootToken<Name>
290
332
 
291
- /**
292
- * Creates an empty schema.
293
- */
294
- export const empty = (): SchemaDefinition => emptySchema()
295
-
296
333
  /**
297
334
  * Creates a schema fragment.
298
335
  *
@@ -316,9 +353,9 @@ export const fragment = <
316
353
  const Relations extends Record<string, Relation.Relation.Any> = {},
317
354
  const Requires extends ReadonlyArray<ComponentRequirement> = readonly []
318
355
  >(definition: {
319
- readonly components?: Components
320
- readonly resources?: Resources
321
- readonly events?: Events
356
+ readonly components?: Components & RegistryOfKind<Components, "component">
357
+ readonly resources?: Resources & RegistryOfKind<Resources, "resource">
358
+ readonly events?: Events & RegistryOfKind<Events, "event">
322
359
  readonly relations?: Relations
323
360
  readonly requires?: Requires
324
361
  } & ValidateFragments<[SchemaDefinition<Components, Resources, Events, Relations>]>): SchemaDefinition<Components, Resources, Events, Relations, PairsOf<Requires[number]>> =>
@@ -420,6 +457,16 @@ type BoundScheduleEntry<S extends Schema.Any, Root> =
420
457
  type BoundTransitionEntry<S extends Schema.Any, Root> =
421
458
  Exclude<BoundScheduleEntry<S, Root>, Schedule.ApplyStateTransitionsStep<any, Root>>
422
459
 
460
+ /**
461
+ * Rejects `applyDeferred()` in an exit phase (`onExit`, `onTransition`): the
462
+ * phase rolls back as one when a system in it fails, which applied commands
463
+ * could not. Nested schedules are checked when the schedule is built.
464
+ */
465
+ type NoApplyDeferredInExit<Entries extends ReadonlyArray<unknown>> =
466
+ [Extract<Entries[number], Schedule.ApplyDeferredStep>] extends [never]
467
+ ? unknown
468
+ : { readonly __fixTransition__: typeof exitApplyDeferredMessage }
469
+
423
470
  type BoundTransitionScheduleResult<
424
471
  S extends Schema.Any,
425
472
  Root,
@@ -428,7 +475,8 @@ type BoundTransitionScheduleResult<
428
475
  > = Machine.TransitionScheduleDefinition<
429
476
  S,
430
477
  M,
431
- Schedule.CompositionExactRequirements<Entries>,
478
+ // The machine itself: a transition schedule on a machine the runtime does not provide could never run.
479
+ Schedule.CompositionExactRequirements<Entries> | M,
432
480
  Root,
433
481
  Schedule.CompositionFailure<Entries>,
434
482
  Schedule.CompositionOrder<Entries>
@@ -610,6 +658,13 @@ export namespace Schema {
610
658
  target: Entity.HandleTarget<S, Root>,
611
659
  intent?: D
612
660
  ) => Entity.Handle<Root, D>
661
+ /**
662
+ * The entity id a handle names, to compare with ids or pass where an id
663
+ * is expected (`queries.x.get(id)`), instead of storing the id beside
664
+ * the handle. Like the handle, it proves nothing about liveness:
665
+ * resolve it through a query. Two handles of the same entity are `===`.
666
+ */
667
+ id: (handle: Entity.Handle<Root, ComponentDescriptor<S> | undefined>) => Entity.EntityId<S, Root>
613
668
  }
614
669
  readonly Query: {
615
670
  <
@@ -624,7 +679,7 @@ export namespace Schema {
624
679
  >(spec: {
625
680
  readonly selection: Selection
626
681
  readonly with?: With
627
- readonly without?: Without
682
+ readonly without?: Without & QueryModule.ExclusionsPossible<Selection, With, Without, Filters>
628
683
  readonly filters?: Filters
629
684
  readonly withRelations?: WithRelations
630
685
  readonly withoutRelations?: WithoutRelations
@@ -658,20 +713,25 @@ export namespace Schema {
658
713
  * Adds entries to a draft (a value not yet spawned). Use
659
714
  * `commands.insert` for an entity that already exists.
660
715
  */
661
- extend: <P extends Entity.ComponentProof, const Entries extends ReadonlyArray<Command.SpawnEntry<S>>>(
662
- draft: Entity.EntityDraft<S, P, Root>,
716
+ extend: <P extends Entity.ComponentProof, const Entries extends ReadonlyArray<Command.SpawnEntry<S>>, Relates = never>(
717
+ draft: Entity.EntityDraft<S, P, Root, Relates>,
663
718
  ...entries: Entries
664
- ) => Command.DraftFor<S, Entries, P, Root>
665
- entry: <D extends ComponentDescriptor<S>>(descriptor: D, value: Descriptor.Value<D>) => Command.Entry<D>
719
+ ) => Command.DraftFor<S, Entries, P, Root, Relates>
720
+ entry: <D extends ComponentDescriptor<S>>(descriptor: D, value: QueryModule.ReadonlyValue<Descriptor.Value<D>>) => Command.Entry<D>
666
721
  entryRaw: <D extends Extract<ComponentDescriptor<S>, ConstructedDescriptor<"component", string, any, any, any>>>(
667
722
  descriptor: D,
668
723
  raw: Descriptor.Raw<D>
669
724
  ) => Result.Result<Command.Entry<D>, Descriptor.ConstructionError<D>>
670
- relate: <P extends Entity.ComponentProof, R extends RelationDescriptor<S>>(
671
- draft: Entity.EntityDraft<S, P, Root>,
725
+ /**
726
+ * Stages a relation on a draft, created with the spawn. Spawning the
727
+ * draft needs the relation in `Game.System.relates(...)`, like
728
+ * `commands.relate`.
729
+ */
730
+ relate: <P extends Entity.ComponentProof, R extends RelationDescriptor<S>, Relates = never>(
731
+ draft: Entity.EntityDraft<S, P, Root, Relates>,
672
732
  relation: R,
673
733
  target: Entity.EntityId<S, Root>
674
- ) => Entity.EntityDraft<S, P, Root>
734
+ ) => Entity.EntityDraft<S, P, Root, Relates | R>
675
735
  }
676
736
  /**
677
737
  * Declares one game-wide mode (menu, playing, paused): queued with
@@ -694,6 +754,12 @@ export namespace Schema {
694
754
  * returns a `SubStateInactive` failure. So `inState(Sub, x)` alone
695
755
  * implies the parent's state.
696
756
  *
757
+ * The sub-state enters together with the parent's commit, before any
758
+ * `onEnter` schedule runs: the parent's `onEnter` systems already see its
759
+ * value, its `despawnOnEnter` scopes are cleared before they spawn, and
760
+ * a failing `onEnter` leaves it entered. Its own `onEnter` schedules
761
+ * run after the parent's.
762
+ *
697
763
  * Use a second top-level machine instead when the two modes are
698
764
  * independent (each keeps its value whatever the other does).
699
765
  *
@@ -775,17 +841,20 @@ export namespace Schema {
775
841
  */
776
842
  commands: typeof Command.commands
777
843
  /** Allows spawning drafts whose components are all among these and what they require (`Schema.requires`); see `Command.spawns`. */
778
- spawns: <const Components extends ReadonlyArray<ComponentDescriptor<S>>>(...components: Components) => Command.Permission<"spawns", Components[number] | Command.RequiredDescriptors<S, Components[number]["name"]>>
844
+ spawns: <const Components extends NonEmpty<ComponentDescriptor<S>>>(...components: Components) => Command.Permission<"spawns", Components[number] | Command.RequiredDescriptors<S, Components[number]["name"]>>
779
845
  /** Allows `insert` (add or replace at the next `applyDeferred()`) of these; see `Command.inserts`. */
780
- inserts: <const Components extends ReadonlyArray<ComponentDescriptor<S>>>(...components: Components) => Command.Permission<"inserts", Components[number]>
846
+ inserts: <const Components extends NonEmpty<ComponentDescriptor<S>>>(...components: Components) => Command.Permission<"inserts", Components[number]>
781
847
  /** Allows `remove` of these. */
782
- removes: <const Components extends ReadonlyArray<ComponentDescriptor<S>>>(...components: Components) => Command.Permission<"removes", Components[number]>
848
+ removes: <const Components extends NonEmpty<ComponentDescriptor<S>>>(...components: Components) => Command.Permission<"removes", Components[number]>
783
849
  /** Allows `despawn` of entities a query proves have one of these; see `Command.despawns`. */
784
- despawns: <const Components extends ReadonlyArray<ComponentDescriptor<S>>>(...components: Components) => Command.Permission<"despawns", Components[number]>
850
+ despawns: <const Components extends NonEmpty<ComponentDescriptor<S>>>(...components: Components) => Command.Permission<"despawns", Components[number]>
785
851
  /** Allows `despawnScope` of this scope. */
786
852
  despawnsScope: <const Scope extends EntityScope.EntityScope<string, Root>>(scope: Scope) => Command.Permission<"despawnsScope", Scope>
787
- /** Allows `relate`, `unrelate`, and `reorderChildren` of these relations. */
788
- relates: <const Relations extends ReadonlyArray<RelationDescriptor<S>>>(...relations: Relations) => Command.Permission<"relates", Relations[number]>
853
+ /**
854
+ * Allows `relate`, `unrelate`, and `reorderChildren` of these relations,
855
+ * and spawning drafts that stage them with `Game.Command.relate(...)`.
856
+ */
857
+ relates: <const Relations extends NonEmpty<RelationDescriptor<S>>>(...relations: Relations) => Command.Permission<"relates", Relations[number]>
789
858
  }
790
859
  readonly Schedule: {
791
860
  /**
@@ -803,20 +872,31 @@ export namespace Schema {
803
872
  state: Machine.StateMachine.Value<M>,
804
873
  plan: readonly [...Entries]
805
874
  ) => BoundTransitionScheduleResult<S, Root, M, Entries>
875
+ /**
876
+ * Systems run when `machine` leaves `state`. The exit phase of one
877
+ * transition (the exit schedules of the sub-states it ends, then of
878
+ * `state`, then `onTransition`) is one transaction: when a system in it
879
+ * fails, everything the phase did is undone (except `local`) and the
880
+ * transition stays queued, so the next attempt starts from the same
881
+ * state. Its commands are applied at the next `applyDeferred()` after
882
+ * the marker, so it cannot contain `applyDeferred()` itself.
883
+ */
806
884
  onExit: <M extends BoundStateMachine<Root>, const Entries extends ReadonlyArray<BoundTransitionEntry<S, Root>>>(
807
885
  machine: M,
808
886
  state: Machine.StateMachine.Value<M>,
809
- plan: readonly [...Entries]
887
+ plan: readonly [...Entries] & NoApplyDeferredInExit<Entries>
810
888
  ) => BoundTransitionScheduleResult<S, Root, M, Entries>
889
+ /** Systems run on one `[from, to]` transition, last in its exit phase (see `onExit`). */
811
890
  onTransition: <M extends BoundStateMachine<Root>, const Entries extends ReadonlyArray<BoundTransitionEntry<S, Root>>>(
812
891
  machine: M,
813
892
  transition: readonly [Machine.StateMachine.Value<M>, Machine.StateMachine.Value<M>],
814
- plan: readonly [...Entries]
893
+ plan: readonly [...Entries] & NoApplyDeferredInExit<Entries>
815
894
  ) => BoundTransitionScheduleResult<S, Root, M, Entries>
816
895
  /**
817
896
  * Builds a schedule whose systems run only while every condition
818
897
  * passes (in addition to their own `when`). Nested schedules are
819
- * gated too; marker steps still run. Gated systems drop the events
898
+ * gated too; marker steps still run, and so do the transition schedules
899
+ * an `applyStateTransitions(bundle)` in the group runs. Gated systems drop the events
820
900
  * published while gated; use `pauseWhen` to keep them. For one system,
821
901
  * use `when` in its spec.
822
902
  */
@@ -848,8 +928,9 @@ export namespace Schema {
848
928
  }
849
929
  readonly Runtime: {
850
930
  /**
851
- * Builds a runtime. Returns a `Result` exactly when a provided resource
852
- * goes through a constructed descriptor and can fail validation.
931
+ * Builds a runtime. Always returns a `Result`: a `Success` (read
932
+ * `.value`) when nothing provided can fail, and a `Result` to check
933
+ * when a provided resource goes through a constructed descriptor.
853
934
  */
854
935
  make: <
855
936
  const ProvidedServices extends Runtime.RuntimeServices<any>,
@@ -858,10 +939,12 @@ export namespace Schema {
858
939
  const Enabled extends Debug.Option | undefined = undefined
859
940
  >(options: {
860
941
  readonly services: ProvidedServices
861
- readonly resources?: Resources
942
+ readonly resources?: Resources & Runtime.ExactResources<S, Resources>
862
943
  readonly machines?: ProvidedMachines
863
944
  /** Attaches a `debug` handle to the runtime; see the `Debug` module. */
864
945
  readonly debug?: Enabled
946
+ /** The game's save migrations (`Snapshot.migrations`): `restore` upgrades older saves with them. */
947
+ readonly saves?: Snapshot.Migrations
865
948
  }) => Runtime.MakeRuntimeResult<S, RuntimeServicesOf<ProvidedServices>, Resources, Root, RuntimeMachinesOf<ProvidedMachines>, Enabled>
866
949
  service: typeof Runtime.service
867
950
  services: typeof Runtime.services
package/src/Snapshot.ts CHANGED
@@ -24,10 +24,17 @@
24
24
  *
25
25
  * Restoring despawns every current entity and spawns the saved ones with their
26
26
  * original ids, so change detection and renderer sync see a restore as normal
27
- * despawns and spawns. Pending commands and events are dropped. Transient
27
+ * despawns and spawns. Pending commands and events emitted by systems are
28
+ * dropped; events the host sent with `runtime.send` and no system has read
29
+ * yet are kept, since they are input that has not been handled. Transient
28
30
  * components are absent from restored entities; transient resources keep their
29
31
  * current value.
30
32
  *
33
+ * When the game's save format changes, give the runtime its migrations
34
+ * (`Game.Runtime.make({ saves: Snapshot.migrations(...) })`): saves carry a
35
+ * `gameVersion`, and `restore` upgrades older ones step by step before
36
+ * validating them.
37
+ *
31
38
  * @module Snapshot
32
39
  * @docGroup runtime
33
40
  *
@@ -90,6 +97,12 @@ export const version = 2
90
97
 
91
98
  export interface WorldSnapshot {
92
99
  readonly version: typeof version
100
+ /**
101
+ * The game's own save version: the last `Snapshot.migration` the runtime
102
+ * that wrote it was given (0 without migrations, and for saves written
103
+ * before game versions existed).
104
+ */
105
+ readonly gameVersion: number
93
106
  /** The next id the runtime will allocate. */
94
107
  readonly nextEntity: number
95
108
  readonly entities: ReadonlyArray<EntitySnapshot>
@@ -113,6 +126,8 @@ export interface WorldSnapshot {
113
126
  export type RestoreError =
114
127
  | { readonly _tag: "InvalidSnapshot"; readonly path: string }
115
128
  | { readonly _tag: "UnsupportedVersion"; readonly version: unknown }
129
+ /** A save written by a newer game than this runtime's migrations reach. */
130
+ | { readonly _tag: "NewerGameVersion"; readonly gameVersion: number; readonly latest: number }
116
131
  | { readonly _tag: "UnknownComponent"; readonly entityId: number; readonly name: string }
117
132
  | { readonly _tag: "InvalidComponent"; readonly entityId: number; readonly name: string; readonly error: unknown }
118
133
  /** A saved entity holds `requiredBy` but lacks `name`, which `Schema.requires` says it needs. */
@@ -126,6 +141,8 @@ export type RestoreError =
126
141
  /** A saved sub-state value while its parent machine is not in the state the sub-state exists in. */
127
142
  | { readonly _tag: "InactiveSubState"; readonly name: string; readonly parent: string; readonly requires: unknown }
128
143
  | { readonly _tag: "DuplicateEntity"; readonly entityId: number }
144
+ /** A scope name that no `Game.EntityScope(...)` of this game defines. */
145
+ | { readonly _tag: "UnknownScope"; readonly name: string }
129
146
  /** A scope member that is not a saved entity, or is already in another scope. */
130
147
  | { readonly _tag: "InvalidScope"; readonly name: string; readonly entityId: number }
131
148
  | { readonly _tag: "UnvalidatedDescriptor"; readonly name: string }
@@ -145,6 +162,8 @@ const invalid = (path: string) => Result.failure<RestoreError>({ _tag: "InvalidS
145
162
  export const parse = (data: unknown): Result.Result<WorldSnapshot, RestoreError> => {
146
163
  if (!isRecord(data)) return invalid("$")
147
164
  if (data["version"] !== version) return Result.failure({ _tag: "UnsupportedVersion", version: data["version"] })
165
+ const gameVersion = data["gameVersion"] ?? 0
166
+ if (typeof gameVersion !== "number" || !Number.isSafeInteger(gameVersion) || gameVersion < 0) return invalid("$.gameVersion")
148
167
  if (!isEntityId(data["nextEntity"])) return invalid("$.nextEntity")
149
168
  const entities = data["entities"]
150
169
  if (!Array.isArray(entities)) return invalid("$.entities")
@@ -176,5 +195,74 @@ export const parse = (data: unknown): Result.Result<WorldSnapshot, RestoreError>
176
195
  for (const [name, members] of Object.entries(scopes)) {
177
196
  if (!Array.isArray(members) || !members.every(isEntityId)) return invalid(`$.scopes.${name}`)
178
197
  }
179
- return Result.success(data as unknown as WorldSnapshot)
198
+ return Result.success({ ...data, gameVersion } as unknown as WorldSnapshot)
199
+ }
200
+
201
+ /**
202
+ * One step of a game's save format: turns a save of version `to - 1` into
203
+ * version `to`. A pure function on the save's plain data, tested like one.
204
+ */
205
+ export interface Migration<To extends number = number> {
206
+ readonly to: To
207
+ readonly migrate: (save: WorldSnapshot) => WorldSnapshot
208
+ }
209
+
210
+ /** A game's save migrations, given to `Game.Runtime.make({ saves })`. Built with {@link migrations}. */
211
+ export interface Migrations<Latest extends number = number> {
212
+ readonly kind: "SnapshotMigrations"
213
+ /** The game version `snapshot()` writes. */
214
+ readonly latest: Latest
215
+ readonly steps: ReadonlyArray<Migration>
216
+ }
217
+
218
+ /**
219
+ * Declares the step that upgrades saves to game version `to` (from `to - 1`).
220
+ *
221
+ * @example
222
+ * ```ts
223
+ * Snapshot.migration(2, (save) => ({ ...save, resources: { ...save.resources, "Town/Gold": 0 } }))
224
+ * ```
225
+ */
226
+ export const migration = <const To extends number>(to: To, migrate: (save: WorldSnapshot) => WorldSnapshot): Migration<To> => ({ to, migrate })
227
+
228
+ type Last<Steps> = Steps extends readonly [...Array<unknown>, infer L extends Migration] ? L["to"] : number
229
+
230
+ /** `unknown` when the steps are numbered 1, 2, 3, ... in order; otherwise the message naming the misplaced one. */
231
+ type Consecutive<Steps, Count extends Array<0> = []> =
232
+ Steps extends readonly [infer Head extends Migration, ...infer Tail]
233
+ ? number extends Head["to"] ? unknown
234
+ : Head["to"] extends [...Count, 0]["length"] ? Consecutive<Tail, [...Count, 0]>
235
+ : { readonly __fixMigrations__: `migration ${Head["to"]} is listed where migration ${[...Count, 0]["length"]} belongs: number migrations 1, 2, 3, ... in order` }
236
+ : unknown
237
+
238
+ /**
239
+ * A game's save migrations, numbered 1, 2, 3, ... in order (checked at
240
+ * compile time for literal numbers, and at runtime). Pass them to
241
+ * `Game.Runtime.make({ saves })`: `snapshot()` then writes the last number as
242
+ * the save's `gameVersion`, and `restore(data)` runs every step after the
243
+ * save's version, in order, before validating it as usual. A save newer than
244
+ * the last step fails with `NewerGameVersion`. Use it instead of editing raw
245
+ * save data before `restore`.
246
+ *
247
+ * @example
248
+ * ```ts
249
+ * const runtime = Game.Runtime.make({
250
+ * services,
251
+ * saves: Snapshot.migrations(
252
+ * Snapshot.migration(1, (save) => ({ ...save, scopes: {} })),
253
+ * Snapshot.migration(2, renameGold)
254
+ * )
255
+ * }).value
256
+ * runtime.restore(raw)
257
+ * ```
258
+ */
259
+ export const migrations = <const Steps extends readonly [Migration, ...Array<Migration>]>(
260
+ ...steps: Steps & Consecutive<Steps>
261
+ ): Migrations<Last<Steps>> => {
262
+ steps.forEach((step, index) => {
263
+ if (step.to !== index + 1) {
264
+ throw new Error(`migration ${step.to} is listed where migration ${index + 1} belongs: number migrations 1, 2, 3, ... in order`)
265
+ }
266
+ })
267
+ return { kind: "SnapshotMigrations", latest: steps.length as Last<Steps>, steps }
180
268
  }