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