@typeonce/ritmo 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/Clock.d.ts +19 -4
- package/dist/Clock.d.ts.map +1 -1
- package/dist/Clock.js +18 -2
- package/dist/Clock.js.map +1 -1
- package/dist/Command.d.ts +181 -40
- package/dist/Command.d.ts.map +1 -1
- package/dist/Command.js +31 -11
- package/dist/Command.js.map +1 -1
- package/dist/Condition.d.ts +12 -1
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js.map +1 -1
- package/dist/Debug.d.ts +127 -13
- package/dist/Debug.d.ts.map +1 -1
- package/dist/Descriptor.d.ts +61 -10
- package/dist/Descriptor.d.ts.map +1 -1
- package/dist/Descriptor.js +22 -10
- package/dist/Descriptor.js.map +1 -1
- package/dist/Entity.d.ts +22 -7
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js.map +1 -1
- package/dist/EntityScope.d.ts +20 -1
- package/dist/EntityScope.d.ts.map +1 -1
- package/dist/EntityScope.js +8 -2
- package/dist/EntityScope.js.map +1 -1
- package/dist/FixedLoop.d.ts +106 -0
- package/dist/FixedLoop.d.ts.map +1 -0
- package/dist/FixedLoop.js +97 -0
- package/dist/FixedLoop.js.map +1 -0
- package/dist/HostClock.d.ts +1 -1
- package/dist/HostClock.d.ts.map +1 -1
- package/dist/HostClock.js +2 -1
- package/dist/HostClock.js.map +1 -1
- package/dist/Machine.d.ts +59 -34
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +10 -3
- package/dist/Machine.js.map +1 -1
- package/dist/Query.d.ts +100 -14
- package/dist/Query.d.ts.map +1 -1
- package/dist/Query.js +34 -5
- package/dist/Query.js.map +1 -1
- package/dist/Relation.d.ts +5 -14
- package/dist/Relation.d.ts.map +1 -1
- package/dist/Relation.js +0 -32
- package/dist/Relation.js.map +1 -1
- package/dist/Runtime.d.ts +61 -15
- package/dist/Runtime.d.ts.map +1 -1
- package/dist/Runtime.js +594 -70
- package/dist/Runtime.js.map +1 -1
- package/dist/Schedule.d.ts +24 -15
- package/dist/Schedule.d.ts.map +1 -1
- package/dist/Schedule.js +2 -1
- package/dist/Schedule.js.map +1 -1
- package/dist/Schema.d.ts +233 -48
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +33 -7
- package/dist/Schema.js.map +1 -1
- package/dist/Snapshot.d.ts +90 -1
- package/dist/Snapshot.d.ts.map +1 -1
- package/dist/Snapshot.js +42 -1
- package/dist/Snapshot.js.map +1 -1
- package/dist/System.d.ts +132 -31
- package/dist/System.d.ts.map +1 -1
- package/dist/System.js +32 -0
- package/dist/System.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/cells.d.ts +2 -0
- package/dist/internal/cells.d.ts.map +1 -1
- package/dist/internal/cells.js +34 -8
- package/dist/internal/cells.js.map +1 -1
- package/dist/internal/debug.d.ts +10 -0
- package/dist/internal/debug.d.ts.map +1 -1
- package/dist/internal/debug.js +171 -12
- package/dist/internal/debug.js.map +1 -1
- package/dist/internal/equal.d.ts +8 -0
- package/dist/internal/equal.d.ts.map +1 -0
- package/dist/internal/equal.js +38 -0
- package/dist/internal/equal.js.map +1 -0
- package/dist/internal/fragments.d.ts +2 -6
- package/dist/internal/fragments.d.ts.map +1 -1
- package/dist/internal/fragments.js +36 -3
- package/dist/internal/fragments.js.map +1 -1
- package/dist/internal/game.d.ts +2 -0
- package/dist/internal/game.d.ts.map +1 -1
- package/dist/internal/game.js +48 -7
- package/dist/internal/game.js.map +1 -1
- package/dist/internal/order.d.ts.map +1 -1
- package/dist/internal/order.js +4 -2
- package/dist/internal/order.js.map +1 -1
- package/dist/internal/queries.d.ts.map +1 -1
- package/dist/internal/queries.js +57 -26
- package/dist/internal/queries.js.map +1 -1
- package/dist/internal/relations.d.ts +10 -0
- package/dist/internal/relations.d.ts.map +1 -0
- package/dist/internal/relations.js +32 -0
- package/dist/internal/relations.js.map +1 -0
- package/dist/internal/requirements.d.ts +26 -0
- package/dist/internal/requirements.d.ts.map +1 -0
- package/dist/internal/requirements.js +46 -0
- package/dist/internal/requirements.js.map +1 -0
- package/dist/internal/streams.d.ts +2 -0
- package/dist/internal/streams.d.ts.map +1 -1
- package/dist/internal/streams.js +10 -0
- package/dist/internal/streams.js.map +1 -1
- package/dist/internal/world.d.ts +7 -1
- package/dist/internal/world.d.ts.map +1 -1
- package/dist/internal/world.js +53 -3
- package/dist/internal/world.js.map +1 -1
- package/package.json +1 -1
- package/src/Clock.ts +29 -4
- package/src/Command.ts +281 -67
- package/src/Condition.ts +17 -1
- package/src/Debug.ts +124 -15
- package/src/Descriptor.ts +92 -21
- package/src/Entity.ts +23 -7
- package/src/EntityScope.ts +21 -2
- package/src/FixedLoop.ts +218 -0
- package/src/HostClock.ts +2 -1
- package/src/Machine.ts +72 -35
- package/src/Query.ts +118 -17
- package/src/Relation.ts +5 -67
- package/src/Runtime.ts +659 -85
- package/src/Schedule.ts +31 -19
- package/src/Schema.ts +297 -57
- package/src/Snapshot.ts +94 -2
- package/src/System.ts +151 -29
- package/src/index.ts +4 -0
- package/src/internal/cells.ts +42 -10
- package/src/internal/debug.ts +179 -14
- package/src/internal/equal.ts +31 -0
- package/src/internal/fragments.ts +35 -5
- package/src/internal/game.ts +69 -7
- package/src/internal/order.ts +8 -4
- package/src/internal/queries.ts +62 -12
- package/src/internal/relations.ts +52 -0
- package/src/internal/requirements.ts +71 -0
- package/src/internal/streams.ts +11 -0
- package/src/internal/world.ts +60 -4
package/src/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,15 +126,23 @@ 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 }
|
|
133
|
+
/** A saved entity holds `requiredBy` but lacks `name`, which `Schema.requires` says it needs. */
|
|
134
|
+
| { readonly _tag: "MissingRequiredComponent"; readonly entityId: number; readonly name: string; readonly requiredBy: string }
|
|
118
135
|
| { readonly _tag: "UnknownResource"; readonly name: string }
|
|
119
136
|
| { readonly _tag: "InvalidResource"; readonly name: string; readonly error: unknown }
|
|
120
137
|
| { readonly _tag: "UnknownRelation"; readonly name: string }
|
|
121
138
|
| { readonly _tag: "InvalidRelation"; readonly name: string; readonly sourceId: number; readonly targetId: number }
|
|
122
139
|
| { readonly _tag: "UnknownMachine"; readonly name: string }
|
|
123
140
|
| { readonly _tag: "InvalidMachineState"; readonly name: string; readonly value: unknown }
|
|
141
|
+
/** A saved sub-state value while its parent machine is not in the state the sub-state exists in. */
|
|
142
|
+
| { readonly _tag: "InactiveSubState"; readonly name: string; readonly parent: string; readonly requires: unknown }
|
|
124
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 }
|
|
125
146
|
/** A scope member that is not a saved entity, or is already in another scope. */
|
|
126
147
|
| { readonly _tag: "InvalidScope"; readonly name: string; readonly entityId: number }
|
|
127
148
|
| { readonly _tag: "UnvalidatedDescriptor"; readonly name: string }
|
|
@@ -141,6 +162,8 @@ const invalid = (path: string) => Result.failure<RestoreError>({ _tag: "InvalidS
|
|
|
141
162
|
export const parse = (data: unknown): Result.Result<WorldSnapshot, RestoreError> => {
|
|
142
163
|
if (!isRecord(data)) return invalid("$")
|
|
143
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")
|
|
144
167
|
if (!isEntityId(data["nextEntity"])) return invalid("$.nextEntity")
|
|
145
168
|
const entities = data["entities"]
|
|
146
169
|
if (!Array.isArray(entities)) return invalid("$.entities")
|
|
@@ -172,5 +195,74 @@ export const parse = (data: unknown): Result.Result<WorldSnapshot, RestoreError>
|
|
|
172
195
|
for (const [name, members] of Object.entries(scopes)) {
|
|
173
196
|
if (!Array.isArray(members) || !members.every(isEntityId)) return invalid(`$.scopes.${name}`)
|
|
174
197
|
}
|
|
175
|
-
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 }
|
|
176
268
|
}
|
package/src/System.ts
CHANGED
|
@@ -193,6 +193,50 @@ export const writeResource = <D extends Descriptor<"resource", string, any>>(
|
|
|
193
193
|
descriptor
|
|
194
194
|
})
|
|
195
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Declares read access to a resource plus whether it changed since this
|
|
198
|
+
* system's previous completed run.
|
|
199
|
+
*/
|
|
200
|
+
export type ResourceChanged<D extends Descriptor<"resource", string, any>> = {
|
|
201
|
+
readonly mode: "changed"
|
|
202
|
+
readonly descriptor: D
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Creates a resource read that also reports `changed()`: whether a committed
|
|
207
|
+
* write left a different value since this system's previous completed run.
|
|
208
|
+
*
|
|
209
|
+
* Use it for work worth doing only when a value moved: rewriting a HUD label,
|
|
210
|
+
* rebuilding a cache from settings. Use `readResource` when the system needs
|
|
211
|
+
* the value every run anyway.
|
|
212
|
+
*
|
|
213
|
+
* A write that leaves an equal value is not a change: primitives compare
|
|
214
|
+
* with `Object.is`, plain objects and arrays by their entries, other objects
|
|
215
|
+
* by reference. This differs from `Game.Query.changed`, where every write
|
|
216
|
+
* through a component write cell counts. The first run reports `true`; a
|
|
217
|
+
* failed run (rolled back) is not a change, and a failed reader sees the same
|
|
218
|
+
* change again next run. Its own writes are not reported to the writer.
|
|
219
|
+
*
|
|
220
|
+
* Conditions (`Game.Condition.check`) cannot declare it: they own no cursor.
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```ts
|
|
224
|
+
* const SyncHud = Game.System("SyncHud", {
|
|
225
|
+
* resources: { score: Game.System.changedResource(Score) },
|
|
226
|
+
* services: { hud: Game.System.service(Hud) }
|
|
227
|
+
* }, ({ resources, services }) => {
|
|
228
|
+
* if (!resources.score.changed()) return
|
|
229
|
+
* services.hud.score.textContent = `${resources.score.get()}`
|
|
230
|
+
* })
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
export const changedResource = <D extends Descriptor<"resource", string, any>>(
|
|
234
|
+
descriptor: D
|
|
235
|
+
): ResourceChanged<D> => ({
|
|
236
|
+
mode: "changed",
|
|
237
|
+
descriptor
|
|
238
|
+
})
|
|
239
|
+
|
|
196
240
|
/**
|
|
197
241
|
* Declares read-only access to an event stream.
|
|
198
242
|
*/
|
|
@@ -314,7 +358,7 @@ export interface ThisTick {
|
|
|
314
358
|
* ```
|
|
315
359
|
*/
|
|
316
360
|
export const thisTick = <
|
|
317
|
-
Read extends EventRead<Descriptor<"event", string, any>> | ResourceRead<Descriptor<"resource", string, any>> | Query.Any<any>
|
|
361
|
+
Read extends EventRead<Descriptor<"event", string, any>> | ResourceRead<Descriptor<"resource", string, any>> | ResourceChanged<Descriptor<"resource", string, any>> | Query.Any<any>
|
|
318
362
|
>(read: Read): Read & ThisTick => ({ ...read, thisTick: true })
|
|
319
363
|
|
|
320
364
|
/**
|
|
@@ -367,7 +411,10 @@ export const readTransitionEvent = <M extends Machine.StateMachine.Any>(
|
|
|
367
411
|
/**
|
|
368
412
|
* Internal union for all supported resource access declarations.
|
|
369
413
|
*/
|
|
370
|
-
type ResourceAccess =
|
|
414
|
+
type ResourceAccess =
|
|
415
|
+
| ResourceRead<Descriptor<"resource", string, any>>
|
|
416
|
+
| ResourceChanged<Descriptor<"resource", string, any>>
|
|
417
|
+
| ResourceWrite<Descriptor<"resource", string, any>>
|
|
371
418
|
/**
|
|
372
419
|
* Internal union for all supported event access declarations.
|
|
373
420
|
*/
|
|
@@ -378,6 +425,15 @@ type EventAccess = EventRead<Descriptor<"event", string, any>> | EventWrite<Desc
|
|
|
378
425
|
*/
|
|
379
426
|
export interface ResourceReadView<T> extends ReadCell<T> {}
|
|
380
427
|
|
|
428
|
+
/**
|
|
429
|
+
* A read-only view over a resource value that also reports whether it
|
|
430
|
+
* changed since this system's previous completed run (`changedResource`).
|
|
431
|
+
*/
|
|
432
|
+
export interface ResourceChangedView<T> extends ReadCell<T> {
|
|
433
|
+
/** Whether a committed write left a different value since this system's previous completed run. `true` on the first run. */
|
|
434
|
+
changed(): boolean
|
|
435
|
+
}
|
|
436
|
+
|
|
381
437
|
/**
|
|
382
438
|
* A mutable view over a resource value.
|
|
383
439
|
*/
|
|
@@ -405,14 +461,16 @@ export interface EventReadView<T> {
|
|
|
405
461
|
* A writable event stream view.
|
|
406
462
|
*/
|
|
407
463
|
export interface EventWriteView<T> {
|
|
408
|
-
|
|
464
|
+
/** Publishes one event. Takes the readonly shape reads return, so a read event can be emitted again. */
|
|
465
|
+
emit(value: import("./Query.ts").ReadonlyValue<T>): void
|
|
409
466
|
}
|
|
410
467
|
|
|
411
468
|
/**
|
|
412
469
|
* A read-only view over one committed finite-state-machine value.
|
|
413
470
|
*/
|
|
414
471
|
export interface MachineReadView<M extends Machine.StateMachine.Any = Machine.StateMachine.Any> {
|
|
415
|
-
|
|
472
|
+
/** The committed value; `undefined` only for a sub-state whose parent is in another state. */
|
|
473
|
+
get(): Machine.CurrentValue<M>
|
|
416
474
|
is(value: Machine.StateMachine.Value<M>): boolean
|
|
417
475
|
}
|
|
418
476
|
|
|
@@ -421,20 +479,38 @@ export interface MachineReadView<M extends Machine.StateMachine.Any = Machine.St
|
|
|
421
479
|
*/
|
|
422
480
|
export interface NextMachineWriteView<M extends Machine.StateMachine.Any = Machine.StateMachine.Any> {
|
|
423
481
|
getPending(): Machine.StateMachine.Value<M> | undefined
|
|
424
|
-
|
|
425
|
-
|
|
482
|
+
/**
|
|
483
|
+
* Queues `value`, committed at the next `applyStateTransitions(...)`. For a
|
|
484
|
+
* sub-state it returns a `Result` that fails with `SubStateInactive` (and
|
|
485
|
+
* queues nothing) while the parent is in another state.
|
|
486
|
+
*
|
|
487
|
+
* One value is queued per machine: a later `set` before the marker
|
|
488
|
+
* replaces an earlier one. A `set` from a transition schedule (`onEnter`,
|
|
489
|
+
* `onExit`, `onTransition`) is committed at the following marker, while a
|
|
490
|
+
* value queued before the current marker still commits at it. A sub-state
|
|
491
|
+
* value set while its parent is still in its state but leaves it at the
|
|
492
|
+
* same marker returns ok and is dropped with the sub-state.
|
|
493
|
+
*/
|
|
494
|
+
set(value: Machine.StateMachine.Value<M>): Machine.SetResult<M>
|
|
495
|
+
/** Like `set`, but committing the current value again is skipped (no exit or enter schedules). */
|
|
496
|
+
setIfChanged(value: Machine.StateMachine.Value<M>): Machine.SetResult<M>
|
|
426
497
|
reset(): void
|
|
427
498
|
}
|
|
428
499
|
|
|
429
500
|
/**
|
|
430
501
|
* A read-only view over the last applied transition payload for one machine.
|
|
502
|
+
*
|
|
503
|
+
* Only meaningful inside transition schedules, so it is only given to
|
|
504
|
+
* systems that declare `transitions: { x: Game.System.transition(M) }`.
|
|
431
505
|
*/
|
|
432
506
|
export interface TransitionReadView<M extends Machine.StateMachine.Any = Machine.StateMachine.Any> {
|
|
433
507
|
get(): Machine.TransitionSnapshot<M>
|
|
434
508
|
}
|
|
435
509
|
|
|
436
510
|
/**
|
|
437
|
-
* A read-only event stream of committed machine transitions
|
|
511
|
+
* A read-only event stream of committed machine transitions, published by
|
|
512
|
+
* `applyStateTransitions(...)`. Like events, each reader sees the
|
|
513
|
+
* transitions published since its own previous run.
|
|
438
514
|
*/
|
|
439
515
|
export interface TransitionEventReadView<M extends Machine.StateMachine.Any = Machine.StateMachine.Any> {
|
|
440
516
|
all(): ReadonlyArray<Machine.TransitionSnapshot<M>>
|
|
@@ -593,10 +669,47 @@ export interface RelationFailureReadView<
|
|
|
593
669
|
* A runtime query handle exposed to system implementations.
|
|
594
670
|
*
|
|
595
671
|
* The handle is already typed from the query spec, so iterating it returns
|
|
596
|
-
* strongly typed entity proofs and cells.
|
|
672
|
+
* strongly typed entity proofs and cells. A query with `added` / `changed`
|
|
673
|
+
* filters only iterates (`each`, `singleOptional`): its lookups would fail
|
|
674
|
+
* for every entity that did not change since this system's previous run, so
|
|
675
|
+
* declare a second query without filters to resolve entities.
|
|
597
676
|
*/
|
|
598
|
-
export
|
|
677
|
+
export type QueryHandle<S extends Schema.Any, Q extends Query.Any> =
|
|
678
|
+
& QueryIteration<S, Q>
|
|
679
|
+
& (Q["filters"] extends readonly [] ? QueryLookups<S, Q> : FilteredQueryLookups)
|
|
680
|
+
|
|
681
|
+
/** What every query handle has: iteration over its current matches. */
|
|
682
|
+
export interface QueryIteration<S extends Schema.Any, Q extends Query.Any> {
|
|
599
683
|
each(): ReadonlyArray<QueryMatch<S, Q>>
|
|
684
|
+
/**
|
|
685
|
+
* Returns a single match when zero or one entity satisfies the query.
|
|
686
|
+
*
|
|
687
|
+
* Use this when absence is acceptable but multiplicity is still a bug. Zero
|
|
688
|
+
* matches return `ok: true` with `value: undefined`; multiple matches remain
|
|
689
|
+
* an explicit `MultipleEntities` failure.
|
|
690
|
+
*
|
|
691
|
+
* @example
|
|
692
|
+
* ```ts
|
|
693
|
+
* const player = queries.player.singleOptional()
|
|
694
|
+
* if (!player.ok || !player.value) {
|
|
695
|
+
* return
|
|
696
|
+
* }
|
|
697
|
+
*
|
|
698
|
+
* player.value.data.position.set({ x: 0, y: 0 })
|
|
699
|
+
* ```
|
|
700
|
+
*/
|
|
701
|
+
singleOptional(): Result.Result<QueryMatch<S, Q> | undefined, Query.MultipleEntitiesError>
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
type FilteredLookupMessage = "lookups need a query without added/changed filters: declare a second query for resolving entities"
|
|
705
|
+
|
|
706
|
+
/** The lookups of a query with filters, each replaced by the message naming the fix. */
|
|
707
|
+
export type FilteredQueryLookups = {
|
|
708
|
+
readonly [K in keyof QueryLookups<Schema.Any, Query.Any>]: (this: FilteredLookupMessage, ...args: ReadonlyArray<unknown>) => never
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** Lookups of one entity or one hierarchy through a query without `added` / `changed` filters. */
|
|
712
|
+
export interface QueryLookups<S extends Schema.Any, Q extends Query.Any> {
|
|
600
713
|
/**
|
|
601
714
|
* Retrieves the match for one specific entity id when it satisfies the query.
|
|
602
715
|
*
|
|
@@ -643,24 +756,6 @@ export interface QueryHandle<S extends Schema.Any, Q extends Query.Any> {
|
|
|
643
756
|
* Returns a single match when exactly one entity satisfies the query.
|
|
644
757
|
*/
|
|
645
758
|
single(): Result.Result<QueryMatch<S, Q>, Query.SingleError>
|
|
646
|
-
/**
|
|
647
|
-
* Returns a single match when zero or one entity satisfies the query.
|
|
648
|
-
*
|
|
649
|
-
* Use this when absence is acceptable but multiplicity is still a bug. Zero
|
|
650
|
-
* matches return `ok: true` with `value: undefined`; multiple matches remain
|
|
651
|
-
* an explicit `MultipleEntities` failure.
|
|
652
|
-
*
|
|
653
|
-
* @example
|
|
654
|
-
* ```ts
|
|
655
|
-
* const player = queries.player.singleOptional()
|
|
656
|
-
* if (!player.ok || !player.value) {
|
|
657
|
-
* return
|
|
658
|
-
* }
|
|
659
|
-
*
|
|
660
|
-
* player.value.data.position.set({ x: 0, y: 0 })
|
|
661
|
-
* ```
|
|
662
|
-
*/
|
|
663
|
-
singleOptional(): Result.Result<QueryMatch<S, Q> | undefined, Query.MultipleEntitiesError>
|
|
664
759
|
}
|
|
665
760
|
|
|
666
761
|
/**
|
|
@@ -791,10 +886,36 @@ export interface SystemAccessInput {
|
|
|
791
886
|
*/
|
|
792
887
|
export type SystemAccessSpec = SystemAccessInput
|
|
793
888
|
|
|
794
|
-
/** Rejects unknown system access categories at a bound constructor. */
|
|
889
|
+
/** Rejects unknown system access categories, and redundant resource and event slots, at a bound constructor. */
|
|
795
890
|
export type ExactAccess<Access extends SystemAccessInput> = Access & {
|
|
796
891
|
readonly [Key in Exclude<keyof Access, keyof SystemAccessInput>]: never
|
|
797
|
-
}
|
|
892
|
+
} & RedundantSlotsFix<Access, "resources", "read"> & RedundantSlotsFix<Access, "events", never>
|
|
893
|
+
|
|
894
|
+
type SlotOf<A> = A extends { readonly mode: infer Mode extends string; readonly descriptor: { readonly name: infer Name extends string } } ? readonly [Mode, Name] : never
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Slots of `Record` that repeat another slot: the same descriptor in the same
|
|
898
|
+
* mode, or in `ReadMode` next to any other slot of it (a write or changed
|
|
899
|
+
* view already reads).
|
|
900
|
+
*/
|
|
901
|
+
type RedundantSlots<Record, ReadMode extends string> = {
|
|
902
|
+
readonly [K in keyof Record]: SlotOf<Record[K]> extends readonly [infer Mode, infer Name]
|
|
903
|
+
? [Extract<SlotOf<Omit<Record, K>[keyof Omit<Record, K>]>, readonly [Mode extends ReadMode ? string : Mode, Name]>] extends [never] ? never : K
|
|
904
|
+
: never
|
|
905
|
+
}[keyof Record]
|
|
906
|
+
|
|
907
|
+
type RedundantSlotsFix<Access, Category extends "resources" | "events", ReadMode extends string> =
|
|
908
|
+
Access extends { readonly [C in Category]?: infer Record }
|
|
909
|
+
? [RedundantSlots<Record, ReadMode>] extends [never]
|
|
910
|
+
? unknown
|
|
911
|
+
: {
|
|
912
|
+
readonly [C in Category]: {
|
|
913
|
+
readonly [K in RedundantSlots<Record, ReadMode>]: {
|
|
914
|
+
readonly __fixSystem__: `${K & string} repeats ${Category extends "resources" ? "a resource" : "an event"} another slot of this system declares: keep one slot (a write or changed view reads too)`
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
: unknown
|
|
798
919
|
|
|
799
920
|
type AccessField<
|
|
800
921
|
Access,
|
|
@@ -847,6 +968,7 @@ export type AnySystemSpec = SystemSpec<any, any, any>
|
|
|
847
968
|
type ResourceContext<Spec extends AnySystemSpec> = {
|
|
848
969
|
readonly [K in keyof Spec["resources"]]:
|
|
849
970
|
Spec["resources"][K] extends ResourceRead<infer D> ? ResourceReadView<Descriptor.Value<D>>
|
|
971
|
+
: Spec["resources"][K] extends ResourceChanged<infer D> ? ResourceChangedView<Descriptor.Value<D>>
|
|
850
972
|
: Spec["resources"][K] extends ResourceWrite<infer D> ? ResourceWriteView<D>
|
|
851
973
|
: never
|
|
852
974
|
}
|
package/src/index.ts
CHANGED
|
@@ -33,6 +33,10 @@ export * as Entity from "./Entity.ts"
|
|
|
33
33
|
* Entity lifetime scopes used by scene and level ownership patterns.
|
|
34
34
|
*/
|
|
35
35
|
export * as EntityScope from "./EntityScope.ts"
|
|
36
|
+
/**
|
|
37
|
+
* Fixed-step timing for any host: clamping, catch-up limits, interpolation.
|
|
38
|
+
*/
|
|
39
|
+
export * as FixedLoop from "./FixedLoop.ts"
|
|
36
40
|
/**
|
|
37
41
|
* The host's wall clock as an injected contract (`standard`, `test()`).
|
|
38
42
|
*/
|
package/src/internal/cells.ts
CHANGED
|
@@ -63,38 +63,64 @@ const withTransition = (cell: WriteCellBase<unknown>, component: string) => Obje
|
|
|
63
63
|
}
|
|
64
64
|
})
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* A required slot is empty only when its match outlived the run that produced
|
|
68
|
+
* it: the entity was despawned, or lost the component. Reading would hand out
|
|
69
|
+
* the internal marker as a value, and writing would put the component back
|
|
70
|
+
* without the insert path, so both fail loudly instead.
|
|
71
|
+
*/
|
|
72
|
+
const staleMatch = (record: EntityRecord, ordinal: number): Error =>
|
|
73
|
+
new Error(
|
|
74
|
+
`${record.nameAt(ordinal)} was used through a query match after ${record.alive ? "its entity lost it" : "its entity was despawned"}: `
|
|
75
|
+
+ "a match is valid during the system run that produced it; keep the entity id or a handle and resolve it again with queries.x.get(id) or queries.x.getHandle(handle)"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
/** Kept out of `get` so the hot read stays small enough to inline. */
|
|
80
|
+
const staleRead = (record: EntityRecord, ordinal: number): never => {
|
|
81
|
+
throw staleMatch(record, ordinal)
|
|
82
|
+
}
|
|
83
|
+
|
|
66
84
|
class ComponentReadCell {
|
|
67
|
-
|
|
85
|
+
// The record's values array, held directly: one load per read instead of
|
|
86
|
+
// two pays for the stale check. Records never replace their array.
|
|
87
|
+
readonly values: Array<unknown>
|
|
68
88
|
readonly ordinal: number
|
|
89
|
+
readonly record: EntityRecord
|
|
69
90
|
|
|
70
91
|
constructor(record: EntityRecord, ordinal: number) {
|
|
71
|
-
this.
|
|
92
|
+
this.values = record.values
|
|
72
93
|
this.ordinal = ordinal
|
|
94
|
+
this.record = record
|
|
73
95
|
}
|
|
74
96
|
|
|
75
97
|
get(): unknown {
|
|
76
|
-
|
|
98
|
+
const value = this.values[this.ordinal]
|
|
99
|
+
return value === ABSENT ? staleRead(this.record, this.ordinal) : value
|
|
77
100
|
}
|
|
78
101
|
}
|
|
79
102
|
|
|
80
103
|
class ComponentWriteCell extends WriteCellBase<unknown> {
|
|
81
|
-
readonly
|
|
104
|
+
readonly values: Array<unknown>
|
|
82
105
|
readonly ordinal: number
|
|
106
|
+
readonly record: EntityRecord
|
|
83
107
|
readonly world: World
|
|
84
108
|
|
|
85
109
|
constructor(record: EntityRecord, ordinal: number, world: World) {
|
|
86
110
|
super()
|
|
87
|
-
this.
|
|
111
|
+
this.values = record.values
|
|
88
112
|
this.ordinal = ordinal
|
|
113
|
+
this.record = record
|
|
89
114
|
this.world = world
|
|
90
115
|
}
|
|
91
116
|
|
|
92
117
|
get(): unknown {
|
|
93
|
-
|
|
118
|
+
const value = this.values[this.ordinal]
|
|
119
|
+
return value === ABSENT ? staleRead(this.record, this.ordinal) : value
|
|
94
120
|
}
|
|
95
121
|
|
|
96
122
|
set(value: unknown): void {
|
|
97
|
-
this.world.setComponentValue(this.record, this.ordinal, value)
|
|
123
|
+
if (!this.world.setComponentValue(this.record, this.ordinal, value)) staleRead(this.record, this.ordinal)
|
|
98
124
|
}
|
|
99
125
|
}
|
|
100
126
|
|
|
@@ -115,6 +141,11 @@ class OptionalComponentCell {
|
|
|
115
141
|
get(): unknown {
|
|
116
142
|
return this.record.values[this.ordinal]
|
|
117
143
|
}
|
|
144
|
+
|
|
145
|
+
value(): unknown {
|
|
146
|
+
const value = this.record.values[this.ordinal]
|
|
147
|
+
return value === ABSENT ? undefined : value
|
|
148
|
+
}
|
|
118
149
|
}
|
|
119
150
|
|
|
120
151
|
class StoreReadCell {
|
|
@@ -167,7 +198,7 @@ export const componentWrite = (
|
|
|
167
198
|
return entityState === undefined ? constructed : withTransition(constructed, entityState)
|
|
168
199
|
}
|
|
169
200
|
|
|
170
|
-
export const componentOptional = (record: EntityRecord, ordinal: number): { readonly present: boolean; get(): unknown } =>
|
|
201
|
+
export const componentOptional = (record: EntityRecord, ordinal: number): { readonly present: boolean; get(): unknown; value(): unknown } =>
|
|
171
202
|
new OptionalComponentCell(record, ordinal)
|
|
172
203
|
|
|
173
204
|
export const storeRead = (store: Map<symbol, unknown>, key: symbol): { get(): unknown } =>
|
|
@@ -198,9 +229,10 @@ export const derivedRead = <T>(read: () => T): { get(): T } => ({ get: read })
|
|
|
198
229
|
export const derivedOptional = <T>(
|
|
199
230
|
isPresent: () => boolean,
|
|
200
231
|
read: () => T
|
|
201
|
-
): { readonly present: boolean; get(): T } => ({
|
|
232
|
+
): { readonly present: boolean; get(): T; value(): T | undefined } => ({
|
|
202
233
|
get present() {
|
|
203
234
|
return isPresent()
|
|
204
235
|
},
|
|
205
|
-
get: read
|
|
236
|
+
get: read,
|
|
237
|
+
value: () => (isPresent() ? read() : undefined)
|
|
206
238
|
})
|