@typeonce/ritmo 0.6.0 → 0.7.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 +2 -0
- package/dist/Brand.d.ts +9 -4
- package/dist/Brand.d.ts.map +1 -1
- package/dist/Brand.js +9 -4
- package/dist/Brand.js.map +1 -1
- package/dist/Clock.d.ts +186 -0
- package/dist/Clock.d.ts.map +1 -0
- package/dist/Clock.js +111 -0
- package/dist/Clock.js.map +1 -0
- package/dist/Command.d.ts +157 -33
- package/dist/Command.d.ts.map +1 -1
- package/dist/Command.js +49 -15
- package/dist/Command.js.map +1 -1
- package/dist/Debug.d.ts +104 -12
- package/dist/Debug.d.ts.map +1 -1
- package/dist/Debug.js.map +1 -1
- package/dist/Decode.d.ts +1 -1
- package/dist/Decode.js +1 -1
- package/dist/Descriptor.d.ts +31 -21
- package/dist/Descriptor.d.ts.map +1 -1
- package/dist/Descriptor.js +21 -12
- package/dist/Descriptor.js.map +1 -1
- package/dist/Entity.d.ts +14 -5
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js +2 -2
- package/dist/Entity.js.map +1 -1
- package/dist/EntityScope.d.ts +6 -0
- package/dist/EntityScope.d.ts.map +1 -1
- package/dist/EntityScope.js +6 -0
- package/dist/EntityScope.js.map +1 -1
- package/dist/HostClock.d.ts +49 -0
- package/dist/HostClock.d.ts.map +1 -0
- package/dist/HostClock.js +46 -0
- package/dist/HostClock.js.map +1 -0
- package/dist/Inspector.d.ts +7 -1
- package/dist/Inspector.d.ts.map +1 -1
- package/dist/Inspector.js.map +1 -1
- package/dist/Machine.d.ts +12 -3
- 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 +28 -24
- package/dist/Query.d.ts.map +1 -1
- package/dist/Query.js +0 -24
- package/dist/Query.js.map +1 -1
- package/dist/Runtime.d.ts +36 -16
- package/dist/Runtime.d.ts.map +1 -1
- package/dist/Runtime.js +230 -67
- package/dist/Runtime.js.map +1 -1
- package/dist/Schedule.d.ts +217 -10
- package/dist/Schedule.d.ts.map +1 -1
- package/dist/Schedule.js +5 -0
- package/dist/Schedule.js.map +1 -1
- package/dist/Schema.d.ts +61 -25
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +0 -8
- package/dist/Schema.js.map +1 -1
- package/dist/System.d.ts +149 -52
- package/dist/System.d.ts.map +1 -1
- package/dist/System.js +52 -6
- package/dist/System.js.map +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -7
- package/dist/index.js.map +1 -1
- package/dist/internal/cells.d.ts +3 -3
- package/dist/internal/cells.d.ts.map +1 -1
- package/dist/internal/cells.js +9 -11
- package/dist/internal/cells.js.map +1 -1
- package/dist/internal/debug.d.ts +6 -0
- package/dist/internal/debug.d.ts.map +1 -1
- package/dist/internal/debug.js +95 -1
- package/dist/internal/debug.js.map +1 -1
- package/dist/internal/game.d.ts.map +1 -1
- package/dist/internal/game.js +9 -2
- package/dist/internal/game.js.map +1 -1
- package/dist/internal/order.d.ts +10 -0
- package/dist/internal/order.d.ts.map +1 -0
- package/dist/internal/order.js +120 -0
- package/dist/internal/order.js.map +1 -0
- package/dist/internal/queries.js +2 -2
- package/dist/internal/queries.js.map +1 -1
- package/package.json +1 -1
- package/src/Brand.ts +9 -4
- package/src/Clock.ts +311 -0
- package/src/Command.ts +271 -73
- package/src/Debug.ts +111 -10
- package/src/Decode.ts +1 -1
- package/src/Descriptor.ts +37 -27
- package/src/Entity.ts +15 -5
- package/src/EntityScope.ts +6 -0
- package/src/HostClock.ts +64 -0
- package/src/Inspector.ts +7 -2
- package/src/Machine.ts +13 -3
- package/src/Query.ts +41 -33
- package/src/Runtime.ts +273 -91
- package/src/Schedule.ts +380 -13
- package/src/Schema.ts +70 -41
- package/src/System.ts +174 -88
- package/src/index.ts +8 -7
- package/src/internal/cells.ts +10 -12
- package/src/internal/debug.ts +104 -4
- package/src/internal/game.ts +11 -3
- package/src/internal/order.ts +140 -0
- package/src/internal/queries.ts +3 -3
- package/dist/Definition.d.ts +0 -99
- package/dist/Definition.d.ts.map +0 -1
- package/dist/Definition.js +0 -68
- package/dist/Definition.js.map +0 -1
- package/dist/Feature.d.ts +0 -106
- package/dist/Feature.d.ts.map +0 -1
- package/dist/Feature.js +0 -87
- package/dist/Feature.js.map +0 -1
- package/dist/Fx.d.ts +0 -128
- package/dist/Fx.d.ts.map +0 -1
- package/dist/Fx.js +0 -112
- package/dist/Fx.js.map +0 -1
- package/dist/internal/definition.d.ts +0 -17
- package/dist/internal/definition.d.ts.map +0 -1
- package/dist/internal/definition.js +0 -25
- package/dist/internal/definition.js.map +0 -1
- package/src/Definition.ts +0 -106
- package/src/Feature.ts +0 -241
- package/src/Fx.ts +0 -176
- package/src/internal/definition.ts +0 -56
package/src/internal/debug.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type * as Machine from "../Machine.ts"
|
|
|
11
11
|
import type * as Query from "../Query.ts"
|
|
12
12
|
import type * as Relation from "../Relation.ts"
|
|
13
13
|
import * as Schedule from "../Schedule.ts"
|
|
14
|
+
import * as OrderInternal from "./order.ts"
|
|
14
15
|
import type { Schema } from "../Schema.ts"
|
|
15
16
|
import type { SystemDefinition } from "../System.ts"
|
|
16
17
|
|
|
@@ -114,6 +115,30 @@ const describeQuery = (slot: string, query: Query.Query.Any<any>): Debug.QueryDe
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
type Mode = { readonly mode: "read" | "write"; readonly descriptor: Descriptor.Any }
|
|
118
|
+
type CommandAccess = { readonly permissions: ReadonlyArray<{ readonly permission: keyof Debug.CommandPermissions; readonly values: ReadonlyArray<{ readonly name: string }> }> }
|
|
119
|
+
|
|
120
|
+
const permissionsOf = (access: CommandAccess | undefined): Debug.CommandPermissions => {
|
|
121
|
+
const of = (kind: keyof Debug.CommandPermissions) =>
|
|
122
|
+
[...new Set((access?.permissions ?? []).filter((p) => p.permission === kind).flatMap((p) => p.values.map((value) => value.name)))]
|
|
123
|
+
return {
|
|
124
|
+
spawns: of("spawns"),
|
|
125
|
+
inserts: of("inserts"),
|
|
126
|
+
removes: of("removes"),
|
|
127
|
+
despawns: of("despawns"),
|
|
128
|
+
despawnsScope: of("despawnsScope"),
|
|
129
|
+
relates: of("relates")
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const permissionVerb: Record<keyof Debug.CommandPermissions, string> = {
|
|
134
|
+
spawns: "spawn",
|
|
135
|
+
inserts: "insert",
|
|
136
|
+
removes: "remove",
|
|
137
|
+
despawns: "despawn",
|
|
138
|
+
despawnsScope: "despawn the scope",
|
|
139
|
+
relates: "relate"
|
|
140
|
+
}
|
|
141
|
+
type ThisTickAccess = Mode & { readonly thisTick?: true }
|
|
117
142
|
|
|
118
143
|
export const describeSystem = (system: AnySystem, placements: ReadonlyArray<string>): Debug.SystemDescription => {
|
|
119
144
|
const spec = system.spec
|
|
@@ -148,7 +173,16 @@ export const describeSystem = (system: AnySystem, placements: ReadonlyArray<stri
|
|
|
148
173
|
removed: names(spec.removed as Record<string, { readonly descriptor: Descriptor.Any }>, (access) => access.descriptor.name),
|
|
149
174
|
despawned: Object.keys(spec.despawned).length > 0,
|
|
150
175
|
relationFailures: names(spec.relationFailures as Record<string, { readonly relation: Relation.Relation.Any }>, (access) => access.relation.name),
|
|
176
|
+
thisTick: [
|
|
177
|
+
...[...Object.values(spec.events as Record<string, ThisTickAccess>), ...Object.values(spec.resources as Record<string, ThisTickAccess>)]
|
|
178
|
+
.filter((access) => access.thisTick === true)
|
|
179
|
+
.map((access) => access.descriptor.name),
|
|
180
|
+
...Object.entries(spec.queries as Record<string, { readonly thisTick?: true }>)
|
|
181
|
+
.filter(([, query]) => query.thisTick === true)
|
|
182
|
+
.map(([slot]) => `queries.${slot}`)
|
|
183
|
+
],
|
|
151
184
|
services: names(spec.services as Record<string, { readonly descriptor: Descriptor.Any }>, (access) => access.descriptor.name),
|
|
185
|
+
commands: permissionsOf(spec.commands as CommandAccess | undefined),
|
|
152
186
|
when: (spec.when as ReadonlyArray<Machine.Condition>).map(renderCondition),
|
|
153
187
|
pauseWhen: (spec.pauseWhen as ReadonlyArray<Machine.Condition>).map(renderCondition),
|
|
154
188
|
local: spec.local !== undefined
|
|
@@ -228,6 +262,12 @@ export interface DescribeInput {
|
|
|
228
262
|
readonly currentMachine: (machine: Machine.StateMachine.Any) => Machine.StateValue | undefined
|
|
229
263
|
readonly providedServices: ReadonlyArray<string>
|
|
230
264
|
readonly externallyWrittenEvents: ReadonlySet<string>
|
|
265
|
+
/** Relations commands have tried to relate or reorder, by the queuing systems. */
|
|
266
|
+
readonly relationMutations: ReadonlyMap<string, ReadonlySet<string>>
|
|
267
|
+
/** What each system's applied commands did, as `kind:name`; see `Runtime`. */
|
|
268
|
+
readonly commandUses: ReadonlyMap<string, ReadonlySet<string>>
|
|
269
|
+
/** Systems that ran in this session. */
|
|
270
|
+
readonly ranSystems: ReadonlySet<string>
|
|
231
271
|
readonly hasResource: (descriptor: Descriptor.Any) => boolean
|
|
232
272
|
readonly schedules: ReadonlyArray<readonly [string, AnySchedule]>
|
|
233
273
|
}
|
|
@@ -269,6 +309,7 @@ export const describe = (input: DescribeInput): Debug.Description => {
|
|
|
269
309
|
}
|
|
270
310
|
}
|
|
271
311
|
for (const name of system.removed) components.read(name, system.name)
|
|
312
|
+
for (const name of [...system.commands.spawns, ...system.commands.inserts, ...system.commands.removes]) components.write(name, system.name)
|
|
272
313
|
for (const name of system.resources.reads) resources.read(name, system.name)
|
|
273
314
|
for (const name of system.resources.writes) resources.write(name, system.name)
|
|
274
315
|
for (const name of system.events.reads) events.read(name, system.name)
|
|
@@ -283,7 +324,7 @@ export const describe = (input: DescribeInput): Debug.Description => {
|
|
|
283
324
|
const appliesTransitions = schedules.some(([, schedule]) =>
|
|
284
325
|
schedule.steps.some((step) => !Schedule.isSystemStep(step) && step.kind === "applyStateTransitions"))
|
|
285
326
|
|
|
286
|
-
const lints: Array<Debug.Lint
|
|
327
|
+
const lints: Array<Omit<Debug.Lint, "fix">> = []
|
|
287
328
|
if (schedules.length > 0) {
|
|
288
329
|
for (const entry of events.list()) {
|
|
289
330
|
if (entry.writers.length > 0 && entry.readers.length === 0) {
|
|
@@ -313,7 +354,44 @@ export const describe = (input: DescribeInput): Debug.Description => {
|
|
|
313
354
|
})
|
|
314
355
|
}
|
|
315
356
|
}
|
|
357
|
+
const failureReaders = new Set(systems.flatMap((system) => system.relationFailures))
|
|
358
|
+
// Declared `relates` permissions, and relations that commands were seen to relate (drafts relate without one).
|
|
359
|
+
const relaters = new Map<string, Set<string>>()
|
|
360
|
+
for (const system of systems) {
|
|
361
|
+
for (const name of system.commands.relates) relaters.set(name, new Set([...(relaters.get(name) ?? []), system.name]))
|
|
362
|
+
}
|
|
363
|
+
for (const [name, queuers] of input.relationMutations) relaters.set(name, new Set([...(relaters.get(name) ?? []), ...queuers]))
|
|
364
|
+
for (const [name, queuers] of relaters) {
|
|
365
|
+
if (failureReaders.has(name)) continue
|
|
366
|
+
lints.push({
|
|
367
|
+
severity: "warning",
|
|
368
|
+
code: "relation-failure-never-read",
|
|
369
|
+
subject: name,
|
|
370
|
+
message: `${name} is related by commands from ${[...queuers].sort().join(", ")}, but no described system reads its failures (Game.System.readRelationFailures(${name})), so a failed relate or reorder is dropped silently`
|
|
371
|
+
})
|
|
372
|
+
}
|
|
316
373
|
lints.push(...readBeforeWriteLints(schedules, described, input.externallyWrittenEvents))
|
|
374
|
+
// Each named schedule is checked as one tick; `tick` checks this at compile time.
|
|
375
|
+
for (const [name, schedule] of input.schedules) {
|
|
376
|
+
for (const issue of OrderInternal.tickOrderIssues(schedule.steps)) {
|
|
377
|
+
lints.push({ severity: "warning", code: "this-tick-order", subject: issue.data, message: `in ${name}, ${issue.message}` })
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Observed with debug: only systems that ran, so an unplayed session reports nothing.
|
|
381
|
+
for (const system of systems) {
|
|
382
|
+
if (!input.ranSystems.has(system.name)) continue
|
|
383
|
+
const uses = input.commandUses.get(system.name) ?? new Set<string>()
|
|
384
|
+
for (const kind of Object.keys(permissionVerb) as Array<keyof Debug.CommandPermissions>) {
|
|
385
|
+
const unused = system.commands[kind].filter((name) => !uses.has(`${kind}:${name}`))
|
|
386
|
+
if (unused.length === 0) continue
|
|
387
|
+
lints.push({
|
|
388
|
+
severity: "info",
|
|
389
|
+
code: "command-permission-unused",
|
|
390
|
+
subject: system.name,
|
|
391
|
+
message: `${system.name} may ${permissionVerb[kind]} ${unused.join(", ")} but never did in this session`
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
}
|
|
317
395
|
for (const entry of components.list()) {
|
|
318
396
|
if (entry.writers.length > 0 && entry.readers.every((reader) => entry.writers.includes(reader))) {
|
|
319
397
|
lints.push({
|
|
@@ -355,7 +433,29 @@ export const describe = (input: DescribeInput): Debug.Description => {
|
|
|
355
433
|
resources: resources.list(),
|
|
356
434
|
events: events.list()
|
|
357
435
|
},
|
|
358
|
-
lints
|
|
436
|
+
lints: lints.map((lint) => ({ ...lint, fix: fixFor(lint.code, lint.subject) }))
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** The fix each lint names, by code. */
|
|
441
|
+
const fixFor = (code: Debug.Lint["code"], subject: string): string => {
|
|
442
|
+
switch (code) {
|
|
443
|
+
case "event-never-read":
|
|
444
|
+
return `read ${subject} in a system with Game.System.readEvent, or stop emitting it; if only a render schedule reads it, pass that schedule as describe-only`
|
|
445
|
+
case "event-never-written":
|
|
446
|
+
return `emit ${subject} from a system with Game.System.writeEvent, or stop reading it; if the host sends it with runtime.send, declare it (debug.declareHostEvents, or hostEvents in a Session or lint target)`
|
|
447
|
+
case "next-state-never-applied":
|
|
448
|
+
return `add Game.Schedule.applyStateTransitions(...) to a schedule that runs after the system that queues ${subject}`
|
|
449
|
+
case "relation-failure-never-read":
|
|
450
|
+
return `read the failures with Game.System.readRelationFailures(${subject}) in a system and handle them`
|
|
451
|
+
case "this-tick-order":
|
|
452
|
+
return "move the writer before the reader in the schedule (the message names both), or read without Game.System.thisTick"
|
|
453
|
+
case "component-never-read":
|
|
454
|
+
return `fine if ${subject} is read outside the described schedules (rendering, inspectors); otherwise stop writing it`
|
|
455
|
+
case "command-permission-unused":
|
|
456
|
+
return `fine if the session did not reach the case where ${subject} uses it; otherwise drop the permission from its Game.System.commands(...)`
|
|
457
|
+
case "read-before-write":
|
|
458
|
+
return "fine when a one-run lag is intended (input before movement); otherwise move the reader after the writer, or read with Game.System.thisTick to make it an error"
|
|
359
459
|
}
|
|
360
460
|
}
|
|
361
461
|
|
|
@@ -380,8 +480,8 @@ const readBeforeWriteLints = (
|
|
|
380
480
|
schedules: ReadonlyArray<readonly [string, AnySchedule]>,
|
|
381
481
|
described: ReadonlyMap<AnySystem, Debug.SystemDescription>,
|
|
382
482
|
externallyWrittenEvents: ReadonlySet<string>
|
|
383
|
-
): Array<Debug.Lint
|
|
384
|
-
const lints: Array<Debug.Lint
|
|
483
|
+
): Array<Omit<Debug.Lint, "fix">> => {
|
|
484
|
+
const lints: Array<Omit<Debug.Lint, "fix">> = []
|
|
385
485
|
for (const [name, schedule] of schedules) {
|
|
386
486
|
const steps = schedule.steps.flatMap((step) =>
|
|
387
487
|
Schedule.isSystemStep(step) ? [{ system: described.get(step as AnySystem)!, accesses: accessesOf(described.get(step as AnySystem)!) }] : [])
|
package/src/internal/game.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* module functions; the precise public types live on `Schema.Game`.
|
|
6
6
|
*/
|
|
7
7
|
import * as Command from "../Command.ts"
|
|
8
|
+
import type * as Debug from "../Debug.ts"
|
|
8
9
|
import * as Condition from "../Condition.ts"
|
|
9
10
|
import * as Entity from "../Entity.ts"
|
|
10
11
|
import * as EntityScope from "../EntityScope.ts"
|
|
@@ -57,9 +58,8 @@ export const makeGame = <S extends Schema.Any, Root>(schema: S, _root: Root): Sc
|
|
|
57
58
|
}),
|
|
58
59
|
Command: {
|
|
59
60
|
spawn: Command.spawn,
|
|
60
|
-
|
|
61
|
+
extend: Command.extend,
|
|
61
62
|
entry: Command.entry,
|
|
62
|
-
entryResult: Command.entryResult,
|
|
63
63
|
entryRaw: Command.entryRaw,
|
|
64
64
|
relate: Command.relate
|
|
65
65
|
},
|
|
@@ -81,6 +81,14 @@ export const makeGame = <S extends Schema.Any, Root>(schema: S, _root: Root): Sc
|
|
|
81
81
|
writeResource: System.writeResource,
|
|
82
82
|
readEvent: System.readEvent,
|
|
83
83
|
writeEvent: System.writeEvent,
|
|
84
|
+
thisTick: System.thisTick,
|
|
85
|
+
commands: Command.commands,
|
|
86
|
+
spawns: Command.spawns,
|
|
87
|
+
inserts: Command.inserts,
|
|
88
|
+
removes: Command.removes,
|
|
89
|
+
despawns: Command.despawns,
|
|
90
|
+
despawnsScope: Command.despawnsScope,
|
|
91
|
+
relates: Command.relates,
|
|
84
92
|
service: System.service,
|
|
85
93
|
machine: System.machine,
|
|
86
94
|
nextState: System.nextState,
|
|
@@ -117,7 +125,7 @@ export const makeGame = <S extends Schema.Any, Root>(schema: S, _root: Root): Sc
|
|
|
117
125
|
readonly services: Runtime.RuntimeServices<any>
|
|
118
126
|
readonly resources?: object
|
|
119
127
|
readonly machines?: Runtime.RuntimeMachines<any>
|
|
120
|
-
readonly debug?:
|
|
128
|
+
readonly debug?: Debug.Option
|
|
121
129
|
}) => Runtime.make({ schema, ...options, machineDefinitions: definedMachines } as never),
|
|
122
130
|
service: Runtime.service,
|
|
123
131
|
services: Runtime.services,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runtime form of the `Game.System.thisTick` check, for erased schedules
|
|
3
|
+
* (`tryTick`) and the `this-tick-order` lint. The compile-time form lives in
|
|
4
|
+
* `Schedule.ts` (`TickOrderErrors`) and produces the same messages.
|
|
5
|
+
*/
|
|
6
|
+
import type { Descriptor } from "../Descriptor.ts"
|
|
7
|
+
import * as Schedule from "../Schedule.ts"
|
|
8
|
+
import type { SystemDefinition } from "../System.ts"
|
|
9
|
+
|
|
10
|
+
type AnySystem = SystemDefinition<any, any, any, any, any>
|
|
11
|
+
type OrderIssue = Schedule.OrderIssue
|
|
12
|
+
type Access = { readonly mode: "read" | "write"; readonly descriptor: Descriptor.Any; readonly thisTick?: true }
|
|
13
|
+
type QueryAccess = {
|
|
14
|
+
readonly thisTick?: true
|
|
15
|
+
readonly selection: Record<string, { readonly mode?: string; readonly descriptor?: Descriptor.Any }>
|
|
16
|
+
readonly with: ReadonlyArray<Descriptor.Any>
|
|
17
|
+
readonly without: ReadonlyArray<Descriptor.Any>
|
|
18
|
+
readonly filters: ReadonlyArray<{ readonly descriptor?: Descriptor.Any }>
|
|
19
|
+
}
|
|
20
|
+
type Permission = { readonly permission: string; readonly values: ReadonlyArray<{ readonly name: string }> }
|
|
21
|
+
|
|
22
|
+
/** One position of a tick: a system, or a point where queued commands are applied. */
|
|
23
|
+
type Item = { readonly kind: "system"; readonly system: AnySystem } | { readonly kind: "flush" }
|
|
24
|
+
|
|
25
|
+
const accesses = (system: AnySystem): ReadonlyArray<Access> => [
|
|
26
|
+
...Object.values(system.spec.events as Record<string, Access>),
|
|
27
|
+
...Object.values(system.spec.resources as Record<string, Access>)
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const sameData = (left: Descriptor.Any, right: Descriptor.Any): boolean =>
|
|
31
|
+
left.kind === right.kind && left.name === right.name
|
|
32
|
+
|
|
33
|
+
const sameSystem = (left: AnySystem, right: AnySystem): boolean => (left.base ?? left) === (right.base ?? right)
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The positions of a tick, flattened: an `applyStateTransitions(...)` step
|
|
37
|
+
* applies queued commands, then runs every schedule of its bundle (as if
|
|
38
|
+
* each may run), markers inside them included.
|
|
39
|
+
*/
|
|
40
|
+
const flatten = (steps: ReadonlyArray<Schedule.ScheduleStep>): Array<Item> =>
|
|
41
|
+
steps.flatMap((step): Array<Item> => {
|
|
42
|
+
if (Schedule.isSystemStep(step)) return [{ kind: "system", system: step as AnySystem }]
|
|
43
|
+
if (step.kind === "applyDeferred") return [{ kind: "flush" }]
|
|
44
|
+
const bundle = step.bundle as { readonly entries: ReadonlyArray<{ readonly steps: ReadonlyArray<Schedule.ScheduleStep> }> } | undefined
|
|
45
|
+
return [{ kind: "flush" }, ...(bundle?.entries ?? []).flatMap((entry) => flatten(entry.steps))]
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const permissionNames = (system: AnySystem, kind: string): Set<string> => {
|
|
49
|
+
const commands = system.spec.commands as { readonly permissions: ReadonlyArray<Permission> } | undefined
|
|
50
|
+
return new Set((commands?.permissions ?? []).filter((p) => p.permission === kind).flatMap((p) => p.values.map((value) => value.name)))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether a system's declared commands can change what a query sees: spawns
|
|
55
|
+
* covering every component it requires, or inserts / removes of any
|
|
56
|
+
* component it names. Despawns never count.
|
|
57
|
+
*/
|
|
58
|
+
const changesQuery = (system: AnySystem, query: QueryAccess): boolean => {
|
|
59
|
+
const slots = Object.values(query.selection)
|
|
60
|
+
const required = [
|
|
61
|
+
...slots.filter((slot) => slot.mode === "read" || slot.mode === "write").flatMap((slot) => slot.descriptor ? [slot.descriptor.name] : []),
|
|
62
|
+
...query.with.map((d) => d.name)
|
|
63
|
+
]
|
|
64
|
+
const named = new Set([
|
|
65
|
+
...slots.flatMap((slot) => slot.descriptor ? [slot.descriptor.name] : []),
|
|
66
|
+
...query.with.map((d) => d.name),
|
|
67
|
+
...query.without.map((d) => d.name),
|
|
68
|
+
...query.filters.flatMap((filter) => filter.descriptor ? [filter.descriptor.name] : [])
|
|
69
|
+
])
|
|
70
|
+
const changes = [...permissionNames(system, "inserts"), ...permissionNames(system, "removes")]
|
|
71
|
+
if (changes.some((name) => named.has(name))) return true
|
|
72
|
+
const spawns = permissionNames(system, "spawns")
|
|
73
|
+
return spawns.size > 0 && required.every((name) => spawns.has(name))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Checks the systems of one tick, in the order they run. A read with no
|
|
78
|
+
* writer in the tick is fine (a reused reader); `event-never-written` covers
|
|
79
|
+
* a writer missing everywhere.
|
|
80
|
+
*/
|
|
81
|
+
export const tickOrderIssues = (steps: ReadonlyArray<Schedule.ScheduleStep>): ReadonlyArray<OrderIssue> => {
|
|
82
|
+
// A schedule's steps never change, so `tryTick` checks each schedule once.
|
|
83
|
+
const cached = checked.get(steps)
|
|
84
|
+
if (cached !== undefined) return cached
|
|
85
|
+
const issues = computeIssues(steps)
|
|
86
|
+
checked.set(steps, issues)
|
|
87
|
+
return issues
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const checked = new WeakMap<ReadonlyArray<Schedule.ScheduleStep>, ReadonlyArray<OrderIssue>>()
|
|
91
|
+
|
|
92
|
+
const computeIssues = (steps: ReadonlyArray<Schedule.ScheduleStep>): ReadonlyArray<OrderIssue> => {
|
|
93
|
+
const items = flatten(steps)
|
|
94
|
+
const issues: Array<OrderIssue> = []
|
|
95
|
+
const systemsAt = items.flatMap((item, at) => item.kind === "system" ? [{ system: item.system, at }] : [])
|
|
96
|
+
for (const { system: reader, at: index } of systemsAt) {
|
|
97
|
+
for (const read of accesses(reader)) {
|
|
98
|
+
if (read.mode !== "read" || read.thisTick !== true) continue
|
|
99
|
+
const data = read.descriptor.name
|
|
100
|
+
for (const { system, at } of systemsAt) {
|
|
101
|
+
if (at <= index || sameSystem(system, reader)) continue
|
|
102
|
+
if (!accesses(system).some((access) => access.mode === "write" && sameData(access.descriptor, read.descriptor))) continue
|
|
103
|
+
issues.push({
|
|
104
|
+
kind: "lateWriter",
|
|
105
|
+
reader: reader.name,
|
|
106
|
+
data,
|
|
107
|
+
writer: system.name,
|
|
108
|
+
message: `${reader.name} needs ${data} from this tick, but its writer ${system.name} runs after it: move ${system.name} before ${reader.name}, or read ${data} without thisTick`
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const [slot, query] of Object.entries(reader.spec.queries as Record<string, QueryAccess>)) {
|
|
113
|
+
if (query.thisTick !== true) continue
|
|
114
|
+
const data = `queries.${slot}`
|
|
115
|
+
let lastFlush = -1
|
|
116
|
+
for (let at = 0; at < index; at++) if (items[at]!.kind === "flush") lastFlush = at
|
|
117
|
+
for (const { system, at } of systemsAt) {
|
|
118
|
+
if (sameSystem(system, reader) || !changesQuery(system, query)) continue
|
|
119
|
+
if (at > index) {
|
|
120
|
+
issues.push({
|
|
121
|
+
kind: "lateWriter",
|
|
122
|
+
reader: reader.name,
|
|
123
|
+
data,
|
|
124
|
+
writer: system.name,
|
|
125
|
+
message: `${system.name} queues commands that ${reader.name} needs this tick (${data}), but it runs after ${reader.name}: move ${system.name} before the applyDeferred() that precedes ${reader.name}, or read ${data} without thisTick`
|
|
126
|
+
})
|
|
127
|
+
} else if (at > lastFlush && at < index) {
|
|
128
|
+
issues.push({
|
|
129
|
+
kind: "unflushedWriter",
|
|
130
|
+
reader: reader.name,
|
|
131
|
+
data,
|
|
132
|
+
writer: system.name,
|
|
133
|
+
message: `${reader.name} needs this tick's commands for ${data}, but ${system.name} queues them with no applyDeferred() in between: add Game.Schedule.applyDeferred() after ${system.name}`
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return issues
|
|
140
|
+
}
|
package/src/internal/queries.ts
CHANGED
|
@@ -33,7 +33,7 @@ type SlotPlan =
|
|
|
33
33
|
readonly mode: "write"
|
|
34
34
|
readonly ordinal: number
|
|
35
35
|
readonly constructor: DescriptorModule.ResultConstructor<unknown, unknown, unknown> | undefined
|
|
36
|
-
readonly
|
|
36
|
+
readonly entityState: string | undefined
|
|
37
37
|
}
|
|
38
38
|
| {
|
|
39
39
|
readonly slot: string
|
|
@@ -143,7 +143,7 @@ export const makeQueryEngine = (world: World) => {
|
|
|
143
143
|
mode: "write",
|
|
144
144
|
ordinal,
|
|
145
145
|
constructor: DescriptorModule.constructorOf(access.descriptor),
|
|
146
|
-
|
|
146
|
+
entityState: DescriptorModule.isEntityState(access.descriptor) ? access.descriptor.name : undefined
|
|
147
147
|
})
|
|
148
148
|
} else {
|
|
149
149
|
slots.push({ slot, mode: "read", ordinal })
|
|
@@ -255,7 +255,7 @@ export const makeQueryEngine = (world: World) => {
|
|
|
255
255
|
case "read":
|
|
256
256
|
return Cells.componentRead(record, plan.ordinal)
|
|
257
257
|
case "write":
|
|
258
|
-
return Cells.componentWrite(record, plan.ordinal, world, plan.constructor, plan.
|
|
258
|
+
return Cells.componentWrite(record, plan.ordinal, world, plan.constructor, plan.entityState)
|
|
259
259
|
case "optional":
|
|
260
260
|
return Cells.componentOptional(record, plan.ordinal)
|
|
261
261
|
case "readRelation":
|
package/dist/Definition.d.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Helpers for validating reusable authored values once and reusing them later.
|
|
3
|
-
*
|
|
4
|
-
* `Definition` is intended for static or rarely changing authored data such as
|
|
5
|
-
* spawn points, collider sizes, arena bounds, or other constants that should
|
|
6
|
-
* cross the validation boundary once and then remain branded afterward.
|
|
7
|
-
*
|
|
8
|
-
* In game code this is the "content setup" companion to runtime/world setup:
|
|
9
|
-
* validate authored constants early, keep them in carried form, and then reuse
|
|
10
|
-
* them from setup systems, reset systems, or content modules without
|
|
11
|
-
* revalidating on every frame.
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```ts
|
|
15
|
-
* // Validate authored constants once when defining the content bundle.
|
|
16
|
-
* const player = Definition.all({
|
|
17
|
-
* spawn: Definition.entry(Vector2, { x: 32, y: 48 }),
|
|
18
|
-
* size: Definition.entry(Size2, { width: 16, height: 16 })
|
|
19
|
-
* })
|
|
20
|
-
* ```
|
|
21
|
-
*
|
|
22
|
-
* @module Definition
|
|
23
|
-
* @docGroup core
|
|
24
|
-
*
|
|
25
|
-
* @categoryDescription Definition Types
|
|
26
|
-
* Named reusable shapes used to validate and retain authored values.
|
|
27
|
-
*
|
|
28
|
-
* @categoryDescription Definition Builders
|
|
29
|
-
* Helpers that validate one or several authored values through existing constructors.
|
|
30
|
-
*/
|
|
31
|
-
import * as internal from "./internal/definition.ts";
|
|
32
|
-
import type * as Result from "./Result.ts";
|
|
33
|
-
/**
|
|
34
|
-
* One reusable validated definition entry.
|
|
35
|
-
*
|
|
36
|
-
* @category Definition Types
|
|
37
|
-
*/
|
|
38
|
-
export type Entry<Value, Error> = internal.Entry<Value, Error>;
|
|
39
|
-
/**
|
|
40
|
-
* Record of named definition entries accepted by {@link all}.
|
|
41
|
-
*
|
|
42
|
-
* @category Definition Types
|
|
43
|
-
*/
|
|
44
|
-
export type Entries = internal.Entries;
|
|
45
|
-
/**
|
|
46
|
-
* Minimal constructor contract accepted by {@link entry}.
|
|
47
|
-
*
|
|
48
|
-
* @category Definition Types
|
|
49
|
-
*/
|
|
50
|
-
export type ResultConstructor<Value, Raw, Error> = internal.ResultConstructor<Value, Raw, Error>;
|
|
51
|
-
/**
|
|
52
|
-
* Success shape produced by {@link all}.
|
|
53
|
-
*
|
|
54
|
-
* @category Definition Types
|
|
55
|
-
*/
|
|
56
|
-
export type SuccessOf<Input extends Entries> = internal.SuccessOf<Input>;
|
|
57
|
-
/**
|
|
58
|
-
* Error shape produced by {@link all}.
|
|
59
|
-
*
|
|
60
|
-
* @category Definition Types
|
|
61
|
-
*/
|
|
62
|
-
export type ErrorOf<Input extends Entries> = internal.ErrorOf<Input>;
|
|
63
|
-
/**
|
|
64
|
-
* Validates one reusable authored value through an existing constructor.
|
|
65
|
-
*
|
|
66
|
-
* Use this for one named authored constant that should cross a constructor
|
|
67
|
-
* boundary now and remain in carried form afterward.
|
|
68
|
-
*
|
|
69
|
-
* @example
|
|
70
|
-
* ```ts
|
|
71
|
-
* // Validate a single authored position once during content setup.
|
|
72
|
-
* const playerSpawn = Definition.entry(Vector2, { x: 32, y: 48 })
|
|
73
|
-
* ```
|
|
74
|
-
*
|
|
75
|
-
* @category Definition Builders
|
|
76
|
-
*/
|
|
77
|
-
export declare const entry: <Value, Raw, Error>(constructor: ResultConstructor<Value, Raw, Error>, raw: Raw) => Entry<Value, Error>;
|
|
78
|
-
/**
|
|
79
|
-
* Validates a named record of reusable authored values at once.
|
|
80
|
-
*
|
|
81
|
-
* Successful entries are returned under the same keys. Failed entries remain
|
|
82
|
-
* keyed so authored-data setup can report exactly which constant was invalid.
|
|
83
|
-
*
|
|
84
|
-
* Use this when one content bundle or level definition should either validate
|
|
85
|
-
* as a whole or report a structured set of authoring failures.
|
|
86
|
-
*
|
|
87
|
-
* @example
|
|
88
|
-
* ```ts
|
|
89
|
-
* // Validate one authored content bundle and keep failures keyed by field.
|
|
90
|
-
* const definitions = Definition.all({
|
|
91
|
-
* spawn: Definition.entry(Vector2, { x: 32, y: 48 }),
|
|
92
|
-
* collider: Definition.entry(Size2, { width: 16, height: 16 })
|
|
93
|
-
* })
|
|
94
|
-
* ```
|
|
95
|
-
*
|
|
96
|
-
* @category Definition Builders
|
|
97
|
-
*/
|
|
98
|
-
export declare const all: <Input extends Entries>(input: Input) => Result.Result<SuccessOf<Input>, ErrorOf<Input>>;
|
|
99
|
-
//# sourceMappingURL=Definition.d.ts.map
|
package/dist/Definition.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Definition.d.ts","sourceRoot":"","sources":["../src/Definition.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,KAAK,QAAQ,MAAM,0BAA0B,CAAA;AACpD,OAAO,KAAK,KAAK,MAAM,MAAM,aAAa,CAAA;AAE1C;;;;GAIG;AACH,MAAM,MAAM,KAAK,CAAC,KAAK,EAAE,KAAK,IAAI,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;AAC9D;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAA;AACtC;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,IAAI,QAAQ,CAAC,iBAAiB,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,CAAA;AAChG;;;;GAIG;AACH,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,OAAO,IAAI,QAAQ,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;AACxE;;;;GAIG;AACH,MAAM,MAAM,OAAO,CAAC,KAAK,SAAS,OAAO,IAAI,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;AAEpE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,KAAK,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,EACpC,WAAW,EAAE,iBAAiB,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,EACjD,GAAG,EAAE,GAAG,KACL,KAAK,CAAC,KAAK,EAAE,KAAK,CAAkB,CAAA;AAEzC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,GAAG,EAAE,CAAC,KAAK,SAAS,OAAO,EACtC,KAAK,EAAE,KAAK,KACT,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAgB,CAAA"}
|
package/dist/Definition.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Helpers for validating reusable authored values once and reusing them later.
|
|
3
|
-
*
|
|
4
|
-
* `Definition` is intended for static or rarely changing authored data such as
|
|
5
|
-
* spawn points, collider sizes, arena bounds, or other constants that should
|
|
6
|
-
* cross the validation boundary once and then remain branded afterward.
|
|
7
|
-
*
|
|
8
|
-
* In game code this is the "content setup" companion to runtime/world setup:
|
|
9
|
-
* validate authored constants early, keep them in carried form, and then reuse
|
|
10
|
-
* them from setup systems, reset systems, or content modules without
|
|
11
|
-
* revalidating on every frame.
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```ts
|
|
15
|
-
* // Validate authored constants once when defining the content bundle.
|
|
16
|
-
* const player = Definition.all({
|
|
17
|
-
* spawn: Definition.entry(Vector2, { x: 32, y: 48 }),
|
|
18
|
-
* size: Definition.entry(Size2, { width: 16, height: 16 })
|
|
19
|
-
* })
|
|
20
|
-
* ```
|
|
21
|
-
*
|
|
22
|
-
* @module Definition
|
|
23
|
-
* @docGroup core
|
|
24
|
-
*
|
|
25
|
-
* @categoryDescription Definition Types
|
|
26
|
-
* Named reusable shapes used to validate and retain authored values.
|
|
27
|
-
*
|
|
28
|
-
* @categoryDescription Definition Builders
|
|
29
|
-
* Helpers that validate one or several authored values through existing constructors.
|
|
30
|
-
*/
|
|
31
|
-
import * as internal from "./internal/definition.js";
|
|
32
|
-
/**
|
|
33
|
-
* Validates one reusable authored value through an existing constructor.
|
|
34
|
-
*
|
|
35
|
-
* Use this for one named authored constant that should cross a constructor
|
|
36
|
-
* boundary now and remain in carried form afterward.
|
|
37
|
-
*
|
|
38
|
-
* @example
|
|
39
|
-
* ```ts
|
|
40
|
-
* // Validate a single authored position once during content setup.
|
|
41
|
-
* const playerSpawn = Definition.entry(Vector2, { x: 32, y: 48 })
|
|
42
|
-
* ```
|
|
43
|
-
*
|
|
44
|
-
* @category Definition Builders
|
|
45
|
-
*/
|
|
46
|
-
export const entry = internal.entry;
|
|
47
|
-
/**
|
|
48
|
-
* Validates a named record of reusable authored values at once.
|
|
49
|
-
*
|
|
50
|
-
* Successful entries are returned under the same keys. Failed entries remain
|
|
51
|
-
* keyed so authored-data setup can report exactly which constant was invalid.
|
|
52
|
-
*
|
|
53
|
-
* Use this when one content bundle or level definition should either validate
|
|
54
|
-
* as a whole or report a structured set of authoring failures.
|
|
55
|
-
*
|
|
56
|
-
* @example
|
|
57
|
-
* ```ts
|
|
58
|
-
* // Validate one authored content bundle and keep failures keyed by field.
|
|
59
|
-
* const definitions = Definition.all({
|
|
60
|
-
* spawn: Definition.entry(Vector2, { x: 32, y: 48 }),
|
|
61
|
-
* collider: Definition.entry(Size2, { width: 16, height: 16 })
|
|
62
|
-
* })
|
|
63
|
-
* ```
|
|
64
|
-
*
|
|
65
|
-
* @category Definition Builders
|
|
66
|
-
*/
|
|
67
|
-
export const all = internal.all;
|
|
68
|
-
//# sourceMappingURL=Definition.js.map
|
package/dist/Definition.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Definition.js","sourceRoot":"","sources":["../src/Definition.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,KAAK,QAAQ,MAAM,0BAA0B,CAAA;AAkCpD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,KAAK,GAGS,QAAQ,CAAC,KAAK,CAAA;AAEzC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,GAAG,GAEuC,QAAQ,CAAC,GAAG,CAAA"}
|
package/dist/Feature.d.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import type * as Schedule from "./Schedule.ts";
|
|
2
|
-
import type { MergeSchemaDefinitions, Schema, SchemaDefinition, ValidateFragments } from "./Schema.ts";
|
|
3
|
-
type EmptySchema = SchemaDefinition<{}, {}, {}, {}>;
|
|
4
|
-
type AnyFeature = {
|
|
5
|
-
readonly kind: "feature";
|
|
6
|
-
readonly name: string;
|
|
7
|
-
readonly schema: Schema.Any;
|
|
8
|
-
readonly requires: ReadonlyArray<AnyFeature>;
|
|
9
|
-
readonly build: unknown;
|
|
10
|
-
readonly output: FeatureBuildOutput;
|
|
11
|
-
};
|
|
12
|
-
type AnySchedule = Schedule.ScheduleDefinition<any, any, any, any, any>;
|
|
13
|
-
/**
|
|
14
|
-
* What a feature builder returns: bootstrap and update schedules, plus any
|
|
15
|
-
* other values the feature wants to expose on the composed project.
|
|
16
|
-
*/
|
|
17
|
-
export type FeatureBuildOutput = {
|
|
18
|
-
readonly bootstrap?: ReadonlyArray<AnySchedule>;
|
|
19
|
-
readonly update?: ReadonlyArray<AnySchedule>;
|
|
20
|
-
};
|
|
21
|
-
/**
|
|
22
|
-
* The bound authoring surface a feature builder receives. It is the normal
|
|
23
|
-
* `Game` API restricted to the descriptors the feature can see; runtimes are
|
|
24
|
-
* built from the composed project instead.
|
|
25
|
-
*/
|
|
26
|
-
export type FeatureBuildGame<Accessible extends Schema.Any, Root = unknown> = Omit<Schema.Game<Accessible, Root>, "Runtime">;
|
|
27
|
-
type FeatureBuildFunction<Accessible extends Schema.Any, Output extends FeatureBuildOutput = FeatureBuildOutput> = (Game: FeatureBuildGame<Accessible>) => Output;
|
|
28
|
-
export interface FeatureDefinition<Name extends string = string, FeatureSchema extends Schema.Any = Schema.Any, Requires extends ReadonlyArray<AnyFeature> = ReadonlyArray<AnyFeature>, Output extends FeatureBuildOutput = FeatureBuildOutput> {
|
|
29
|
-
readonly kind: "feature";
|
|
30
|
-
readonly name: Name;
|
|
31
|
-
readonly schema: FeatureSchema;
|
|
32
|
-
readonly requires: Requires;
|
|
33
|
-
readonly build: FeatureBuildFunction<MergeSchemaDefinitions<FeatureSchema, MergeFeatureSchemas<Requires>>, Output>;
|
|
34
|
-
readonly output: Output;
|
|
35
|
-
}
|
|
36
|
-
type MergeFeatureSchemas<Features extends ReadonlyArray<AnyFeature>> = Features extends readonly [infer Head extends AnyFeature, ...infer Tail extends Array<AnyFeature>] ? MergeSchemaDefinitions<FeatureClosureSchema<Head>, MergeFeatureSchemas<Tail>> : EmptySchema;
|
|
37
|
-
type FeatureClosureSchema<F extends AnyFeature> = F extends FeatureDefinition<any, infer FeatureSchema extends Schema.Any, infer Requires extends ReadonlyArray<AnyFeature>, any> ? MergeSchemaDefinitions<FeatureSchema, MergeFeatureSchemas<Requires>> : EmptySchema;
|
|
38
|
-
type FeatureNames<Features extends ReadonlyArray<AnyFeature>> = Features[number]["name"];
|
|
39
|
-
type DuplicateFeatureNames<Features extends ReadonlyArray<AnyFeature>, Seen extends string = never> = Features extends readonly [infer Head extends AnyFeature, ...infer Tail extends Array<AnyFeature>] ? Head["name"] extends Seen ? Head["name"] | DuplicateFeatureNames<Tail, Seen> : DuplicateFeatureNames<Tail, Seen | Head["name"]> : never;
|
|
40
|
-
type MissingFeatureDependencies<Features extends ReadonlyArray<AnyFeature>> = Exclude<Features[number]["requires"][number]["name"], FeatureNames<Features>>;
|
|
41
|
-
type FeatureSchemas<Features extends ReadonlyArray<AnyFeature>> = {
|
|
42
|
-
readonly [K in keyof Features]: Features[K]["schema"];
|
|
43
|
-
};
|
|
44
|
-
/**
|
|
45
|
-
* Rejects duplicate feature names, missing required features, and schema
|
|
46
|
-
* fragments whose keys or descriptor names collide.
|
|
47
|
-
*/
|
|
48
|
-
type ValidateFeatureSelection<Features extends ReadonlyArray<AnyFeature>> = [
|
|
49
|
-
DuplicateFeatureNames<Features>
|
|
50
|
-
] extends [never] ? [MissingFeatureDependencies<Features>] extends [never] ? ValidateFragments<FeatureSchemas<Features>> : {
|
|
51
|
-
readonly __fixFeatureDependencies__: MissingFeatureDependencies<Features>;
|
|
52
|
-
} : {
|
|
53
|
-
readonly __fixFeatureSelection__: DuplicateFeatureNames<Features>;
|
|
54
|
-
};
|
|
55
|
-
type RebindFeatureSchedule<ScheduleValue, S extends Schema.Any, Root> = ScheduleValue extends Schedule.ScheduleDefinition<any, infer Requirements, any, infer Carried, infer Failure> ? Schedule.ScheduleDefinition<S, Requirements, Root, Carried, Failure> : never;
|
|
56
|
-
type FeatureScheduleArray<Output, Key extends "bootstrap" | "update"> = Key extends keyof Output ? Extract<Output[Key], ReadonlyArray<AnySchedule>> : readonly [];
|
|
57
|
-
type NormalizeFeatureOutput<Output extends object, S extends Schema.Any, Root> = {
|
|
58
|
-
readonly [K in Exclude<keyof Output, "bootstrap" | "update">]: Output[K];
|
|
59
|
-
} & {
|
|
60
|
-
readonly bootstrap: ReadonlyArray<RebindFeatureSchedule<FeatureScheduleArray<Output, "bootstrap">[number], S, Root>>;
|
|
61
|
-
readonly update: ReadonlyArray<RebindFeatureSchedule<FeatureScheduleArray<Output, "update">[number], S, Root>>;
|
|
62
|
-
};
|
|
63
|
-
type FeatureOutputRecord<Features extends ReadonlyArray<AnyFeature>, S extends Schema.Any, Root> = {
|
|
64
|
-
readonly [K in FeatureNames<Features>]: NormalizeFeatureOutput<Extract<Features[number], {
|
|
65
|
-
readonly name: K;
|
|
66
|
-
}>["output"], S, Root>;
|
|
67
|
-
};
|
|
68
|
-
/**
|
|
69
|
-
* The result of composing features: the merged schema, the bound `Game`,
|
|
70
|
-
* each feature's normalized output, and the aggregated schedules in feature
|
|
71
|
-
* order.
|
|
72
|
-
*/
|
|
73
|
-
export interface ComposedFeatureProject<Features extends ReadonlyArray<AnyFeature>, Root = unknown, S extends Schema.Any = MergeFeatureSchemas<Features>> {
|
|
74
|
-
readonly schema: S;
|
|
75
|
-
readonly Game: Schema.Game<S, Root>;
|
|
76
|
-
readonly features: FeatureOutputRecord<Features, S, Root>;
|
|
77
|
-
readonly schedules: {
|
|
78
|
-
readonly bootstrap: ReadonlyArray<FeatureOutputRecord<Features, S, Root>[FeatureNames<Features>]["bootstrap"][number]>;
|
|
79
|
-
readonly update: ReadonlyArray<FeatureOutputRecord<Features, S, Root>[FeatureNames<Features>]["update"][number]>;
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Defines one pre-bind feature.
|
|
84
|
-
*/
|
|
85
|
-
export declare const define: <const Name extends string, FeatureSchema extends Schema.Any, const Requires extends ReadonlyArray<AnyFeature> = [], Output extends FeatureBuildOutput = FeatureBuildOutput>(name: Name, options: {
|
|
86
|
-
readonly schema: FeatureSchema;
|
|
87
|
-
readonly requires?: Requires;
|
|
88
|
-
readonly build: FeatureBuildFunction<MergeSchemaDefinitions<FeatureSchema, MergeFeatureSchemas<Requires>>, Output>;
|
|
89
|
-
}) => FeatureDefinition<Name, FeatureSchema, Requires, Output>;
|
|
90
|
-
/**
|
|
91
|
-
* Composes features into one merged schema and one bound `Game`, then builds
|
|
92
|
-
* every feature against it.
|
|
93
|
-
*
|
|
94
|
-
* Aggregated schedule order is the selected feature order, not dependency
|
|
95
|
-
* order. Run them with `runtime.tick(...project.schedules.update)`.
|
|
96
|
-
*/
|
|
97
|
-
export declare const compose: <Root, const Features extends readonly [AnyFeature, ...Array<AnyFeature>]>(options: {
|
|
98
|
-
readonly root: Root;
|
|
99
|
-
readonly features: Features;
|
|
100
|
-
} & ValidateFeatureSelection<Features>) => ComposedFeatureProject<Features, Root>;
|
|
101
|
-
export declare const Feature: {
|
|
102
|
-
define: typeof define;
|
|
103
|
-
compose: typeof compose;
|
|
104
|
-
};
|
|
105
|
-
export {};
|
|
106
|
-
//# sourceMappingURL=Feature.d.ts.map
|