@adobe/ecs 0.2.3 → 0.3.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/dist/implementation/compiler/augmentations.js +8 -1
- package/dist/implementation/compiler/augmentations.js.map +1 -1
- package/dist/implementation/compiler/body-access.d.ts +18 -1
- package/dist/implementation/compiler/body-access.js +104 -15
- package/dist/implementation/compiler/body-access.js.map +1 -1
- package/dist/implementation/compiler/estree.d.ts +5 -2
- package/dist/implementation/compiler/estree.js +8 -5
- package/dist/implementation/compiler/estree.js.map +1 -1
- package/dist/implementation/compiler/inferred.js +5 -2
- package/dist/implementation/compiler/inferred.js.map +1 -1
- package/dist/implementation/compiler/transform.js +5 -4
- package/dist/implementation/compiler/transform.js.map +1 -1
- package/dist/implementation/compiler/type-schemas.d.ts +1 -0
- package/dist/implementation/compiler/type-schemas.js +35 -2
- package/dist/implementation/compiler/type-schemas.js.map +1 -1
- package/dist/implementation/world/activate.d.ts +14 -3
- package/dist/implementation/world/activate.js +28 -10
- package/dist/implementation/world/activate.js.map +1 -1
- package/dist/implementation/world/buffer.d.ts +7 -0
- package/dist/implementation/world/buffer.js +7 -0
- package/dist/implementation/world/buffer.js.map +1 -1
- package/dist/implementation/world/cascade.d.ts +10 -3
- package/dist/implementation/world/cascade.js +48 -23
- package/dist/implementation/world/cascade.js.map +1 -1
- package/dist/implementation/world/commands.js +41 -5
- package/dist/implementation/world/commands.js.map +1 -1
- package/dist/implementation/world/create.js +57 -6
- package/dist/implementation/world/create.js.map +1 -1
- package/dist/implementation/world/execute.d.ts +4 -4
- package/dist/implementation/world/execute.js +228 -93
- package/dist/implementation/world/execute.js.map +1 -1
- package/dist/implementation/world/finalizers.d.ts +10 -0
- package/dist/implementation/world/finalizers.js +60 -10
- package/dist/implementation/world/finalizers.js.map +1 -1
- package/dist/implementation/world/join.js +12 -2
- package/dist/implementation/world/join.js.map +1 -1
- package/dist/implementation/world/program.js +5 -1
- package/dist/implementation/world/program.js.map +1 -1
- package/dist/implementation/world/resource-changes.d.ts +18 -0
- package/dist/implementation/world/resource-changes.js +66 -0
- package/dist/implementation/world/resource-changes.js.map +1 -0
- package/dist/implementation/world/rows.js +21 -16
- package/dist/implementation/world/rows.js.map +1 -1
- package/dist/implementation/world/run-changes.d.ts +38 -0
- package/dist/implementation/world/run-changes.js +138 -0
- package/dist/implementation/world/run-changes.js.map +1 -0
- package/dist/implementation/world/runner.js +16 -12
- package/dist/implementation/world/runner.js.map +1 -1
- package/dist/implementation/world/state.d.ts +15 -0
- package/dist/implementation/world/state.js +22 -1
- package/dist/implementation/world/state.js.map +1 -1
- package/dist/implementation/world/synchronize.js +81 -24
- package/dist/implementation/world/synchronize.js.map +1 -1
- package/dist/implementation/world/tables-read.d.ts +5 -3
- package/dist/implementation/world/tables-read.js +16 -4
- package/dist/implementation/world/tables-read.js.map +1 -1
- package/dist/implementation/world/tables.d.ts +4 -2
- package/dist/implementation/world/tables.js +137 -38
- package/dist/implementation/world/tables.js.map +1 -1
- package/dist/implementation/world/thread-observer.js +1 -1
- package/dist/implementation/world/thread-observer.js.map +1 -1
- package/dist/implementation/world/thread-systems.js +10 -6
- package/dist/implementation/world/thread-systems.js.map +1 -1
- package/dist/implementation/world/up.js +7 -1
- package/dist/implementation/world/up.js.map +1 -1
- package/dist/implementation/world/where.d.ts +8 -0
- package/dist/implementation/world/where.js +40 -11
- package/dist/implementation/world/where.js.map +1 -1
- package/dist/interface/finalizer/body.d.ts +40 -4
- package/dist/interface/finalizer/create.d.ts +8 -6
- package/dist/interface/finalizer/create.js +45 -10
- package/dist/interface/finalizer/create.js.map +1 -1
- package/dist/interface/finalizer/declaration.d.ts +26 -4
- package/dist/interface/indexes/declaration-of.d.ts +17 -0
- package/dist/interface/indexes/declaration-of.js +20 -0
- package/dist/interface/indexes/declaration-of.js.map +1 -0
- package/dist/interface/indexes/index.d.ts +1 -0
- package/dist/interface/indexes/index.js +1 -0
- package/dist/interface/indexes/index.js.map +1 -1
- package/dist/interface/registry/index.d.ts +1 -1
- package/dist/interface/registry/index.js +1 -1
- package/dist/interface/registry/index.js.map +1 -1
- package/dist/interface/registry/names.d.ts +4 -0
- package/dist/interface/registry/names.js +20 -0
- package/dist/interface/registry/names.js.map +1 -1
- package/dist/interface/registry/schedule.d.ts +29 -1
- package/dist/interface/registry/schedule.js +261 -25
- package/dist/interface/registry/schedule.js.map +1 -1
- package/dist/interface/system/access.d.ts +15 -0
- package/dist/interface/system/check.d.ts +7 -0
- package/dist/interface/system/check.js +21 -3
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +31 -5
- package/dist/interface/system/create-resolved.js +2 -1
- package/dist/interface/system/create-resolved.js.map +1 -1
- package/dist/interface/system/create.d.ts +10 -6
- package/dist/interface/system/create.js +5 -4
- package/dist/interface/system/create.js.map +1 -1
- package/dist/interface/system/declaration.d.ts +11 -4
- package/dist/interface/system/describe.d.ts +120 -0
- package/dist/interface/system/describe.js +116 -0
- package/dist/interface/system/describe.js.map +1 -0
- package/dist/interface/system/index.d.ts +1 -0
- package/dist/interface/system/index.js +1 -0
- package/dist/interface/system/index.js.map +1 -1
- package/dist/interface/system/leaf.d.ts +7 -0
- package/dist/interface/system/render.js +2 -1
- package/dist/interface/system/render.js.map +1 -1
- package/dist/interface/system/resolve.d.ts +14 -1
- package/dist/interface/system/resolve.js +50 -1
- package/dist/interface/system/resolve.js.map +1 -1
- package/dist/interface/system/shared-only.d.ts +7 -0
- package/dist/interface/system/shared-only.js +2 -0
- package/dist/interface/system/shared-only.js.map +1 -0
- package/dist/interface/system/tables.d.ts +9 -1
- package/dist/interface/world/table/index.d.ts +3 -2
- package/dist/interface/world/world.d.ts +15 -0
- package/package.json +11 -2
- package/src/guide/01-resources.md +4 -3
- package/src/guide/03-systems.out +1 -1
- package/src/guide/05-scheduling.md +12 -6
- package/src/guide/05-scheduling.out +1 -0
- package/src/guide/05-scheduling.ts +17 -0
- package/src/guide/06-sync-points.md +20 -8
- package/src/guide/06-sync-points.out +3 -2
- package/src/guide/06-sync-points.ts +24 -5
- package/src/guide/10-finalizers.md +5 -0
- package/src/guide/18-identity.out +1 -1
- package/src/guide/guide.test.ts +6 -153
- package/src/implementation/compiler/augmentations.test.ts +28 -1
- package/src/implementation/compiler/augmentations.ts +5 -1
- package/src/implementation/compiler/body-access.ts +102 -12
- package/src/implementation/compiler/estree.ts +9 -6
- package/src/implementation/compiler/inferred.test.ts +1 -1
- package/src/implementation/compiler/inferred.ts +5 -2
- package/src/implementation/compiler/transform.test.ts +41 -21
- package/src/implementation/compiler/transform.ts +5 -3
- package/src/implementation/compiler/type-schemas.ts +28 -2
- package/src/implementation/world/activate.ts +32 -10
- package/src/implementation/world/batches.test.ts +3 -3
- package/src/implementation/world/buffer.ts +7 -0
- package/src/implementation/world/cascade.ts +46 -22
- package/src/implementation/world/commands.ts +37 -5
- package/src/implementation/world/create.test.ts +8 -8
- package/src/implementation/world/create.ts +61 -6
- package/src/implementation/world/defaults.test.ts +24 -1
- package/src/implementation/world/despawn-where.test.ts +310 -0
- package/src/implementation/world/execute.ts +222 -86
- package/src/implementation/world/finalizers.ts +63 -11
- package/src/implementation/world/ids.test.ts +1 -1
- package/src/implementation/world/indexes.test.ts +2 -1
- package/src/implementation/world/join.test.ts +19 -0
- package/src/implementation/world/join.ts +16 -2
- package/src/implementation/world/lifetime.test-helper.ts +1 -1
- package/src/implementation/world/names.test.ts +1 -2
- package/src/implementation/world/passes.test.ts +61 -0
- package/src/implementation/world/perf.md +12 -1
- package/src/implementation/world/program.ts +4 -1
- package/src/implementation/world/resource-changes.test.ts +61 -0
- package/src/implementation/world/resource-changes.ts +67 -0
- package/src/implementation/world/rows.ts +21 -14
- package/src/implementation/world/rules.test.ts +37 -0
- package/src/implementation/world/run-changes.test.ts +88 -0
- package/src/implementation/world/run-changes.ts +160 -0
- package/src/implementation/world/run-results.test.ts +17 -0
- package/src/implementation/world/runner.ts +17 -13
- package/src/implementation/world/schedule.test.ts +42 -1
- package/src/implementation/world/shared.test.ts +2 -2
- package/src/implementation/world/state.ts +27 -0
- package/src/implementation/world/sync-points.test.ts +147 -0
- package/src/implementation/world/synchronize.ts +73 -20
- package/src/implementation/world/tables-read.ts +16 -5
- package/src/implementation/world/tables.test.ts +139 -4
- package/src/implementation/world/tables.ts +118 -35
- package/src/implementation/world/thread-observer.ts +1 -1
- package/src/implementation/world/thread-systems.ts +10 -6
- package/src/implementation/world/up.test.ts +13 -1
- package/src/implementation/world/up.ts +6 -1
- package/src/implementation/world/watch-traits-late.test-helper.ts +14 -0
- package/src/implementation/world/watch-traits.test-helper.ts +10 -0
- package/src/implementation/world/watch-traits.test.ts +62 -0
- package/src/implementation/world/where.ts +38 -10
- package/src/interface/finalizer/body.ts +33 -4
- package/src/interface/finalizer/create.test.ts +6 -1
- package/src/interface/finalizer/create.ts +45 -12
- package/src/interface/finalizer/declaration.ts +31 -4
- package/src/interface/indexes/create.test.ts +10 -0
- package/src/interface/indexes/declaration-of.ts +34 -0
- package/src/interface/indexes/index.ts +1 -0
- package/src/interface/registry/index.ts +1 -1
- package/src/interface/registry/names.ts +19 -0
- package/src/interface/registry/schedule.ts +275 -29
- package/src/interface/rule/rule.type-test.ts +23 -1
- package/src/interface/system/access.ts +16 -0
- package/src/interface/system/check.ts +17 -2
- package/src/interface/system/commands.ts +28 -4
- package/src/interface/system/commands.type-test.ts +31 -0
- package/src/interface/system/create-resolved.ts +2 -1
- package/src/interface/system/create.test.ts +10 -2
- package/src/interface/system/create.ts +9 -5
- package/src/interface/system/declaration.ts +10 -4
- package/src/interface/system/describe.test.ts +98 -0
- package/src/interface/system/describe.ts +122 -0
- package/src/interface/system/index.ts +1 -0
- package/src/interface/system/leaf.ts +7 -0
- package/src/interface/system/render.ts +2 -1
- package/src/interface/system/resolve.test.ts +17 -1
- package/src/interface/system/resolve.ts +60 -2
- package/src/interface/system/shared-only.ts +5 -0
- package/src/interface/system/system.type-test.ts +6 -0
- package/src/interface/system/tables.ts +7 -1
- package/src/interface/world/run.type-test.ts +11 -1
- package/src/interface/world/table/index.ts +3 -2
- package/src/interface/world/world.ts +13 -0
- package/src/perftest/benchmarks.ts +20 -0
- package/src/perftest/chained.ts +108 -0
- package/src/perftest/structural.ts +20 -0
- package/src/perftest/tables.ts +107 -0
|
@@ -49,7 +49,7 @@ describe("System.create", () => {
|
|
|
49
49
|
const body = (): void => {};
|
|
50
50
|
// A body the build doesn't see is given what it uses.
|
|
51
51
|
const move = System.create(giveAccess({ components: { with: [position] } }, { write: [position], resources: { read: [time] } }), body);
|
|
52
|
-
expect(definitionOf(move)).toEqual({ kind: "system", name: "move", declaration: { components: { write: [position] }, resources: { read: [time] } }, body });
|
|
52
|
+
expect(definitionOf(move)).toEqual({ kind: "system", name: "move", declaration: { components: { write: [position] }, resources: { read: [time] } }, body, commands: expect.any(Function) });
|
|
53
53
|
expect(move.name).toBe("move");
|
|
54
54
|
expect(definitionOf({ name: "move" })).toBe(undefined);
|
|
55
55
|
});
|
|
@@ -71,7 +71,7 @@ describe("System.create", () => {
|
|
|
71
71
|
it("labels a system made with neither an id nor a binding by its kind numbered", () => {
|
|
72
72
|
const body = ({ commands }: { readonly commands: System.Commands }): void => { commands.spawn([position])({ [position]: 0 }); };
|
|
73
73
|
const [alone] = [System.create(giveAccess(identify(body, "setup.ts#spawnOnce")))];
|
|
74
|
-
expect(definitionOf(alone)).toEqual({ kind: "system", name: "spawnOnce", declaration: {}, body });
|
|
74
|
+
expect(definitionOf(alone)).toEqual({ kind: "system", name: "spawnOnce", declaration: {}, body, commands: expect.any(Function) });
|
|
75
75
|
const [first, second] = [System.create(() => {}), System.create({ systems: [alone] })].map(system => system.name);
|
|
76
76
|
const number = Number(first?.slice("system #".length));
|
|
77
77
|
expect([first, second]).toEqual([`system #${number}`, `system #${number + 1}`]);
|
|
@@ -171,6 +171,14 @@ describe("System.create of a composite", () => {
|
|
|
171
171
|
const looped = System.create({ before: [first], after: [second] }, () => {});
|
|
172
172
|
expect(() => System.create({ systems: [first, second, looped] })).toThrow("Ordering cycle");
|
|
173
173
|
});
|
|
174
|
+
it("takes a row declared whole as written, every member but references, and says so in a conflict", () => {
|
|
175
|
+
const pass = (row: object): void => { void row; };
|
|
176
|
+
const whole = System.create({ components: { with: [position, parent], whole: true } }, row => { pass(row); });
|
|
177
|
+
const reader = System.create({ components: { with: [position] } }, row => { void row[position]; });
|
|
178
|
+
const definition = definitionOf(whole);
|
|
179
|
+
expect(definition?.kind === "system" ? definition.declaration.components : null).toEqual({ read: [parent], write: [position], whole: true });
|
|
180
|
+
expect(() => System.create([whole, reader])).toThrow(`both access "typesSystemTest/position" and one writes it; order them with before or after, or, if they never match one entity, add a component of one to the other's not. "whole" takes its row whole (components.whole), so it writes every member.`);
|
|
181
|
+
});
|
|
174
182
|
it("runs readonly members after its sync point, in no tier, never conflicting and ignoring constraints naming them", () => {
|
|
175
183
|
const write = System.create({ components: { with: [position] } }, row => { row[position] += 1; });
|
|
176
184
|
const watch = System.create({ readonly: true, components: { with: [position] } }, row => { void row[position]; });
|
|
@@ -16,8 +16,9 @@ import type { Listed, Property } from './property.js';
|
|
|
16
16
|
import type { System } from './system.js';
|
|
17
17
|
import type { OneThread, ThreadedBy, ThreadsOf } from './threads.js';
|
|
18
18
|
import type { HeldBy } from './values.js';
|
|
19
|
+
import type { SharedOnly } from './shared-only.js';
|
|
19
20
|
import type { Shared } from '../component/value.js';
|
|
20
|
-
import { accessOf, compact, componentLists, lookupsLists, resourceUses, upReads, whereReadsOf } from './resolve.js';
|
|
21
|
+
import { accessOf, commandListsOf, compact, componentLists, lookupsLists, resourceUses, upReads, whereReadsOf } from './resolve.js';
|
|
21
22
|
|
|
22
23
|
type Lookups<D> = Property<D, "lookup">;
|
|
23
24
|
|
|
@@ -68,9 +69,10 @@ type Targeted = {
|
|
|
68
69
|
readonly order?: never;
|
|
69
70
|
};
|
|
70
71
|
|
|
72
|
+
|
|
71
73
|
/** `where` names only shared components, and keeps rows unordered across archetypes, so it's without `up`, `order` and a target. */
|
|
72
74
|
type Where<D> = D extends { readonly components: { readonly where: infer W } }
|
|
73
|
-
? { readonly components?: { readonly where?:
|
|
75
|
+
? { readonly components?: { readonly where?: SharedOnly<W> } }
|
|
74
76
|
& (D extends { readonly up: object } | { readonly order: object } | { readonly target: string }
|
|
75
77
|
? { readonly components?: { readonly where?: { readonly "error: where can't be used with up, order or a target, which run rows across archetypes": true } } } : {})
|
|
76
78
|
: {};
|
|
@@ -99,6 +101,7 @@ type Validate<D extends Declaration> =
|
|
|
99
101
|
? { readonly kernel?: boolean | { readonly "error: a GPU kernel can't have optional components yet": true } } : {})
|
|
100
102
|
& (D extends { readonly readonly: true } ? Readonly : {})
|
|
101
103
|
// Tables are copies a readonly system without components reads.
|
|
104
|
+
& (D extends { readonly tables: { readonly where: infer W } } ? { readonly tables?: { readonly where?: SharedOnly<W> } } : {})
|
|
102
105
|
& (D extends { readonly tables: object }
|
|
103
106
|
? (D extends { readonly readonly: true } ? {} : { readonly tables?: { readonly "error: tables are read by a readonly system": true } })
|
|
104
107
|
& (Property<D, "components"> extends undefined ? {} : { readonly tables?: { readonly "error: a system reading tables has no components": true } })
|
|
@@ -175,9 +178,10 @@ export function create(first: Declaration | Leaf["body"] | readonly System<unkno
|
|
|
175
178
|
const body = typeof first === "function" ? first : second;
|
|
176
179
|
if (body === undefined) throw new Error("A system needs a body, and a composite its systems.");
|
|
177
180
|
const name = nameOf(first, id, "system");
|
|
178
|
-
const
|
|
181
|
+
const access = accessOf(`System "${name}"`, first);
|
|
182
|
+
const declaration = resolve(`System "${name}"`, declared, access);
|
|
179
183
|
const schema = inputSchemaOf(name, body, declaration.components === undefined ? 1 : 2, input);
|
|
180
|
-
const system: Leaf & System<unknown> = { kind: "system", name, declaration, input: schema, body };
|
|
184
|
+
const system: Leaf & System<unknown> = { kind: "system", name, declaration, input: schema, body, commands: commandListsOf(`System "${name}"`, access) };
|
|
181
185
|
checkLeaf(system);
|
|
182
186
|
claimSystem(system, id);
|
|
183
187
|
return system;
|
|
@@ -194,7 +198,7 @@ const resolve = (what: string, declared: Declaration, access: ReturnType<typeof
|
|
|
194
198
|
for (const key of [...whereReads, ...declared.tables?.resources ?? []]) if (!uses.read.includes(key) && !uses.write.includes(key) && !conditions.includes(key)) uses.read.push(key);
|
|
195
199
|
const resolved: Resolved = {
|
|
196
200
|
...rest,
|
|
197
|
-
...components === undefined ? {} : { components: componentLists(components, access.components, reading, whereReads) },
|
|
201
|
+
...components === undefined ? {} : { components: componentLists(components, components.whole === true ? null : access.components, reading, whereReads) },
|
|
198
202
|
...lookup === undefined ? {} : { lookup: lookupsLists(lookup, access) },
|
|
199
203
|
...up === undefined ? {} : { up: { through: up.through, read: upReads(what, access) } },
|
|
200
204
|
};
|
|
@@ -52,10 +52,16 @@ export type Declaration = {
|
|
|
52
52
|
readonly target?: string;
|
|
53
53
|
/** Makes this a render system (see `render`): its body records GPU work over the GPU copy of the components it names. */
|
|
54
54
|
readonly shader?: Shader;
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Hard constraint: runs before each system listed, and, for a resource listed, before every other system of the
|
|
57
|
+
* composite that uses it, but those its constraints already order before it and those listing it in `before` too.
|
|
58
|
+
*/
|
|
59
|
+
readonly before?: readonly (System<unknown> | Resource.Key)[];
|
|
60
|
+
/**
|
|
61
|
+
* Hard constraint: runs after each system listed, and, for a resource listed, after every other system of the
|
|
62
|
+
* composite that uses it, but those its constraints already order after it and those listing it in `after` too.
|
|
63
|
+
*/
|
|
64
|
+
readonly after?: readonly (System<unknown> | Resource.Key)[];
|
|
59
65
|
/** Runs inside the span of the systems listed: after what they follow and before what follows them. */
|
|
60
66
|
readonly during?: readonly System<unknown>[];
|
|
61
67
|
/**
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { Index } from "../indexes/index-type.js";
|
|
4
|
+
import { System } from "./system.js";
|
|
5
|
+
|
|
6
|
+
const position = Symbol("describeTest/position");
|
|
7
|
+
const cell = Symbol("describeTest/cell");
|
|
8
|
+
const speed = Symbol("describeTest/speed");
|
|
9
|
+
const hover = Symbol("describeTest/hover");
|
|
10
|
+
|
|
11
|
+
declare module "@adobe/ecs" {
|
|
12
|
+
export interface Components {
|
|
13
|
+
[position]: { type: number };
|
|
14
|
+
[cell]: { type: number };
|
|
15
|
+
}
|
|
16
|
+
export interface Resources {
|
|
17
|
+
[speed]: { type: number };
|
|
18
|
+
[hover]: { type: number };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const byCell = Index.create({ key: [cell] });
|
|
23
|
+
|
|
24
|
+
const spawnCells = System.create(({ commands }) => { commands.spawn([position])({ [position]: 1 }); });
|
|
25
|
+
const move = System.create({ components: { with: [position] }, after: [spawnCells] }, (row, { resources }) => { row[position] += resources[speed]; });
|
|
26
|
+
const deriveCells = System.create({ components: { with: [position], not: [cell] }, after: [move] }, (row, { commands }) => { commands.add(row.entity, cell, row[position]); });
|
|
27
|
+
const pick = System.create({ lookup: { cells: { index: byCell } }, after: [deriveCells] }, ({ lookup, resources }) => { resources[hover] = lookup.cells(1).length; });
|
|
28
|
+
const nudge = System.create({ components: { with: [position], whole: true } }, row => { Reflect.set(row, position, 0); });
|
|
29
|
+
const watch = System.create({ readonly: true, components: { with: [position] } }, row => { void row[position]; });
|
|
30
|
+
const frame = System.create([spawnCells, move, deriveCells, pick, watch]);
|
|
31
|
+
|
|
32
|
+
describe("System.describe", () => {
|
|
33
|
+
it("gives tiers in run order, readonly systems apart, and what each system uses, as data", () => {
|
|
34
|
+
const described = System.describe(frame);
|
|
35
|
+
expect(described.tiers.map(tier => tier.systems.map(system => system.name))).toEqual([["spawnCells"], ["move"], ["deriveCells"], ["pick"]]);
|
|
36
|
+
expect(described.readonly.map(system => system.system)).toEqual([watch]);
|
|
37
|
+
const [[spawning], [moving], [deriving], [picking]] = described.tiers.map(tier => tier.systems);
|
|
38
|
+
expect(spawning?.commands).toEqual({ spawn: [position], add: [], remove: [], despawn: false });
|
|
39
|
+
expect([moving?.components.write, moving?.resources.read, moving?.commands?.add]).toEqual([[position], [speed], []]);
|
|
40
|
+
expect([deriving?.components.read, deriving?.components.not, deriving?.commands?.add]).toEqual([[position], [cell], [cell]]);
|
|
41
|
+
expect([picking?.lookups, picking?.resources.write]).toEqual([[{ name: "cells", index: "byCell", read: [] }], [hover]]);
|
|
42
|
+
expect(moving?.after).toEqual([{ system: spawnCells, name: "spawnCells", because: "after", conflicts: [] }]);
|
|
43
|
+
expect(deriving?.after).toEqual([{ system: move, name: "move", because: "after", conflicts: [position] }]);
|
|
44
|
+
expect(System.describe(nudge).tiers[0]?.systems[0]?.whole).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
it("gives the fewest sync points between tiers whose commands a later system depends on, and why", () => {
|
|
47
|
+
expect(System.describe(frame).tiers.map(tier => tier.sync)).toEqual([
|
|
48
|
+
[{ from: spawnCells, to: move, keys: [position] }, { from: spawnCells, to: deriveCells, keys: [position] }],
|
|
49
|
+
null,
|
|
50
|
+
[{ from: deriveCells, to: pick, keys: [cell] }],
|
|
51
|
+
null,
|
|
52
|
+
]);
|
|
53
|
+
// Despawning may change anything a later system matches; writing values needs no sync point.
|
|
54
|
+
const clear = System.create({ components: { with: [cell] } }, (row, { commands }) => { commands.despawn(row.entity); });
|
|
55
|
+
const count = System.create({ components: { with: [position] }, after: [clear] }, row => { void row[position]; });
|
|
56
|
+
const tally = System.create({ after: [count] }, ({ resources }) => { resources[hover] = resources[speed]; });
|
|
57
|
+
expect(System.describe(System.create([clear, count, tally])).tiers.map(tier => tier.sync)).toEqual([[{ from: clear, to: count, keys: [] }], null, null]);
|
|
58
|
+
});
|
|
59
|
+
it("lists no conflict on a key two systems reach through rows that never match one entity", () => {
|
|
60
|
+
const placeCells = System.create({ components: { with: [position, cell] } }, row => { row[cell] = row[position]; });
|
|
61
|
+
const clearCells = System.create({ components: { with: [cell], not: [position] }, after: [placeCells] }, row => { row[cell] = 0; });
|
|
62
|
+
expect(System.describe(System.create([placeCells, clearCells])).tiers[1]?.systems[0]?.after).toEqual([{ system: placeCells, name: "placeCells", because: "after", conflicts: [] }]);
|
|
63
|
+
});
|
|
64
|
+
it("says a system follows another by a resource one lists in before or after", () => {
|
|
65
|
+
const setSpeed = System.create({ before: [speed] }, ({ resources }) => { resources[speed] = 2; });
|
|
66
|
+
const readSpeed = System.create({ components: { with: [position] } }, (row, { resources }) => { row[position] = resources[speed]; });
|
|
67
|
+
const readHover = System.create({ after: [hover] }, ({ resources }) => resources[hover] + resources[speed]);
|
|
68
|
+
const setHover = System.create({ after: [setSpeed] }, ({ resources }) => { resources[hover] = 1; });
|
|
69
|
+
const described = System.describe(System.create([readHover, readSpeed, setHover, setSpeed]));
|
|
70
|
+
expect(described.tiers.map(tier => tier.systems.map(system => [system.name, system.after.map(placement => [placement.name, placement.because])]))).toEqual([
|
|
71
|
+
[["setSpeed", []]],
|
|
72
|
+
[["readSpeed", [["setSpeed", "before"]]], ["setHover", [["setSpeed", "after"]]]],
|
|
73
|
+
[["readHover", [["setHover", "after"]]]],
|
|
74
|
+
]);
|
|
75
|
+
// @ts-expect-error ordering takes resources, not components
|
|
76
|
+
expect(() => System.create({ after: [position] }, () => {})).toThrow(`uses resource "describeTest/position", which isn't a registered resource`);
|
|
77
|
+
});
|
|
78
|
+
it("gives a text form to read", () => {
|
|
79
|
+
expect(System.describe(frame).text).toBe([
|
|
80
|
+
"tier 0 spawnCells commands spawn describeTest/position",
|
|
81
|
+
"sync spawnCells's commands of describeTest/position, for move",
|
|
82
|
+
" spawnCells's commands of describeTest/position, for deriveCells",
|
|
83
|
+
"tier 1 move writes describeTest/position",
|
|
84
|
+
" resources reads describeTest/speed",
|
|
85
|
+
" after spawnCells (after)",
|
|
86
|
+
"tier 2 deriveCells reads describeTest/position",
|
|
87
|
+
" requires not describeTest/cell",
|
|
88
|
+
" commands add describeTest/cell",
|
|
89
|
+
" after move (after: describeTest/position)",
|
|
90
|
+
"sync deriveCells's commands of describeTest/cell, for pick",
|
|
91
|
+
"tier 3 pick resources writes describeTest/hover",
|
|
92
|
+
" looks up cells through byCell",
|
|
93
|
+
" after deriveCells (after)",
|
|
94
|
+
"readonly",
|
|
95
|
+
" watch reads describeTest/position",
|
|
96
|
+
].join("\n"));
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { indexDefinitionOf } from '../registry/indexes.js';
|
|
3
|
+
import { labelOf } from '../registry/names.js';
|
|
4
|
+
import { requireDefinition } from '../registry/program.js';
|
|
5
|
+
import { isLeaf, placementsOf, scheduleOf, type Placement } from '../registry/schedule.js';
|
|
6
|
+
import type { Leaf } from './leaf.js';
|
|
7
|
+
import type { System } from './system.js';
|
|
8
|
+
|
|
9
|
+
type Key = symbol;
|
|
10
|
+
|
|
11
|
+
const none: readonly never[] = [];
|
|
12
|
+
|
|
13
|
+
/** A leaf as the system value it is. */
|
|
14
|
+
const asSystem = (leaf: Leaf): System<unknown, unknown> => leaf;
|
|
15
|
+
|
|
16
|
+
/** What the build inferred one system uses, and what placed it, as plain data. */
|
|
17
|
+
const describeLeaf = (leaf: Leaf, placements: readonly Placement[]) => {
|
|
18
|
+
const { components, resources, lookup, up, order } = leaf.declaration;
|
|
19
|
+
const commands = leaf.commands();
|
|
20
|
+
return {
|
|
21
|
+
/** The system, the value its constructor returned. */
|
|
22
|
+
system: asSystem(leaf),
|
|
23
|
+
name: leaf.name,
|
|
24
|
+
components: {
|
|
25
|
+
read: [...components?.read ?? none, ...up === undefined ? none : [up.through]],
|
|
26
|
+
write: components?.write ?? none,
|
|
27
|
+
/** What it requires without reading: `with` members it doesn't use, `added` and `changed` filters' among them. */
|
|
28
|
+
has: components?.has ?? none,
|
|
29
|
+
optional: components?.optional ?? none,
|
|
30
|
+
not: components?.not ?? none,
|
|
31
|
+
added: components?.added ?? none,
|
|
32
|
+
changed: components?.changed ?? none,
|
|
33
|
+
/** What it reads of each row's parent, through `up`. */
|
|
34
|
+
parent: up?.read ?? none,
|
|
35
|
+
},
|
|
36
|
+
resources: {
|
|
37
|
+
read: resources?.read ?? none,
|
|
38
|
+
write: resources?.write ?? none,
|
|
39
|
+
reduce: resources?.reduce ?? none,
|
|
40
|
+
changed: resources?.changed ?? none,
|
|
41
|
+
is: resources?.is ?? none,
|
|
42
|
+
not: resources?.not ?? none,
|
|
43
|
+
},
|
|
44
|
+
lookups: Object.entries(lookup ?? {}).map(([name, used]) => ({
|
|
45
|
+
name,
|
|
46
|
+
/** The label of the index it looks up through, or null for a lookup by entity. */
|
|
47
|
+
index: "index" in used && used.index !== undefined ? indexDefinitionOf(used.index)?.name ?? null : null,
|
|
48
|
+
read: used.read ?? none,
|
|
49
|
+
})),
|
|
50
|
+
/** The label of the sorted index ordering its rows, or null. */
|
|
51
|
+
order: indexDefinitionOf(order)?.name ?? null,
|
|
52
|
+
/** What its commands name by kind, and whether it despawns; null where the build couldn't follow them. */
|
|
53
|
+
commands: commands === null ? null : {
|
|
54
|
+
spawn: commands?.spawn ?? none,
|
|
55
|
+
add: commands?.add ?? none,
|
|
56
|
+
remove: commands?.remove ?? none,
|
|
57
|
+
despawn: commands?.despawn === true,
|
|
58
|
+
},
|
|
59
|
+
/** Whether it takes its row whole (`components: { whole: true }`), every member written. */
|
|
60
|
+
whole: components?.whole === true,
|
|
61
|
+
/** The systems it follows directly, with what ordered it after each and the keys that need that order. */
|
|
62
|
+
after: placements.map(placement => ({ system: asSystem(placement.leaf), name: placement.leaf.name, because: placement.because, conflicts: placement.conflicts })),
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type Described = ReturnType<typeof describeLeaf>;
|
|
67
|
+
|
|
68
|
+
const keys = (list: readonly Key[]): string => list.map(labelOf).join(" ");
|
|
69
|
+
|
|
70
|
+
/** A system's lines of text, after its name in a column of `width`. */
|
|
71
|
+
const linesOf = (system: Described, width: number): readonly string[] => {
|
|
72
|
+
const lines: string[] = [];
|
|
73
|
+
const add = (label: string, text: string): void => { if (text !== "") lines.push(`${label.padEnd(10)}${text}`); };
|
|
74
|
+
const { components, resources, commands } = system;
|
|
75
|
+
add("reads", keys(components.read));
|
|
76
|
+
add("writes", `${keys(components.write)}${system.whole ? " (its whole row)" : ""}`);
|
|
77
|
+
add("requires", [keys(components.has), components.not.length === 0 ? "" : `not ${keys(components.not)}`, components.optional.length === 0 ? "" : `optional ${keys(components.optional)}`].filter(text => text !== "").join(", "));
|
|
78
|
+
add("filters", [...components.added.map(key => `added ${labelOf(key)}`), ...components.changed.map(key => `changed ${labelOf(key)}`), ...resources.changed.map(key => `changed ${labelOf(key)}`)].join(", "));
|
|
79
|
+
add("parent", keys(components.parent));
|
|
80
|
+
add("resources", [resources.read.length === 0 ? "" : `reads ${keys(resources.read)}`, resources.write.length === 0 ? "" : `writes ${keys(resources.write)}`,
|
|
81
|
+
resources.reduce.length === 0 ? "" : `reduces ${keys(resources.reduce)}`, resources.is.length === 0 ? "" : `while ${keys(resources.is)}`,
|
|
82
|
+
resources.not.length === 0 ? "" : `unless ${keys(resources.not)}`].filter(text => text !== "").join(", "));
|
|
83
|
+
for (const lookup of system.lookups) add("looks up", `${lookup.name}${lookup.index === null ? "" : ` through ${lookup.index}`}${lookup.read.length === 0 ? "" : `: ${keys(lookup.read)}`}`);
|
|
84
|
+
if (system.order !== null) add("ordered", `by ${system.order}`);
|
|
85
|
+
if (commands === null) add("commands", "unknown: the build couldn't follow them");
|
|
86
|
+
else add("commands", [commands.spawn.length === 0 ? "" : `spawn ${keys(commands.spawn)}`, commands.add.length === 0 ? "" : `add ${keys(commands.add)}`,
|
|
87
|
+
commands.remove.length === 0 ? "" : `remove ${keys(commands.remove)}`, commands.despawn ? "despawn" : ""].filter(text => text !== "").join(", "));
|
|
88
|
+
add("after", system.after.map(placement => `${placement.name} (${placement.because}${placement.conflicts.length === 0 ? "" : `: ${keys(placement.conflicts)}`})`).join(", "));
|
|
89
|
+
const indent = " ".repeat(width);
|
|
90
|
+
return lines.length === 0 ? [system.name] : lines.map((line, i) => `${i === 0 ? system.name.padEnd(width) : indent}${line}`);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Describes how a system or composite runs: its tiers in run order, systems in one tier having no ordering path between
|
|
95
|
+
* them, each with the sync point after it, if any, and its readonly systems apart, which run after its last sync
|
|
96
|
+
* point. For each system, what the build inferred it reads, writes, looks up and commands, whether it takes its row
|
|
97
|
+
* whole, and the systems it follows directly, with what ordered it after each. `text` gives it all to read; the rest
|
|
98
|
+
* is data to assert on.
|
|
99
|
+
*/
|
|
100
|
+
export const describe = <I, R>(system: System<I, R>) => {
|
|
101
|
+
const definition = requireDefinition(system);
|
|
102
|
+
const schedule = scheduleOf(definition);
|
|
103
|
+
const placements = placementsOf(definition);
|
|
104
|
+
const tiers = schedule.tiers.map((tier, t) => ({
|
|
105
|
+
systems: tier.map(leaf => describeLeaf(leaf, placements.get(leaf) ?? none)),
|
|
106
|
+
/**
|
|
107
|
+
* Why a sync point follows the tier, applying the commands so far: each system whose commands may change what a
|
|
108
|
+
* later one that follows it uses, through `keys`, none listed where it despawns or a cascade could; null for none.
|
|
109
|
+
*/
|
|
110
|
+
sync: schedule.syncs[t]?.map(reason => ({ from: asSystem(reason.from), to: asSystem(reason.to), keys: reason.keys })) ?? null,
|
|
111
|
+
}));
|
|
112
|
+
const readonly = schedule.observer.flatMap(member => isLeaf(member) ? [describeLeaf(member, none)] : []);
|
|
113
|
+
const width = Math.max(0, ...[...tiers.flatMap(tier => tier.systems), ...readonly].map(described => described.name.length)) + 2;
|
|
114
|
+
const text = [
|
|
115
|
+
...tiers.flatMap((tier, t) => [
|
|
116
|
+
...tier.systems.flatMap((described, i) => linesOf(described, width).map((line, l) => `${(i === 0 && l === 0 ? `tier ${t}` : "").padEnd(8)}${line}`)),
|
|
117
|
+
...(tier.sync ?? none).map((reason, r) => `${(r === 0 ? "sync" : "").padEnd(8)}${reason.from.name}'s commands${reason.keys.length === 0 ? "" : ` of ${keys(reason.keys)}`}, for ${reason.to.name}`),
|
|
118
|
+
]),
|
|
119
|
+
...readonly.length === 0 ? none : ["readonly", ...readonly.flatMap(described => linesOf(described, width).map(line => `${"".padEnd(8)}${line}`))],
|
|
120
|
+
].map(line => line.trimEnd()).join("\n");
|
|
121
|
+
return { tiers, readonly, text };
|
|
122
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
2
|
import type { Schema } from '../schema/schema.js';
|
|
3
|
+
import type { CommandLists } from './access.js';
|
|
3
4
|
import type { Resolved } from './declaration.js';
|
|
4
5
|
import type { Name } from './name.js';
|
|
5
6
|
|
|
@@ -17,4 +18,10 @@ export type Leaf = {
|
|
|
17
18
|
readonly declaration: Resolved;
|
|
18
19
|
readonly input: InputSchema | undefined;
|
|
19
20
|
readonly body: (...parameters: never[]) => void;
|
|
21
|
+
/**
|
|
22
|
+
* What its body's commands name, as the build inferred them, read at first need: null where it couldn't follow
|
|
23
|
+
* them, undefined for none. Apart from its declaration, so a module evaluated again with other commands declares the
|
|
24
|
+
* same system.
|
|
25
|
+
*/
|
|
26
|
+
readonly commands: () => CommandLists | null | undefined;
|
|
20
27
|
};
|
|
@@ -11,7 +11,7 @@ import type { Body } from './body.js';
|
|
|
11
11
|
import type { ReturnsNoPromise } from './input.js';
|
|
12
12
|
import { accessorsOf } from '../gpu/accessors.js';
|
|
13
13
|
import { checkLeaf } from './check.js';
|
|
14
|
-
import { accessOf, compact, resourceUses } from './resolve.js';
|
|
14
|
+
import { accessOf, compact, noCommands, resourceUses } from './resolve.js';
|
|
15
15
|
import type { Leaf } from './leaf.js';
|
|
16
16
|
import type { StructComponent } from './struct.js';
|
|
17
17
|
import type { Shared } from '../component/value.js';
|
|
@@ -96,6 +96,7 @@ export const render = <const D extends RenderDeclaration, B extends Body<D>>(
|
|
|
96
96
|
},
|
|
97
97
|
input: undefined,
|
|
98
98
|
body,
|
|
99
|
+
commands: noCommands,
|
|
99
100
|
};
|
|
100
101
|
if (uses.write.length > 0 || uses.reduce.length > 0) throw new Error(`${what} writes or reduces into resources; a render system only reads.`);
|
|
101
102
|
checkLeaf(system);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
import "../registry/declarations.js";
|
|
4
|
-
import { componentLists, lookupsLists, resourceUses, type Access } from "./resolve.js";
|
|
4
|
+
import { commandListsOf, componentLists, lookupsLists, resourceUses, type Access } from "./resolve.js";
|
|
5
5
|
|
|
6
6
|
const mass = Symbol("resolveTest/mass");
|
|
7
7
|
const pool = Symbol("resolveTest/pool");
|
|
@@ -47,3 +47,19 @@ describe("resourceUses", () => {
|
|
|
47
47
|
expect(() => resourceUses("System \"s\"", changing(pool), undefined)).not.toThrow();
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
|
+
|
|
51
|
+
describe("commandListsOf", () => {
|
|
52
|
+
const commanding = (commands: () => unknown): Access => ({ components: null, resources: null, lookup: null, up: null, commands, at: "a test" });
|
|
53
|
+
it("reads what the commands name at the first call, once", () => {
|
|
54
|
+
let reads = 0;
|
|
55
|
+
const lists = commandListsOf("System \"s\"", commanding(() => { reads++; return { spawn: [[mass, position]], add: [mass], remove: [], despawn: false }; }));
|
|
56
|
+
expect(reads).toBe(0);
|
|
57
|
+
expect([lists(), lists(), reads]).toEqual([{ spawn: [mass, position], add: [mass] }, { spawn: [mass, position], add: [mass] }, 1]);
|
|
58
|
+
});
|
|
59
|
+
it("names the system and the declaration its commands name before it's initialized", () => {
|
|
60
|
+
for (const message of ["Cannot access 'standard' before initialization", "can't access lexical declaration 'standard' before initialization"]) {
|
|
61
|
+
const lists = commandListsOf("System \"s\"", commanding(() => { throw new ReferenceError(message); }));
|
|
62
|
+
expect(lists).toThrow(`System "s" names "standard" in its commands before it's initialized: declare it above the system.`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -3,7 +3,7 @@ import { takeAccessOf } from '../registry/ids.js';
|
|
|
3
3
|
import { indexDefinitionOf } from '../registry/indexes.js';
|
|
4
4
|
import { declarationOf, labelOf } from '../registry/names.js';
|
|
5
5
|
import { isString, isStruct } from '../registry/schemas.js';
|
|
6
|
-
import type { ComponentAccess, ComponentLists, Lookup, SystemLookup, SystemLookupLists } from './access.js';
|
|
6
|
+
import type { CommandLists, ComponentAccess, ComponentLists, Lookup, SystemLookup, SystemLookupLists } from './access.js';
|
|
7
7
|
|
|
8
8
|
type Key = symbol;
|
|
9
9
|
|
|
@@ -23,11 +23,26 @@ export type Access = {
|
|
|
23
23
|
readonly up: readonly Key[] | null;
|
|
24
24
|
/** The resources the predicates of its declaration's `where` read; absent without predicates. */
|
|
25
25
|
readonly where?: readonly Key[] | null;
|
|
26
|
+
/**
|
|
27
|
+
* What its commands name, keys or lists of them, by kind, and whether it despawns, or a function giving that; absent
|
|
28
|
+
* where no build said.
|
|
29
|
+
*/
|
|
30
|
+
readonly commands?: unknown;
|
|
26
31
|
readonly at: string;
|
|
27
32
|
};
|
|
28
33
|
|
|
34
|
+
type Named = readonly (Key | readonly Key[])[];
|
|
35
|
+
type Commanded = { readonly spawn: Named; readonly add: Named; readonly remove: Named; readonly despawn: boolean };
|
|
36
|
+
|
|
29
37
|
const isKeys = (value: unknown): value is readonly Key[] => Array.isArray(value) && value.every(key => typeof key === "symbol");
|
|
30
38
|
|
|
39
|
+
const isNamed = (value: unknown): value is Named => Array.isArray(value) && value.every(item => typeof item === "symbol" || isKeys(item));
|
|
40
|
+
|
|
41
|
+
/** A binding a command named that holds neither a key nor a list of them isn't followed. */
|
|
42
|
+
const isCommanded = (value: unknown): value is Commanded => typeof value === "object" && value !== null
|
|
43
|
+
&& isNamed(Reflect.get(value, "spawn")) && isNamed(Reflect.get(value, "add")) && isNamed(Reflect.get(value, "remove"))
|
|
44
|
+
&& typeof Reflect.get(value, "despawn") === "boolean";
|
|
45
|
+
|
|
31
46
|
const isUsed = (value: unknown): value is Used => typeof value === "object" && value !== null
|
|
32
47
|
&& isKeys(Reflect.get(value, "read")) && isKeys(Reflect.get(value, "write"))
|
|
33
48
|
&& (Reflect.get(value, "mutate") === undefined || isKeys(Reflect.get(value, "mutate")));
|
|
@@ -106,7 +121,7 @@ export const componentLists = (components: ComponentAccess, used: Used | null, r
|
|
|
106
121
|
: used.write;
|
|
107
122
|
const read = used === null ? without(held, write) : without(used.read, write);
|
|
108
123
|
const has = [...without(members, read, write), ...without(filtered, read, write)];
|
|
109
|
-
return compact({ read, write, has, optional, not: components.not, added: components.added, changed: components.changed, where, whereReads });
|
|
124
|
+
return compact({ read, write, has, optional, not: components.not, added: components.added, changed: components.changed, where, whereReads, whole: components.whole });
|
|
110
125
|
};
|
|
111
126
|
|
|
112
127
|
/**
|
|
@@ -161,5 +176,48 @@ export const whereReadsOf = (what: string, components: ComponentAccess | undefin
|
|
|
161
176
|
return reads;
|
|
162
177
|
};
|
|
163
178
|
|
|
179
|
+
/**
|
|
180
|
+
* What a body's commands name, as the world takes it, read at the first call and kept: null where the build couldn't
|
|
181
|
+
* follow them, nothing for none. The build gives them as a function, so a list declared below the system is read once
|
|
182
|
+
* its module has loaded; one still uninitialized then, as an import cycle leaves it, throws naming it.
|
|
183
|
+
*/
|
|
184
|
+
export const commandListsOf = (what: string, access: Access): (() => CommandLists | null | undefined) => {
|
|
185
|
+
let read = false;
|
|
186
|
+
let lists: CommandLists | null | undefined;
|
|
187
|
+
return () => {
|
|
188
|
+
if (!read) {
|
|
189
|
+
lists = listsOf(what, access.commands);
|
|
190
|
+
read = true;
|
|
191
|
+
}
|
|
192
|
+
return lists;
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/** The command lists of a system whose body issues none. */
|
|
197
|
+
export const noCommands = (): undefined => undefined;
|
|
198
|
+
|
|
199
|
+
const listsOf = (what: string, given: unknown): CommandLists | null | undefined => {
|
|
200
|
+
const commands = typeof given === "function" ? evaluated(what, () => Reflect.apply(given, undefined, [])) : given;
|
|
201
|
+
if (commands === undefined) return undefined;
|
|
202
|
+
if (!isCommanded(commands)) return null;
|
|
203
|
+
if (!commands.despawn && commands.spawn.length + commands.add.length + commands.remove.length === 0) return undefined;
|
|
204
|
+
const flat = (named: Named): Key[] => [...new Set(named.flat())];
|
|
205
|
+
const lists = compact({ spawn: flat(commands.spawn), add: flat(commands.add), remove: flat(commands.remove), despawn: commands.despawn ? true as const : undefined });
|
|
206
|
+
return Object.keys(lists).length === 0 ? undefined : lists;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/** What the build's function of a body's commands gives. */
|
|
210
|
+
const evaluated = (what: string, commands: () => unknown): unknown => {
|
|
211
|
+
try {
|
|
212
|
+
return commands();
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (!(error instanceof ReferenceError)) throw error;
|
|
215
|
+
// Engines quote the binding in the message: "Cannot access 'standard' before initialization", or "can't access
|
|
216
|
+
// lexical declaration 'standard' before initialization".
|
|
217
|
+
const binding = /'([A-Za-z_$][\w$]*)'/.exec(error.message)?.[1];
|
|
218
|
+
throw new Error(`${what} names ${binding === undefined ? "a declaration" : `"${binding}"`} in its commands before it's initialized: declare it above the system.`, { cause: error });
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
164
222
|
/** What a body reads of its parent's values. */
|
|
165
223
|
export const upReads = (what: string, access: Access): readonly Key[] => followed(what, access.up, "its parent's values", access);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import type { Shared } from '../component/value.js';
|
|
3
|
+
|
|
4
|
+
/** A `where`'s keys that aren't shared components, each an error: `where` names only shared components. */
|
|
5
|
+
export type SharedOnly<W> = { readonly [K in Exclude<keyof W, Shared>]: { readonly "error: where names only shared components, which each archetype holds one value of": K } };
|
|
@@ -154,6 +154,12 @@ System.create({
|
|
|
154
154
|
// @ts-expect-error ordering takes systems
|
|
155
155
|
after: ["test/unregistered"],
|
|
156
156
|
}, () => {});
|
|
157
|
+
// Ordering by a resource: before or after every other system of the composite that uses it.
|
|
158
|
+
System.create({ before: [time], after: [integrate, score] }, ({ resources }) => { resources[time] = resources[score]; });
|
|
159
|
+
System.create({
|
|
160
|
+
// @ts-expect-error ordering takes systems and resources, not components
|
|
161
|
+
after: [position],
|
|
162
|
+
}, () => {});
|
|
157
163
|
|
|
158
164
|
// @ts-expect-error a system with input can't use change filters
|
|
159
165
|
System.create({ components: { changed: [position] } }, (_row, _context, input: { readonly distance: number }) => { void input; });
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
2
|
import type { Component } from '../component/component.js';
|
|
3
|
+
import type { Shared } from '../component/value.js';
|
|
3
4
|
import type { Resource } from '../resource/resource.js';
|
|
4
5
|
import type { Table } from '../world/table/table.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* What a readonly system's `tables` reads: of every archetype matching `with` and `
|
|
8
|
+
* What a readonly system's `tables` reads: of every archetype matching `with`, `not` and `where` that holds any of
|
|
8
9
|
* `components`, a table of those it holds; and, with `resources`, each resource's value.
|
|
9
10
|
*/
|
|
10
11
|
export type TablesDeclaration = {
|
|
11
12
|
readonly components: readonly Component.Key[];
|
|
12
13
|
readonly with?: readonly Component.Key[];
|
|
13
14
|
readonly not?: readonly Component.Key[];
|
|
15
|
+
/**
|
|
16
|
+
* Keeps the tables to the archetypes holding these values of shared components, each one required as `with`
|
|
17
|
+
* requires it, as a system's `where` of values keeps its system to them.
|
|
18
|
+
*/
|
|
19
|
+
readonly where?: { readonly [K in Shared]?: Component<K> };
|
|
14
20
|
readonly resources?: readonly Resource.Key[];
|
|
15
21
|
};
|
|
16
22
|
|
|
@@ -62,7 +62,17 @@ void ecs.Report;
|
|
|
62
62
|
// @ts-expect-error no run numbers
|
|
63
63
|
const numbered: Promise<number> = world.run(main);
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
// Asked for its changes, after its input, a run resolves to them, in the shape changes finalizers take.
|
|
66
|
+
const changes = world.run(move, { distance: 1 }, { changes: true });
|
|
67
|
+
const alone = world.run(main, { changes: true });
|
|
68
|
+
check<Equal<typeof changes, typeof alone>>();
|
|
69
|
+
check<Equal<Awaited<typeof alone>["resources"][number]["key"], Ecs.Resource.Key>>();
|
|
70
|
+
// @ts-expect-error its input comes first
|
|
71
|
+
void world.run(move, { changes: true }, { distance: 1 });
|
|
72
|
+
// @ts-expect-error a run asks only for its changes
|
|
73
|
+
void world.run(main, { changes: false });
|
|
74
|
+
|
|
75
|
+
void [counted, notText, nextValue, scaledValue, drawn, rows, composite, input, nothing, numbered, time, changes];
|
|
66
76
|
|
|
67
77
|
// A body returns no promise: it starts the work, and a later run finishes it.
|
|
68
78
|
// @ts-expect-error a body returns no promise
|
|
@@ -13,7 +13,8 @@ export type Column =
|
|
|
13
13
|
| { readonly kind: "value"; readonly value: unknown };
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* What `commands.spawnTables` spawned: each table's
|
|
17
|
-
*
|
|
16
|
+
* What `commands.spawnTables` spawned: each table's entities in its row order, the one spawned for each row, the one
|
|
17
|
+
* existing for a row its `existing` maps, and `Entity.none` for a row it didn't spawn; and how many rows it didn't,
|
|
18
|
+
* those owned by an entity outside the tables.
|
|
18
19
|
*/
|
|
19
20
|
export type Spawned = { readonly entities: readonly Float64Array[]; readonly dropped: number };
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
2
|
import type { Input } from '../system/input.js';
|
|
3
3
|
import type { System } from '../system/system.js';
|
|
4
|
+
import type { ChangeSet } from '../finalizer/body.js';
|
|
5
|
+
import type { Essential } from '../component/value.js';
|
|
6
|
+
import type { Resource } from '../resource/resource.js';
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* A world. Systems read and change it; readonly systems read it after each run's and tick's sync point. Every member's
|
|
@@ -23,6 +26,16 @@ export type World = {
|
|
|
23
26
|
* added or GPU work, is reported out of band, as ticks' failures are (see `add`).
|
|
24
27
|
*/
|
|
25
28
|
run<I, R>(system: System<I, R>, ...input: Input<NoInfer<I>>): Promise<R>;
|
|
29
|
+
/**
|
|
30
|
+
* Runs as `run` does, and resolves to this run's own changes, in the shape `Finalizer.changes` gives, rather than its
|
|
31
|
+
* value: the structural changes its commands made, rules and cascades included, to every component that isn't
|
|
32
|
+
* derived or an object a joined thread fills, the values its adds gave components entities had before and after, and
|
|
33
|
+
* each resource its systems write or reduce into whose value changed, compared as `Finalizer.changes` compares them;
|
|
34
|
+
* `previous` and `next` give values before and after the run. Commands the finalizers of an earlier sync point
|
|
35
|
+
* queued apply first, at a sync point of their own. Values written to rows aren't structural changes, so they're in
|
|
36
|
+
* none. The run pays for this only when asked. Only on the world's thread: a joined thread's rejects.
|
|
37
|
+
*/
|
|
38
|
+
run<I, R>(system: System<I, R>, ...input: [...Input<NoInfer<I>>, options: { readonly changes: true }]): Promise<ChangeSet<Essential, Resource.Key>>;
|
|
26
39
|
/**
|
|
27
40
|
* Adds a system to every tick, from the next one on, ordered with those already added by its constraints, as a
|
|
28
41
|
* composite's members are. A tick runs on a beat of the `cadence` resource where `always` is set, `wake` is due or
|