@adobe/ecs 0.2.3 → 0.2.4
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/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 +4 -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/world/activate.js +14 -2
- 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 +40 -5
- package/dist/implementation/world/commands.js.map +1 -1
- package/dist/implementation/world/create.js +53 -2
- package/dist/implementation/world/create.js.map +1 -1
- package/dist/implementation/world/execute.d.ts +4 -4
- package/dist/implementation/world/execute.js +132 -83
- package/dist/implementation/world/execute.js.map +1 -1
- package/dist/implementation/world/finalizers.d.ts +2 -0
- package/dist/implementation/world/finalizers.js +24 -3
- 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/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 +13 -0
- package/dist/implementation/world/state.js +13 -1
- package/dist/implementation/world/state.js.map +1 -1
- package/dist/implementation/world/synchronize.js +72 -22
- 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 +3 -2
- package/dist/implementation/world/tables.js +83 -37
- 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 +18 -3
- package/dist/interface/finalizer/create.d.ts +7 -5
- package/dist/interface/finalizer/create.js +8 -5
- package/dist/interface/finalizer/create.js.map +1 -1
- package/dist/interface/finalizer/declaration.d.ts +4 -2
- 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/schedule.d.ts +28 -1
- package/dist/interface/registry/schedule.js +182 -21
- 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 +17 -3
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +26 -5
- package/dist/interface/system/create-resolved.js +1 -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/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 +6 -0
- package/dist/interface/system/render.js +1 -0
- package/dist/interface/system/render.js.map +1 -1
- package/dist/interface/system/resolve.d.ts +5 -1
- package/dist/interface/system/resolve.js +19 -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/03-systems.out +1 -1
- package/src/guide/06-sync-points.md +12 -8
- package/src/guide/06-sync-points.out +1 -2
- package/src/guide/06-sync-points.ts +9 -5
- package/src/guide/18-identity.out +1 -1
- package/src/guide/guide.test.ts +3 -150
- 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 +4 -2
- package/src/implementation/compiler/transform.test.ts +41 -21
- package/src/implementation/compiler/transform.ts +5 -3
- package/src/implementation/world/activate.ts +11 -2
- 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 +36 -5
- package/src/implementation/world/create.test.ts +8 -8
- package/src/implementation/world/create.ts +57 -2
- package/src/implementation/world/despawn-where.test.ts +287 -0
- package/src/implementation/world/execute.ts +125 -78
- package/src/implementation/world/finalizers.ts +27 -4
- 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/names.test.ts +1 -2
- package/src/implementation/world/perf.md +3 -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/runner.ts +17 -13
- package/src/implementation/world/shared.test.ts +2 -2
- package/src/implementation/world/state.ts +19 -0
- package/src/implementation/world/sync-points.test.ts +135 -0
- package/src/implementation/world/synchronize.ts +65 -18
- package/src/implementation/world/tables-read.ts +16 -5
- package/src/implementation/world/tables.test.ts +106 -4
- package/src/implementation/world/tables.ts +71 -33
- 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/where.ts +38 -10
- package/src/interface/finalizer/body.ts +13 -3
- package/src/interface/finalizer/create.test.ts +3 -1
- package/src/interface/finalizer/create.ts +12 -7
- package/src/interface/finalizer/declaration.ts +4 -2
- 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/schedule.ts +196 -23
- package/src/interface/system/access.ts +16 -0
- package/src/interface/system/check.ts +15 -2
- package/src/interface/system/commands.ts +23 -4
- package/src/interface/system/commands.type-test.ts +31 -0
- package/src/interface/system/create-resolved.ts +1 -1
- package/src/interface/system/create.test.ts +8 -0
- package/src/interface/system/create.ts +9 -5
- package/src/interface/system/describe.test.ts +79 -0
- package/src/interface/system/describe.ts +122 -0
- package/src/interface/system/index.ts +1 -0
- package/src/interface/system/leaf.ts +6 -0
- package/src/interface/system/render.ts +1 -0
- package/src/interface/system/resolve.ts +25 -2
- package/src/interface/system/shared-only.ts +5 -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 +18 -0
- package/src/perftest/structural.ts +20 -0
- package/src/perftest/tables.ts +107 -0
|
@@ -4,8 +4,8 @@ import type { Definition } from '../system/definition.js';
|
|
|
4
4
|
import type { Leaf } from '../system/leaf.js';
|
|
5
5
|
import { firstItemOf } from './items.js';
|
|
6
6
|
import { requireDefinition } from './program.js';
|
|
7
|
-
import { firstKeyOf, labelOf, sameDeclaration } from './names.js';
|
|
8
|
-
import { lookupKeysOf, orderKeysOf } from './indexes.js';
|
|
7
|
+
import { declarationOf, firstKeyOf, labelOf, sameDeclaration } from './names.js';
|
|
8
|
+
import { indexDefinitionOf, lookupKeysOf, orderKeysOf } from './indexes.js';
|
|
9
9
|
|
|
10
10
|
type Key = symbol;
|
|
11
11
|
|
|
@@ -29,9 +29,15 @@ const checkInputs = (leaves: readonly Leaf[]): void => {
|
|
|
29
29
|
/**
|
|
30
30
|
* Leaves in run order, grouped into tiers. Leaves in one tier have no ordering path between them. `reductions` are the
|
|
31
31
|
* resources its leaves reduce into, each with how many of them contribute. `observer` are its readonly leaves, in no
|
|
32
|
-
* tier: they run after its sync point, ordered with nothing.
|
|
32
|
+
* tier: they run after its sync point, ordered with nothing. `syncs` holds, after each tier, the reasons for a sync
|
|
33
|
+
* point there, which applies the commands so far before the next tier, or null for none.
|
|
33
34
|
*/
|
|
34
|
-
export type Schedule = {
|
|
35
|
+
export type Schedule = {
|
|
36
|
+
readonly tiers: readonly (readonly Leaf[])[];
|
|
37
|
+
readonly reductions: ReadonlyMap<Key, number>;
|
|
38
|
+
readonly observer: readonly Leaf[];
|
|
39
|
+
readonly syncs: readonly (readonly SyncReason[] | null)[];
|
|
40
|
+
};
|
|
35
41
|
|
|
36
42
|
export const isLeaf = (system: Definition): system is Leaf => system.kind !== "composite";
|
|
37
43
|
|
|
@@ -42,7 +48,14 @@ export const isObserver = (system: Definition): system is Leaf & { readonly decl
|
|
|
42
48
|
const start = (unit: number): number => 2 * unit;
|
|
43
49
|
const end = (unit: number): number => 2 * unit + 1;
|
|
44
50
|
|
|
45
|
-
type Graph = {
|
|
51
|
+
type Graph = {
|
|
52
|
+
readonly units: readonly Definition[];
|
|
53
|
+
readonly successors: readonly Set<number>[];
|
|
54
|
+
/** Each unit's members, by unit: none for a leaf. */
|
|
55
|
+
readonly members: readonly (readonly number[])[];
|
|
56
|
+
/** The units a list of systems names, readonly systems left out. */
|
|
57
|
+
readonly resolve: (systems: readonly object[] | undefined) => readonly number[];
|
|
58
|
+
};
|
|
46
59
|
|
|
47
60
|
/**
|
|
48
61
|
* A leaf runs from its start to its end, and a composite spans its members. `after` and `before` order one system's end
|
|
@@ -86,7 +99,7 @@ const graphOf = (root: Definition): Graph => {
|
|
|
86
99
|
successors[end(unit)].add(end(other));
|
|
87
100
|
}
|
|
88
101
|
});
|
|
89
|
-
return { units, successors };
|
|
102
|
+
return { units, successors, members, resolve };
|
|
90
103
|
};
|
|
91
104
|
|
|
92
105
|
/** Kahn's algorithm over nodes. Throws on a cycle. */
|
|
@@ -165,6 +178,74 @@ const conflictOf = (a: Access, b: Access): Key | undefined => {
|
|
|
165
178
|
return undefined;
|
|
166
179
|
};
|
|
167
180
|
|
|
181
|
+
/** Every key two accesses conflict on. */
|
|
182
|
+
const conflictsOf = (a: Access, b: Access): readonly Key[] => [...new Set([
|
|
183
|
+
...[...a.writes].filter(key => b.writes.has(key) || b.reads.has(key) || b.reduces.has(key)),
|
|
184
|
+
...[...b.writes].filter(key => a.reads.has(key) || a.reduces.has(key)),
|
|
185
|
+
])];
|
|
186
|
+
|
|
187
|
+
/** A system's leaves in its tiers, with their units and accesses, and its graph with readers of folds after their contributors. */
|
|
188
|
+
const orderedOf = (system: Definition) => {
|
|
189
|
+
const graph = graphOf(system);
|
|
190
|
+
const leaves = graph.units.flatMap((unit, index) => isLeaf(unit) && !isObserver(unit) ? [{ leaf: unit, unit: index }] : []);
|
|
191
|
+
const accesses = leaves.map(({ leaf }) => accessOf(leaf));
|
|
192
|
+
// Leaves that read a resource run after every leaf that reduces into it, so they read the finished fold.
|
|
193
|
+
const reductions = new Map<Key, number>();
|
|
194
|
+
accesses.forEach((access, contributor) => {
|
|
195
|
+
for (const key of access.reduces) {
|
|
196
|
+
reductions.set(key, (reductions.get(key) ?? 0) + 1);
|
|
197
|
+
accesses.forEach((other, reader) => {
|
|
198
|
+
if (reader !== contributor && other.reads.has(key)) graph.successors[end(leaves[contributor].unit)].add(start(leaves[reader].unit));
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
return { graph, leaves, accesses, reductions };
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* What put a leaf after another: a constraint of either, or of a composite holding it, naming the other or a composite
|
|
207
|
+
* holding it (`after`, `before`), its reading a fold the other contributes to (`reduction`), or being placed during a
|
|
208
|
+
* span (`during`). `conflicts` are the keys both access, one writing, which need the order.
|
|
209
|
+
*/
|
|
210
|
+
export type Placement = { readonly leaf: Leaf; readonly because: "after" | "before" | "during" | "reduction"; readonly conflicts: readonly Key[] };
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* For each leaf in a system's tiers, the leaves it follows directly: those with an ordering path to it through no other
|
|
214
|
+
* leaf of its tiers.
|
|
215
|
+
*/
|
|
216
|
+
export const placementsOf = (system: Definition): ReadonlyMap<Leaf, readonly Placement[]> => {
|
|
217
|
+
const { graph, leaves, accesses } = orderedOf(system);
|
|
218
|
+
const reach = reachability(topologicalOrder(graph), graph.successors);
|
|
219
|
+
const ancestors = graph.units.map(() => [] as number[]);
|
|
220
|
+
graph.members.forEach((members, unit) => {
|
|
221
|
+
const visit = (member: number): void => {
|
|
222
|
+
ancestors[member].push(unit);
|
|
223
|
+
for (const inner of graph.members[member]) visit(inner);
|
|
224
|
+
};
|
|
225
|
+
for (const member of members) visit(member);
|
|
226
|
+
});
|
|
227
|
+
/** Whether `systems` names `unit` or a composite holding it. */
|
|
228
|
+
const names = (systems: readonly object[] | undefined, unit: number): boolean => {
|
|
229
|
+
const named = graph.resolve(systems);
|
|
230
|
+
return named.includes(unit) || ancestors[unit].some(holder => named.includes(holder));
|
|
231
|
+
};
|
|
232
|
+
const constrains = (unit: number, list: "after" | "before", other: number): boolean =>
|
|
233
|
+
[unit, ...ancestors[unit]].some(holder => holder !== 0 && names(graph.units[holder].declaration[list], other));
|
|
234
|
+
const placements = new Map<Leaf, readonly Placement[]>();
|
|
235
|
+
leaves.forEach(({ leaf, unit }, b) => {
|
|
236
|
+
const before = leaves.flatMap((other, a) => a !== b && reaches(reach, end(other.unit), start(unit)) ? [a] : []);
|
|
237
|
+
const direct = before.filter(a => !before.some(other => other !== a && reaches(reach, end(leaves[a].unit), start(leaves[other].unit))));
|
|
238
|
+
placements.set(leaf, direct.map(a => {
|
|
239
|
+
const other = leaves[a];
|
|
240
|
+
const because = constrains(unit, "after", other.unit) ? "after"
|
|
241
|
+
: constrains(other.unit, "before", unit) ? "before"
|
|
242
|
+
: [...accesses[a].reduces].some(key => accesses[b].reads.has(key)) ? "reduction" : "during";
|
|
243
|
+
return { leaf: other.leaf, because, conflicts: conflictsOf(accesses[a], accesses[b]) };
|
|
244
|
+
}));
|
|
245
|
+
});
|
|
246
|
+
return placements;
|
|
247
|
+
};
|
|
248
|
+
|
|
168
249
|
const schedules = new WeakMap<Definition, Schedule>();
|
|
169
250
|
|
|
170
251
|
/** A system's schedule, created once per system. Throws on a cycle, or on two conflicting leaves with no ordering path between them. */
|
|
@@ -179,33 +260,125 @@ export const scheduleOf = (system: Definition): Schedule => {
|
|
|
179
260
|
* or on inputs that don't merge.
|
|
180
261
|
*/
|
|
181
262
|
const createSchedule = (system: Definition): Schedule => {
|
|
182
|
-
const graph =
|
|
263
|
+
const { graph, leaves, accesses, reductions } = orderedOf(system);
|
|
183
264
|
const observer = graph.units.filter(isObserver);
|
|
184
|
-
const leaves = graph.units.flatMap((unit, index) => isLeaf(unit) && !isObserver(unit) ? [{ leaf: unit, unit: index }] : []);
|
|
185
265
|
checkInputs(leaves.map(({ leaf }) => leaf));
|
|
186
|
-
const accesses = leaves.map(({ leaf }) => accessOf(leaf));
|
|
187
|
-
// Leaves that read a resource run after every leaf that reduces into it, so they read the finished fold.
|
|
188
|
-
const reductions = new Map<Key, number>();
|
|
189
|
-
accesses.forEach((access, contributor) => {
|
|
190
|
-
for (const key of access.reduces) {
|
|
191
|
-
reductions.set(key, (reductions.get(key) ?? 0) + 1);
|
|
192
|
-
accesses.forEach((other, reader) => {
|
|
193
|
-
if (reader !== contributor && other.reads.has(key)) graph.successors[end(leaves[contributor].unit)].add(start(leaves[reader].unit));
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
266
|
const order = topologicalOrder(graph);
|
|
198
267
|
const reach = reachability(order, graph.successors);
|
|
199
268
|
for (let a = 0; a < leaves.length; a++) {
|
|
200
269
|
for (let b = a + 1; b < leaves.length; b++) {
|
|
201
270
|
const key = conflictOf(accesses[a], accesses[b]);
|
|
202
271
|
if (key !== undefined && !ordered(reach, leaves[a].unit, leaves[b].unit)) {
|
|
203
|
-
|
|
272
|
+
const whole = [leaves[a].leaf, leaves[b].leaf].filter(leaf => leaf.declaration.components?.whole === true)
|
|
273
|
+
.map(leaf => ` "${leaf.name}" takes its row whole (components.whole), so it writes every member.`).join("");
|
|
274
|
+
throw new Error(`Systems "${leaves[a].leaf.name}" and "${leaves[b].leaf.name}" both access "${labelOf(key)}" and one writes it; order them with before or after.${whole}`);
|
|
204
275
|
}
|
|
205
276
|
}
|
|
206
277
|
}
|
|
207
278
|
const depth = depthsOf(graph, order);
|
|
208
|
-
const
|
|
209
|
-
for (const { leaf, unit } of leaves) (
|
|
210
|
-
|
|
279
|
+
const byDepth: Leaf[][] = [];
|
|
280
|
+
for (const { leaf, unit } of leaves) (byDepth[depth[start(unit)]] ??= []).push(leaf);
|
|
281
|
+
const tiers = byDepth.filter(tier => tier !== undefined);
|
|
282
|
+
// A sync point comes between tiers, so a schedule of one tier has none.
|
|
283
|
+
if (tiers.length < 2) return { tiers, reductions, observer, syncs: tiers.map(() => null) };
|
|
284
|
+
const tierOf = new Map<Leaf, number>();
|
|
285
|
+
tiers.forEach((tier, t) => { for (const leaf of tier) tierOf.set(leaf, t); });
|
|
286
|
+
const edges: { readonly lo: number; readonly hi: number; readonly reason: SyncReason }[] = [];
|
|
287
|
+
leaves.forEach((from, a) => {
|
|
288
|
+
const changed = structuralOf(from.leaf);
|
|
289
|
+
if (changed === null) return;
|
|
290
|
+
leaves.forEach((to, b) => {
|
|
291
|
+
if (a === b || !reaches(reach, end(from.unit), start(to.unit))) return;
|
|
292
|
+
const keys = affected(changed, touchesOf(to.leaf));
|
|
293
|
+
if (keys !== null) edges.push({ lo: tierOf.get(from.leaf) ?? 0, hi: (tierOf.get(to.leaf) ?? 0) - 1, reason: { from: from.leaf, to: to.leaf, keys } });
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
return { tiers, reductions, observer, syncs: syncsOf(tiers.length, edges) };
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Why a sync point follows a tier: a system in it or before it whose commands may change, structurally, what a system
|
|
301
|
+
* after the sync point that follows it reads, matches, looks up or indexes. `keys` are the components its commands name
|
|
302
|
+
* that the later one uses, none where it despawns, or where its commands or a cascade could change anything.
|
|
303
|
+
*/
|
|
304
|
+
export type SyncReason = { readonly from: Leaf; readonly to: Leaf; readonly keys: readonly Key[] };
|
|
305
|
+
|
|
306
|
+
/** The components a leaf's commands may add or remove, by spawn, add or remove; `"any"` once it despawns or isn't followed. */
|
|
307
|
+
const structuralOf = (leaf: Leaf): ReadonlySet<Key> | "any" | null => {
|
|
308
|
+
const { commands } = leaf;
|
|
309
|
+
if (commands === undefined) return null;
|
|
310
|
+
if (commands === null || commands.despawn === true) return "any";
|
|
311
|
+
const keys = new Set([...commands.spawn ?? [], ...commands.add ?? [], ...commands.remove ?? []].map(firstKeyOf));
|
|
312
|
+
// A reference written may despawn its holder, or remove itself, at the sync point.
|
|
313
|
+
for (const key of keys) if (declarationOf(key)?.reference !== undefined) return "any";
|
|
314
|
+
return keys.size === 0 ? null : keys;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The components a leaf's rows, lookups, indexes and order name, which commands could change its view of; `any` when
|
|
319
|
+
* one of them matches every entity, `cascaded` when it names a component a rule derives or `within` removes.
|
|
320
|
+
*/
|
|
321
|
+
const touchesOf = (leaf: Leaf): { readonly keys: ReadonlySet<Key>; readonly any: boolean; readonly cascaded: boolean } => {
|
|
322
|
+
const { components, lookup, up, order } = leaf.declaration;
|
|
323
|
+
const keys = new Set<Key>();
|
|
324
|
+
let any = false;
|
|
325
|
+
const add = (list: readonly Key[] | undefined): void => { for (const key of list ?? []) keys.add(firstKeyOf(key)); };
|
|
326
|
+
/** Names what a query requires; one requiring nothing matches every entity. */
|
|
327
|
+
const requires = (required: readonly (readonly Key[] | undefined)[], others: readonly (readonly Key[] | undefined)[]): void => {
|
|
328
|
+
if (required.every(list => list === undefined || list.length === 0)) any = true;
|
|
329
|
+
for (const list of [...required, ...others]) add(list);
|
|
330
|
+
};
|
|
331
|
+
const indexed = (index: unknown): void => {
|
|
332
|
+
const declaration = indexDefinitionOf(index)?.declaration;
|
|
333
|
+
if (declaration !== undefined) requires([declaration.key, declaration.components?.has], [declaration.components?.not]);
|
|
334
|
+
};
|
|
335
|
+
if (components !== undefined) {
|
|
336
|
+
requires([components.read, components.write, components.has, components.added, components.changed], [components.optional, components.not]);
|
|
337
|
+
add(Object.getOwnPropertySymbols(components.where ?? {}));
|
|
338
|
+
}
|
|
339
|
+
for (const name in lookup) {
|
|
340
|
+
const used = lookup[name];
|
|
341
|
+
if ("index" in used && used.index !== undefined) {
|
|
342
|
+
indexed(used.index);
|
|
343
|
+
add(used.read);
|
|
344
|
+
}
|
|
345
|
+
else if (!("index" in used)) requires([used.read, used.has], [used.optional, used.not, Array.isArray(used.changed) ? used.changed : undefined]);
|
|
346
|
+
}
|
|
347
|
+
if (order !== undefined) indexed(order);
|
|
348
|
+
if (up !== undefined) add([up.through, ...up.read ?? []]);
|
|
349
|
+
const cascaded = [...keys].some(key => { const declared = declarationOf(key); return declared?.derived === true || declared?.within !== undefined; });
|
|
350
|
+
return { keys, any, cascaded };
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
/** The keys through which `changed` may change what `touches` names; null where it can't. */
|
|
354
|
+
const affected = (changed: ReadonlySet<Key> | "any", touches: ReturnType<typeof touchesOf>): readonly Key[] | null => {
|
|
355
|
+
if (touches.keys.size === 0 && !touches.any) return null;
|
|
356
|
+
if (changed === "any") return [];
|
|
357
|
+
if (touches.any || touches.cascaded) return [...changed];
|
|
358
|
+
const keys = [...changed].filter(key => touches.keys.has(key));
|
|
359
|
+
return keys.length === 0 ? null : keys;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* The fewest sync points that put one between the tiers of each edge, after a tier from `lo` to `hi`: each placed as
|
|
364
|
+
* late as the first edge it must cover allows, so it covers every edge open then. An edge's reason goes to the first
|
|
365
|
+
* sync point within its tiers.
|
|
366
|
+
*/
|
|
367
|
+
const syncsOf = (count: number, edges: readonly { readonly lo: number; readonly hi: number; readonly reason: SyncReason }[]): readonly (readonly SyncReason[] | null)[] => {
|
|
368
|
+
const syncs: (SyncReason[] | null)[] = new Array<SyncReason[] | null>(count).fill(null);
|
|
369
|
+
let last = -1;
|
|
370
|
+
for (const { lo, hi } of [...edges].sort((a, b) => a.hi - b.hi)) {
|
|
371
|
+
if (last >= lo) continue;
|
|
372
|
+
last = hi;
|
|
373
|
+
syncs[hi] = [];
|
|
374
|
+
}
|
|
375
|
+
for (const { lo, hi, reason } of edges) {
|
|
376
|
+
for (let t = lo; t <= hi; t++) {
|
|
377
|
+
const reasons = syncs[t];
|
|
378
|
+
if (reasons === null) continue;
|
|
379
|
+
reasons.push(reason);
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return syncs;
|
|
211
384
|
};
|
|
@@ -27,6 +27,12 @@ export type ComponentAccess = {
|
|
|
27
27
|
* change filters, an archetype it skipped keeps its rows unseen until it passes again.
|
|
28
28
|
*/
|
|
29
29
|
readonly where?: { readonly [K in Shared]?: Component<K> | ((value: Component<K>, context: { readonly resources: ReadResources }) => boolean) };
|
|
30
|
+
/**
|
|
31
|
+
* Takes its row whole, as a body passing it on does, where the build can't follow how it's used: every member counts
|
|
32
|
+
* as written, for order, conflicts and placement, but references and shared components, which change only through
|
|
33
|
+
* commands, as read. Without it, a body whose row the build can't follow fails the build.
|
|
34
|
+
*/
|
|
35
|
+
readonly whole?: true;
|
|
30
36
|
};
|
|
31
37
|
|
|
32
38
|
/**
|
|
@@ -85,6 +91,8 @@ export type ComponentLists = {
|
|
|
85
91
|
readonly where?: { readonly [key: symbol]: unknown };
|
|
86
92
|
/** The resources its `where`'s predicates read, which the build infers: when one changed, they're evaluated again. */
|
|
87
93
|
readonly whereReads?: readonly Key[];
|
|
94
|
+
/** Whether it takes its row whole, as its declaration said. */
|
|
95
|
+
readonly whole?: true;
|
|
88
96
|
};
|
|
89
97
|
|
|
90
98
|
/** A lookup as the world takes it, with what its body reads of its rows in `read`. */
|
|
@@ -105,3 +113,11 @@ export type ResourceLists = {
|
|
|
105
113
|
readonly is?: readonly Key[];
|
|
106
114
|
readonly not?: readonly Key[];
|
|
107
115
|
};
|
|
116
|
+
|
|
117
|
+
/** The components a system's commands name, by kind, and whether it despawns, as the build inferred them. */
|
|
118
|
+
export type CommandLists = {
|
|
119
|
+
readonly spawn?: readonly Key[];
|
|
120
|
+
readonly add?: readonly Key[];
|
|
121
|
+
readonly remove?: readonly Key[];
|
|
122
|
+
readonly despawn?: true;
|
|
123
|
+
};
|
|
@@ -138,11 +138,23 @@ const checkWhere = (leaf: Leaf): void => {
|
|
|
138
138
|
const where = declaration.components?.where;
|
|
139
139
|
if (where === undefined) return;
|
|
140
140
|
if (declaration.up !== undefined || declaration.order !== undefined || declaration.target !== undefined) throw new Error(`System "${name}" uses where with up, order or a target, which run rows across archetypes.`);
|
|
141
|
+
checkWhereValues(`System "${name}"`, "where", "a system", where, true);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Throws for a `where`, the one `named` says of `subject`, which keeps what `keeps` says to archetypes, naming what
|
|
146
|
+
* isn't a shared component or giving one a value not of its type, or a predicate unless `predicates`.
|
|
147
|
+
*/
|
|
148
|
+
export const checkWhereValues = (subject: string, named: string, keeps: string, where: { readonly [key: symbol]: unknown }, predicates: boolean): void => {
|
|
141
149
|
for (const key of Object.getOwnPropertySymbols(where)) {
|
|
142
150
|
const found = declarationOf(key);
|
|
143
|
-
if (kindOf(key) !== "component" || found?.shared !== true) throw new Error(
|
|
151
|
+
if (kindOf(key) !== "component" || found?.shared !== true) throw new Error(`${subject}: ${named} names "${labelOf(key)}", which isn't a shared component: where keeps ${keeps} to archetypes by their shared values.`);
|
|
144
152
|
const value: unknown = Reflect.get(where, key);
|
|
145
|
-
if (typeof value
|
|
153
|
+
if (typeof value === "function") {
|
|
154
|
+
if (!predicates) throw new Error(`${subject}: ${named} gives "${labelOf(key)}" a predicate, and takes only values.`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (found.type !== undefined && !matchesSchema(found.type, value)) throw new Error(`${subject}: ${named} gives "${labelOf(key)}" ${labelOf(value)}, which isn't one of its type's values.`);
|
|
146
158
|
}
|
|
147
159
|
};
|
|
148
160
|
|
|
@@ -176,6 +188,7 @@ const checkTables = (leaf: Leaf): void => {
|
|
|
176
188
|
requireRegistered("System", name, "component", tables.with);
|
|
177
189
|
requireRegistered("System", name, "component", tables.not);
|
|
178
190
|
requireRegistered("System", name, "resource", tables.resources);
|
|
191
|
+
if (tables.where !== undefined) checkWhereValues(`System "${name}"`, "tables' where", "tables", tables.where, false);
|
|
179
192
|
};
|
|
180
193
|
|
|
181
194
|
/**
|
|
@@ -3,6 +3,7 @@ import type { Component } from '../component/component.js';
|
|
|
3
3
|
import type { Entity } from '../entity/entity.js';
|
|
4
4
|
import type { Essential, Shared } from '../component/value.js';
|
|
5
5
|
import type { Table } from '../world/table/table.js';
|
|
6
|
+
import type { SharedOnly } from './shared-only.js';
|
|
6
7
|
import type { ValuesOf } from './values-of.js';
|
|
7
8
|
|
|
8
9
|
/** The value an `add` of one component takes: none for a tag. */
|
|
@@ -63,6 +64,18 @@ export type Commands = {
|
|
|
63
64
|
...constant: RequiresPreset<C> extends true ? [constant?: P & SharedPreset<C> & Named<C, P>] : [constant?: P & Named<C, P>]
|
|
64
65
|
): (...values: SpawnValues<C, P>) => Entity;
|
|
65
66
|
despawn(entity: Entity): void;
|
|
67
|
+
/**
|
|
68
|
+
* Despawns, as a `despawn` of each would, every entity that has each component of `with` and none of `not` once the
|
|
69
|
+
* commands issued before it apply, and every entity spawned at the same sync point that does, whenever spawned.
|
|
70
|
+
* Derived components may be named. `where` keeps it to the archetypes holding these values of shared components,
|
|
71
|
+
* each one required as `with` requires it, as a system's `where` of values does. Never the resource entity. Only on
|
|
72
|
+
* the world's thread: a joined thread's throws.
|
|
73
|
+
*/
|
|
74
|
+
despawnWhere<const W extends object = {}>(query: {
|
|
75
|
+
readonly with: readonly [Component.Key, ...Component.Key[]];
|
|
76
|
+
readonly not?: readonly Component.Key[];
|
|
77
|
+
readonly where?: W & SharedOnly<NoInfer<W>> & { readonly [K in Shared]?: Component<K> };
|
|
78
|
+
}): void;
|
|
66
79
|
/** Adds or replaces one component's value, marking it changed. */
|
|
67
80
|
add<K extends Essential>(entity: Entity, component: K, ...value: ValueArgument<K>): void;
|
|
68
81
|
/** Adds or replaces the values of `components`, marking them changed. */
|
|
@@ -77,9 +90,15 @@ export type Commands = {
|
|
|
77
90
|
* outside them isn't spawned, nor anything it owns, through owner references, as despawning its owner would; a
|
|
78
91
|
* weak reference to one is left out of its row, which is spawned without it; any other value naming one is
|
|
79
92
|
* `Entity.none`. Returns, at once, each table's new entities, alive at the sync point. A column's stride must be its
|
|
80
|
-
* component's in this world, and its length its table's count.
|
|
81
|
-
*
|
|
82
|
-
*
|
|
93
|
+
* component's in this world, and its length its table's count. On the world's thread only.
|
|
94
|
+
* - `existing` maps entities to ones already alive in this world, which stand for them: a row whose entity it maps
|
|
95
|
+
* isn't spawned, its entry in the result is the entity it maps to, and every value naming one it maps names that
|
|
96
|
+
* entity, whether or not it's in the tables. Fails for one that isn't alive.
|
|
97
|
+
* - `resources` sets each resource it names, by key, at the sync point, as commands apply, each value checked
|
|
98
|
+
* against its resource's schema; not one only the world writes, nor one living on another thread.
|
|
83
99
|
*/
|
|
84
|
-
spawnTables(tables: readonly Table[],
|
|
100
|
+
spawnTables(tables: readonly Table[], options?: {
|
|
101
|
+
readonly resources?: { readonly [key: symbol]: unknown };
|
|
102
|
+
readonly existing?: ReadonlyMap<Entity, Entity>;
|
|
103
|
+
}): Table.Spawned;
|
|
85
104
|
};
|
|
@@ -13,6 +13,7 @@ const label = Symbol("commandsTypeTest/label");
|
|
|
13
13
|
const player = Symbol("commandsTypeTest/player");
|
|
14
14
|
const slot = Symbol("commandsTypeTest/slot");
|
|
15
15
|
const time = Symbol("commandsTypeTest/time");
|
|
16
|
+
const chunk = Symbol("commandsTypeTest/chunk");
|
|
16
17
|
declare module '@adobe/ecs' {
|
|
17
18
|
export interface Components {
|
|
18
19
|
[position]: { type: number };
|
|
@@ -20,6 +21,7 @@ declare module '@adobe/ecs' {
|
|
|
20
21
|
[label]: { type: string | null };
|
|
21
22
|
[player]: { type: true };
|
|
22
23
|
[slot]: { type: number; derived: true };
|
|
24
|
+
[chunk]: { type: number; shared: true };
|
|
23
25
|
}
|
|
24
26
|
export interface Resources {
|
|
25
27
|
[time]: { type: number };
|
|
@@ -84,4 +86,33 @@ export const issue = (commands: Commands, entity: Entity): void => {
|
|
|
84
86
|
// @ts-expect-error commands never remove a derived component
|
|
85
87
|
commands.remove(entity, slot);
|
|
86
88
|
commands.despawn(entity);
|
|
89
|
+
|
|
90
|
+
// `despawnWhere` takes a query of at least one component, derived ones included.
|
|
91
|
+
commands.despawnWhere({ with: [player] });
|
|
92
|
+
commands.despawnWhere({ with: [position, slot], not: [player] });
|
|
93
|
+
commands.despawnWhere({ with: particle });
|
|
94
|
+
// @ts-expect-error `with` names at least one component
|
|
95
|
+
commands.despawnWhere({ with: [] });
|
|
96
|
+
// @ts-expect-error a query names components, not resources
|
|
97
|
+
commands.despawnWhere({ with: [time] });
|
|
98
|
+
// @ts-expect-error in `not` too
|
|
99
|
+
commands.despawnWhere({ with: [player], not: [time] });
|
|
100
|
+
// @ts-expect-error it takes a query, not an entity
|
|
101
|
+
commands.despawnWhere(entity);
|
|
102
|
+
// Its `where` gives values of shared components, as a system's `where` of values does.
|
|
103
|
+
commands.despawnWhere({ with: [position], where: { [chunk]: 3 } });
|
|
104
|
+
const unloaded = { [chunk]: 4 } as const;
|
|
105
|
+
commands.despawnWhere({ with: [position], where: unloaded });
|
|
106
|
+
// @ts-expect-error a value of the component's type
|
|
107
|
+
commands.despawnWhere({ with: [position], where: { [chunk]: "3" } });
|
|
108
|
+
// @ts-expect-error where names only shared components
|
|
109
|
+
commands.despawnWhere({ with: [position], where: { [position]: 3 } });
|
|
110
|
+
// @ts-expect-error where takes values, not predicates
|
|
111
|
+
commands.despawnWhere({ with: [position], where: { [chunk]: (value: number) => value > 3 } });
|
|
112
|
+
const placed = { [position]: 3 } as const;
|
|
113
|
+
// @ts-expect-error where names only shared components, in a constant too
|
|
114
|
+
commands.despawnWhere({ with: [position], where: placed });
|
|
115
|
+
const mixed = { [chunk]: 3, [position]: 3 } as const;
|
|
116
|
+
// @ts-expect-error and in a constant naming a shared one beside it
|
|
117
|
+
commands.despawnWhere({ with: [position], where: mixed });
|
|
87
118
|
};
|
|
@@ -14,7 +14,7 @@ import type { System } from './system.js';
|
|
|
14
14
|
export const createResolved = (declaration: Resolved, body: Leaf["body"], input?: unknown): System<unknown> => {
|
|
15
15
|
const name = nameOf(declaration, undefined, "system");
|
|
16
16
|
const schema = inputSchemaOf(name, body, declaration.components === undefined ? 1 : 2, input);
|
|
17
|
-
const system: Leaf & System<unknown> = { kind: "system", name, declaration, input: schema, body };
|
|
17
|
+
const system: Leaf & System<unknown> = { kind: "system", name, declaration, input: schema, body, commands: undefined };
|
|
18
18
|
checkLeaf(system);
|
|
19
19
|
claimSystem(system, undefined);
|
|
20
20
|
return system;
|
|
@@ -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. "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, commandLists, 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: commandLists(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
|
};
|
|
@@ -0,0 +1,79 @@
|
|
|
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("gives a text form to read", () => {
|
|
60
|
+
expect(System.describe(frame).text).toBe([
|
|
61
|
+
"tier 0 spawnCells commands spawn describeTest/position",
|
|
62
|
+
"sync spawnCells's commands of describeTest/position, for move",
|
|
63
|
+
" spawnCells's commands of describeTest/position, for deriveCells",
|
|
64
|
+
"tier 1 move writes describeTest/position",
|
|
65
|
+
" resources reads describeTest/speed",
|
|
66
|
+
" after spawnCells (after)",
|
|
67
|
+
"tier 2 deriveCells reads describeTest/position",
|
|
68
|
+
" requires not describeTest/cell",
|
|
69
|
+
" commands add describeTest/cell",
|
|
70
|
+
" after move (after: describeTest/position)",
|
|
71
|
+
"sync deriveCells's commands of describeTest/cell, for pick",
|
|
72
|
+
"tier 3 pick resources writes describeTest/hover",
|
|
73
|
+
" looks up cells through byCell",
|
|
74
|
+
" after deriveCells (after)",
|
|
75
|
+
"readonly",
|
|
76
|
+
" watch reads describeTest/position",
|
|
77
|
+
].join("\n"));
|
|
78
|
+
});
|
|
79
|
+
});
|