@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.
- package/README.md +3 -2
- package/dist/Clock.d.ts +19 -4
- package/dist/Clock.d.ts.map +1 -1
- package/dist/Clock.js +18 -2
- package/dist/Clock.js.map +1 -1
- package/dist/Command.d.ts +101 -45
- package/dist/Command.d.ts.map +1 -1
- package/dist/Command.js +22 -4
- package/dist/Command.js.map +1 -1
- package/dist/Debug.d.ts +66 -8
- package/dist/Debug.d.ts.map +1 -1
- package/dist/Descriptor.d.ts +27 -9
- package/dist/Descriptor.d.ts.map +1 -1
- package/dist/Descriptor.js +13 -9
- package/dist/Descriptor.js.map +1 -1
- package/dist/Entity.d.ts +15 -8
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js.map +1 -1
- package/dist/FixedLoop.d.ts +106 -0
- package/dist/FixedLoop.d.ts.map +1 -0
- package/dist/FixedLoop.js +97 -0
- package/dist/FixedLoop.js.map +1 -0
- package/dist/HostClock.d.ts +1 -1
- package/dist/HostClock.d.ts.map +1 -1
- package/dist/HostClock.js +2 -1
- package/dist/HostClock.js.map +1 -1
- package/dist/Machine.d.ts +4 -1
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +4 -1
- package/dist/Machine.js.map +1 -1
- package/dist/Query.d.ts +52 -10
- package/dist/Query.d.ts.map +1 -1
- package/dist/Query.js +2 -1
- package/dist/Query.js.map +1 -1
- package/dist/Relation.d.ts +5 -14
- package/dist/Relation.d.ts.map +1 -1
- package/dist/Relation.js +0 -32
- package/dist/Relation.js.map +1 -1
- package/dist/Runtime.d.ts +58 -15
- package/dist/Runtime.d.ts.map +1 -1
- package/dist/Runtime.js +288 -70
- package/dist/Runtime.js.map +1 -1
- package/dist/Schedule.d.ts +24 -15
- package/dist/Schedule.d.ts.map +1 -1
- package/dist/Schedule.js +2 -1
- package/dist/Schedule.js.map +1 -1
- package/dist/Schema.d.ts +98 -29
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +4 -4
- package/dist/Schema.js.map +1 -1
- package/dist/Snapshot.d.ts +76 -1
- package/dist/Snapshot.d.ts.map +1 -1
- package/dist/Snapshot.js +42 -1
- package/dist/Snapshot.js.map +1 -1
- package/dist/System.d.ts +68 -24
- package/dist/System.d.ts.map +1 -1
- package/dist/System.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/cells.d.ts +2 -0
- package/dist/internal/cells.d.ts.map +1 -1
- package/dist/internal/cells.js +34 -8
- package/dist/internal/cells.js.map +1 -1
- package/dist/internal/debug.d.ts +3 -0
- package/dist/internal/debug.d.ts.map +1 -1
- package/dist/internal/debug.js +83 -15
- package/dist/internal/debug.js.map +1 -1
- package/dist/internal/fragments.d.ts +2 -6
- package/dist/internal/fragments.d.ts.map +1 -1
- package/dist/internal/fragments.js +25 -2
- package/dist/internal/fragments.js.map +1 -1
- package/dist/internal/game.d.ts +2 -0
- package/dist/internal/game.d.ts.map +1 -1
- package/dist/internal/game.js +16 -3
- package/dist/internal/game.js.map +1 -1
- package/dist/internal/queries.d.ts.map +1 -1
- package/dist/internal/queries.js +27 -18
- package/dist/internal/queries.js.map +1 -1
- package/dist/internal/relations.d.ts +10 -0
- package/dist/internal/relations.d.ts.map +1 -0
- package/dist/internal/relations.js +32 -0
- package/dist/internal/relations.js.map +1 -0
- package/dist/internal/streams.d.ts +2 -0
- package/dist/internal/streams.d.ts.map +1 -1
- package/dist/internal/streams.js +10 -0
- package/dist/internal/streams.js.map +1 -1
- package/dist/internal/world.d.ts +7 -1
- package/dist/internal/world.d.ts.map +1 -1
- package/dist/internal/world.js +53 -3
- package/dist/internal/world.js.map +1 -1
- package/package.json +1 -1
- package/src/Clock.ts +29 -4
- package/src/Command.ts +161 -67
- package/src/Debug.ts +61 -9
- package/src/Descriptor.ts +43 -17
- package/src/Entity.ts +16 -9
- package/src/FixedLoop.ts +218 -0
- package/src/HostClock.ts +2 -1
- package/src/Machine.ts +4 -1
- package/src/Query.ts +47 -10
- package/src/Relation.ts +5 -67
- package/src/Runtime.ts +345 -85
- package/src/Schedule.ts +31 -19
- package/src/Schema.ts +118 -35
- package/src/Snapshot.ts +90 -2
- package/src/System.ts +76 -23
- package/src/index.ts +4 -0
- package/src/internal/cells.ts +42 -10
- package/src/internal/debug.ts +84 -15
- package/src/internal/fragments.ts +23 -3
- package/src/internal/game.ts +19 -3
- package/src/internal/queries.ts +28 -2
- package/src/internal/relations.ts +52 -0
- package/src/internal/streams.ts +11 -0
- 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> =
|
|
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
|
-
/**
|
|
207
|
-
type
|
|
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
|
|
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,
|
|
247
|
+
type CommandWritersOf<Systems, Required, Named, ReaderSystem> =
|
|
237
248
|
Systems extends AnySystem
|
|
238
249
|
? IsErased<Systems["spec"]> extends true ? never
|
|
239
|
-
: Systems
|
|
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
|
|
247
|
-
: R extends DataRead<infer Kind, infer Key, infer
|
|
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
|
|
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.
|
|
257
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
671
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
788
|
-
|
|
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
|
|
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.
|
|
852
|
-
*
|
|
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
|
|
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
|
}
|