@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
|
@@ -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 and resources left out. */
|
|
57
|
+
readonly resolve: (systems: readonly unknown[] | 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
|
|
@@ -66,8 +79,9 @@ const graphOf = (root: Definition): Graph => {
|
|
|
66
79
|
visit(root);
|
|
67
80
|
const successors = units.flatMap(() => [new Set<number>(), new Set<number>()]);
|
|
68
81
|
// A readonly system is ordered with nothing, so constraints naming one are ignored.
|
|
69
|
-
const resolve = (systems: readonly
|
|
82
|
+
const resolve = (systems: readonly unknown[] | undefined): readonly number[] =>
|
|
70
83
|
(systems ?? []).flatMap(system => {
|
|
84
|
+
if (typeof system !== "object" || system === null) return [];
|
|
71
85
|
const unit = indexOf.get(firstItemOf(system));
|
|
72
86
|
return unit === undefined || isObserver(units[unit]) ? [] : [unit];
|
|
73
87
|
});
|
|
@@ -86,7 +100,7 @@ const graphOf = (root: Definition): Graph => {
|
|
|
86
100
|
successors[end(unit)].add(end(other));
|
|
87
101
|
}
|
|
88
102
|
});
|
|
89
|
-
return { units, successors };
|
|
103
|
+
return { units, successors, members, resolve };
|
|
90
104
|
};
|
|
91
105
|
|
|
92
106
|
/** Kahn's algorithm over nodes. Throws on a cycle. */
|
|
@@ -136,7 +150,7 @@ const depthsOf = ({ units, successors }: Graph, order: readonly number[]): numbe
|
|
|
136
150
|
return depth;
|
|
137
151
|
};
|
|
138
152
|
|
|
139
|
-
type Access = { readonly reads: ReadonlySet<Key>; readonly writes: ReadonlySet<Key>; readonly reduces: ReadonlySet<Key
|
|
153
|
+
type Access = { readonly reads: ReadonlySet<Key>; readonly writes: ReadonlySet<Key>; readonly reduces: ReadonlySet<Key>; readonly leaf: Leaf };
|
|
140
154
|
|
|
141
155
|
/** The resources a leaf reduces into. */
|
|
142
156
|
export const reducedBy = (leaf: Leaf): readonly Key[] => {
|
|
@@ -155,16 +169,155 @@ export const accessOf = (leaf: Leaf): Access => {
|
|
|
155
169
|
for (const key of orderKeysOf(leaf.declaration)) reads.add(firstKeyOf(key));
|
|
156
170
|
if (up !== undefined) for (const key of [up.through, ...up.read ?? []]) reads.add(firstKeyOf(key));
|
|
157
171
|
const writes = new Set<Key>([...components?.write ?? [], ...resources?.write ?? []].map(firstKeyOf));
|
|
158
|
-
return { reads, writes, reduces: new Set(reducedBy(leaf).map(firstKeyOf)) };
|
|
172
|
+
return { reads, writes, reduces: new Set(reducedBy(leaf).map(firstKeyOf)), leaf };
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const names = (list: readonly Key[] | undefined, key: Key): boolean => list !== undefined && list.some(item => firstKeyOf(item) === key);
|
|
176
|
+
|
|
177
|
+
/** Whether a leaf reaches a key only through its rows: neither looked up, read through `up`, indexed nor ordered by. */
|
|
178
|
+
const onlyThroughRows = (leaf: Leaf, key: Key): boolean => {
|
|
179
|
+
const { components, lookup, up } = leaf.declaration;
|
|
180
|
+
if (!names(components?.read, key) && !names(components?.write, key) && !names(components?.added, key) && !names(components?.changed, key)) return false;
|
|
181
|
+
for (const name in lookup) if (names(lookup[name].read, key) || names(lookupKeysOf(lookup[name]), key)) return false;
|
|
182
|
+
if (names(orderKeysOf(leaf.declaration), key)) return false;
|
|
183
|
+
return up === undefined || (firstKeyOf(up.through) !== key && !names(up.read, key));
|
|
159
184
|
};
|
|
160
185
|
|
|
161
|
-
/**
|
|
186
|
+
/** Whether no entity can match both leaves' rows: one requires a component the other's `not` lists. */
|
|
187
|
+
const exclusive = (a: Leaf, b: Leaf): boolean => {
|
|
188
|
+
const requiresExcluded = (one: Leaf, other: Leaf): boolean => {
|
|
189
|
+
const { components } = one.declaration;
|
|
190
|
+
const not = other.declaration.components?.not;
|
|
191
|
+
if (components === undefined || not === undefined || not.length === 0) return false;
|
|
192
|
+
for (const list of [components.read, components.write, components.has, components.added, components.changed]) {
|
|
193
|
+
for (const key of list ?? []) if (!names(components.optional, firstKeyOf(key)) && names(not, firstKeyOf(key))) return true;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
};
|
|
197
|
+
return requiresExcluded(a, b) || requiresExcluded(b, a);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/** Whether a conflict on `key` is waived: both reach it only through rows that never match one entity. */
|
|
201
|
+
const waived = (a: Access, b: Access, key: Key): boolean => onlyThroughRows(a.leaf, key) && onlyThroughRows(b.leaf, key) && exclusive(a.leaf, b.leaf);
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The first key two accesses conflict on, one writing it, or undefined. Contributors to one resource don't conflict
|
|
205
|
+
* with each other: their fold doesn't depend on which runs first. Nor do two whose rows never match one entity on a key
|
|
206
|
+
* both reach only through their rows.
|
|
207
|
+
*/
|
|
162
208
|
const conflictOf = (a: Access, b: Access): Key | undefined => {
|
|
163
|
-
for (const key of a.writes) if (b.writes.has(key) || b.reads.has(key) || b.reduces.has(key)) return key;
|
|
164
|
-
for (const key of b.writes) if (a.reads.has(key) || a.reduces.has(key)) return key;
|
|
209
|
+
for (const key of a.writes) if ((b.writes.has(key) || b.reads.has(key) || b.reduces.has(key)) && !waived(a, b, key)) return key;
|
|
210
|
+
for (const key of b.writes) if ((a.reads.has(key) || a.reduces.has(key)) && !waived(a, b, key)) return key;
|
|
165
211
|
return undefined;
|
|
166
212
|
};
|
|
167
213
|
|
|
214
|
+
/** Every key two accesses conflict on, as `conflictOf` finds the first. */
|
|
215
|
+
const conflictsOf = (a: Access, b: Access): readonly Key[] => [...new Set([
|
|
216
|
+
...[...a.writes].filter(key => b.writes.has(key) || b.reads.has(key) || b.reduces.has(key)),
|
|
217
|
+
...[...b.writes].filter(key => a.reads.has(key) || a.reduces.has(key)),
|
|
218
|
+
])].filter(key => !waived(a, b, key));
|
|
219
|
+
|
|
220
|
+
/** A system's leaves in its tiers, with their units and accesses, and its graph with readers of folds after their contributors. */
|
|
221
|
+
const orderedOf = (system: Definition) => {
|
|
222
|
+
const graph = graphOf(system);
|
|
223
|
+
const leaves = graph.units.flatMap((unit, index) => isLeaf(unit) && !isObserver(unit) ? [{ leaf: unit, unit: index }] : []);
|
|
224
|
+
const accesses = leaves.map(({ leaf }) => accessOf(leaf));
|
|
225
|
+
// Leaves that read a resource run after every leaf that reduces into it, so they read the finished fold.
|
|
226
|
+
const reductions = new Map<Key, number>();
|
|
227
|
+
accesses.forEach((access, contributor) => {
|
|
228
|
+
for (const key of access.reduces) {
|
|
229
|
+
reductions.set(key, (reductions.get(key) ?? 0) + 1);
|
|
230
|
+
accesses.forEach((other, reader) => {
|
|
231
|
+
if (reader !== contributor && other.reads.has(key)) graph.successors[end(leaves[contributor].unit)].add(start(leaves[reader].unit));
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
return { graph, leaves, accesses, reductions, keyed: orderByResources(graph, leaves, accesses) };
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const noneKeyed: ReadonlyMap<string, "before" | "after"> = new Map();
|
|
239
|
+
|
|
240
|
+
const listsResource = (list: readonly unknown[] | undefined): boolean => list !== undefined && list.some(item => typeof item === "symbol");
|
|
241
|
+
|
|
242
|
+
/** The resources a list of `before` or `after` names, each by its first symbol. */
|
|
243
|
+
const resourcesIn = (list: readonly unknown[] | undefined): readonly Key[] =>
|
|
244
|
+
(list ?? []).flatMap(item => typeof item === "symbol" ? [firstKeyOf(item)] : []);
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Orders each leaf naming a resource in `before` or `after` before or after every other leaf using it, adding the edges
|
|
248
|
+
* to `graph`: but one its constraints already order the other way, and one naming the resource in the same list. By
|
|
249
|
+
* unit, as `"<earlier> <later>"`, the list that ordered each pair.
|
|
250
|
+
*/
|
|
251
|
+
const orderByResources = (graph: Graph, leaves: readonly { readonly leaf: Leaf; readonly unit: number }[], accesses: readonly Access[]): ReadonlyMap<string, "before" | "after"> => {
|
|
252
|
+
if (!leaves.some(({ leaf }) => listsResource(leaf.declaration.before) || listsResource(leaf.declaration.after))) return noneKeyed;
|
|
253
|
+
const keyed = new Map<string, "before" | "after">();
|
|
254
|
+
const lists = leaves.map(({ leaf }) => ({ before: resourcesIn(leaf.declaration.before), after: resourcesIn(leaf.declaration.after) }));
|
|
255
|
+
const uses = leaves.map(({ leaf }, index) => new Set([...accesses[index].reads, ...accesses[index].writes, ...accesses[index].reduces,
|
|
256
|
+
...[...leaf.declaration.resources?.is ?? [], ...leaf.declaration.resources?.not ?? []].map(firstKeyOf)]));
|
|
257
|
+
const reach = reachability(topologicalOrder(graph), graph.successors);
|
|
258
|
+
const edges: (readonly [number, number, "before" | "after"])[] = [];
|
|
259
|
+
leaves.forEach(({ unit }, index) => {
|
|
260
|
+
for (const list of ["before", "after"] as const) {
|
|
261
|
+
for (const key of lists[index][list]) {
|
|
262
|
+
leaves.forEach((other, o) => {
|
|
263
|
+
if (o === index || !uses[o].has(key) || lists[o][list].includes(key)) return;
|
|
264
|
+
const [earlier, later] = list === "before" ? [unit, other.unit] : [other.unit, unit];
|
|
265
|
+
if (!reaches(reach, end(later), start(earlier))) edges.push([earlier, later, list]);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
for (const [earlier, later, list] of edges) {
|
|
271
|
+
graph.successors[end(earlier)].add(start(later));
|
|
272
|
+
keyed.set(`${earlier} ${later}`, list);
|
|
273
|
+
}
|
|
274
|
+
return keyed;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* What put a leaf after another: a constraint of either, or of a composite holding it, naming the other or a composite
|
|
279
|
+
* holding it (`after`, `before`), its reading a fold the other contributes to (`reduction`), or being placed during a
|
|
280
|
+
* span (`during`). `conflicts` are the keys both access, one writing, which need the order.
|
|
281
|
+
*/
|
|
282
|
+
export type Placement = { readonly leaf: Leaf; readonly because: "after" | "before" | "during" | "reduction"; readonly conflicts: readonly Key[] };
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* For each leaf in a system's tiers, the leaves it follows directly: those with an ordering path to it through no other
|
|
286
|
+
* leaf of its tiers.
|
|
287
|
+
*/
|
|
288
|
+
export const placementsOf = (system: Definition): ReadonlyMap<Leaf, readonly Placement[]> => {
|
|
289
|
+
const { graph, leaves, accesses, keyed } = orderedOf(system);
|
|
290
|
+
const reach = reachability(topologicalOrder(graph), graph.successors);
|
|
291
|
+
const ancestors = graph.units.map(() => [] as number[]);
|
|
292
|
+
graph.members.forEach((members, unit) => {
|
|
293
|
+
const visit = (member: number): void => {
|
|
294
|
+
ancestors[member].push(unit);
|
|
295
|
+
for (const inner of graph.members[member]) visit(inner);
|
|
296
|
+
};
|
|
297
|
+
for (const member of members) visit(member);
|
|
298
|
+
});
|
|
299
|
+
/** Whether `systems` names `unit` or a composite holding it. */
|
|
300
|
+
const names = (systems: readonly unknown[] | undefined, unit: number): boolean => {
|
|
301
|
+
const named = graph.resolve(systems);
|
|
302
|
+
return named.includes(unit) || ancestors[unit].some(holder => named.includes(holder));
|
|
303
|
+
};
|
|
304
|
+
const constrains = (unit: number, list: "after" | "before", other: number): boolean =>
|
|
305
|
+
[unit, ...ancestors[unit]].some(holder => holder !== 0 && names(graph.units[holder].declaration[list], other));
|
|
306
|
+
const placements = new Map<Leaf, readonly Placement[]>();
|
|
307
|
+
leaves.forEach(({ leaf, unit }, b) => {
|
|
308
|
+
const before = leaves.flatMap((other, a) => a !== b && reaches(reach, end(other.unit), start(unit)) ? [a] : []);
|
|
309
|
+
const direct = before.filter(a => !before.some(other => other !== a && reaches(reach, end(leaves[a].unit), start(leaves[other].unit))));
|
|
310
|
+
placements.set(leaf, direct.map(a => {
|
|
311
|
+
const other = leaves[a];
|
|
312
|
+
const because = constrains(unit, "after", other.unit) ? "after"
|
|
313
|
+
: constrains(other.unit, "before", unit) ? "before"
|
|
314
|
+
: keyed.get(`${other.unit} ${unit}`) ?? ([...accesses[a].reduces].some(key => accesses[b].reads.has(key)) ? "reduction" : "during");
|
|
315
|
+
return { leaf: other.leaf, because, conflicts: conflictsOf(accesses[a], accesses[b]) };
|
|
316
|
+
}));
|
|
317
|
+
});
|
|
318
|
+
return placements;
|
|
319
|
+
};
|
|
320
|
+
|
|
168
321
|
const schedules = new WeakMap<Definition, Schedule>();
|
|
169
322
|
|
|
170
323
|
/** 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 +332,126 @@ export const scheduleOf = (system: Definition): Schedule => {
|
|
|
179
332
|
* or on inputs that don't merge.
|
|
180
333
|
*/
|
|
181
334
|
const createSchedule = (system: Definition): Schedule => {
|
|
182
|
-
const graph =
|
|
335
|
+
const { graph, leaves, accesses, reductions } = orderedOf(system);
|
|
183
336
|
const observer = graph.units.filter(isObserver);
|
|
184
|
-
const leaves = graph.units.flatMap((unit, index) => isLeaf(unit) && !isObserver(unit) ? [{ leaf: unit, unit: index }] : []);
|
|
185
337
|
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
338
|
const order = topologicalOrder(graph);
|
|
198
339
|
const reach = reachability(order, graph.successors);
|
|
199
340
|
for (let a = 0; a < leaves.length; a++) {
|
|
200
341
|
for (let b = a + 1; b < leaves.length; b++) {
|
|
201
342
|
const key = conflictOf(accesses[a], accesses[b]);
|
|
202
343
|
if (key !== undefined && !ordered(reach, leaves[a].unit, leaves[b].unit)) {
|
|
203
|
-
|
|
344
|
+
const whole = [leaves[a].leaf, leaves[b].leaf].filter(leaf => leaf.declaration.components?.whole === true)
|
|
345
|
+
.map(leaf => ` "${leaf.name}" takes its row whole (components.whole), so it writes every member.`).join("");
|
|
346
|
+
const rows = onlyThroughRows(leaves[a].leaf, key) && onlyThroughRows(leaves[b].leaf, key) ? ", or, if they never match one entity, add a component of one to the other's not" : "";
|
|
347
|
+
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${rows}.${whole}`);
|
|
204
348
|
}
|
|
205
349
|
}
|
|
206
350
|
}
|
|
207
351
|
const depth = depthsOf(graph, order);
|
|
208
|
-
const
|
|
209
|
-
for (const { leaf, unit } of leaves) (
|
|
210
|
-
|
|
352
|
+
const byDepth: Leaf[][] = [];
|
|
353
|
+
for (const { leaf, unit } of leaves) (byDepth[depth[start(unit)]] ??= []).push(leaf);
|
|
354
|
+
const tiers = byDepth.filter(tier => tier !== undefined);
|
|
355
|
+
// A sync point comes between tiers, so a schedule of one tier has none.
|
|
356
|
+
if (tiers.length < 2) return { tiers, reductions, observer, syncs: tiers.map(() => null) };
|
|
357
|
+
const tierOf = new Map<Leaf, number>();
|
|
358
|
+
tiers.forEach((tier, t) => { for (const leaf of tier) tierOf.set(leaf, t); });
|
|
359
|
+
const edges: { readonly lo: number; readonly hi: number; readonly reason: SyncReason }[] = [];
|
|
360
|
+
leaves.forEach((from, a) => {
|
|
361
|
+
const changed = structuralOf(from.leaf);
|
|
362
|
+
if (changed === null) return;
|
|
363
|
+
leaves.forEach((to, b) => {
|
|
364
|
+
if (a === b || !reaches(reach, end(from.unit), start(to.unit))) return;
|
|
365
|
+
const keys = affected(changed, touchesOf(to.leaf));
|
|
366
|
+
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 } });
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
return { tiers, reductions, observer, syncs: syncsOf(tiers.length, edges) };
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Why a sync point follows a tier: a system in it or before it whose commands may change, structurally, what a system
|
|
374
|
+
* after the sync point that follows it reads, matches, looks up or indexes. `keys` are the components its commands name
|
|
375
|
+
* that the later one uses, none where it despawns, or where its commands or a cascade could change anything.
|
|
376
|
+
*/
|
|
377
|
+
export type SyncReason = { readonly from: Leaf; readonly to: Leaf; readonly keys: readonly Key[] };
|
|
378
|
+
|
|
379
|
+
/** The components a leaf's commands may add or remove, by spawn, add or remove; `"any"` once it despawns or isn't followed. */
|
|
380
|
+
const structuralOf = (leaf: Leaf): ReadonlySet<Key> | "any" | null => {
|
|
381
|
+
const commands = leaf.commands();
|
|
382
|
+
if (commands === undefined) return null;
|
|
383
|
+
if (commands === null || commands.despawn === true) return "any";
|
|
384
|
+
const keys = new Set([...commands.spawn ?? [], ...commands.add ?? [], ...commands.remove ?? []].map(firstKeyOf));
|
|
385
|
+
// A reference written may despawn its holder, or remove itself, at the sync point.
|
|
386
|
+
for (const key of keys) if (declarationOf(key)?.reference !== undefined) return "any";
|
|
387
|
+
return keys.size === 0 ? null : keys;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The components a leaf's rows, lookups, indexes and order name, which commands could change its view of; `any` when
|
|
392
|
+
* one of them matches every entity, `cascaded` when it names a component a rule derives or `within` removes.
|
|
393
|
+
*/
|
|
394
|
+
const touchesOf = (leaf: Leaf): { readonly keys: ReadonlySet<Key>; readonly any: boolean; readonly cascaded: boolean } => {
|
|
395
|
+
const { components, lookup, up, order } = leaf.declaration;
|
|
396
|
+
const keys = new Set<Key>();
|
|
397
|
+
let any = false;
|
|
398
|
+
const add = (list: readonly Key[] | undefined): void => { for (const key of list ?? []) keys.add(firstKeyOf(key)); };
|
|
399
|
+
/** Names what a query requires; one requiring nothing matches every entity. */
|
|
400
|
+
const requires = (required: readonly (readonly Key[] | undefined)[], others: readonly (readonly Key[] | undefined)[]): void => {
|
|
401
|
+
if (required.every(list => list === undefined || list.length === 0)) any = true;
|
|
402
|
+
for (const list of [...required, ...others]) add(list);
|
|
403
|
+
};
|
|
404
|
+
const indexed = (index: unknown): void => {
|
|
405
|
+
const declaration = indexDefinitionOf(index)?.declaration;
|
|
406
|
+
if (declaration !== undefined) requires([declaration.key, declaration.components?.has], [declaration.components?.not]);
|
|
407
|
+
};
|
|
408
|
+
if (components !== undefined) {
|
|
409
|
+
requires([components.read, components.write, components.has, components.added, components.changed], [components.optional, components.not]);
|
|
410
|
+
add(Object.getOwnPropertySymbols(components.where ?? {}));
|
|
411
|
+
}
|
|
412
|
+
for (const name in lookup) {
|
|
413
|
+
const used = lookup[name];
|
|
414
|
+
if ("index" in used && used.index !== undefined) {
|
|
415
|
+
indexed(used.index);
|
|
416
|
+
add(used.read);
|
|
417
|
+
}
|
|
418
|
+
else if (!("index" in used)) requires([used.read, used.has], [used.optional, used.not, Array.isArray(used.changed) ? used.changed : undefined]);
|
|
419
|
+
}
|
|
420
|
+
if (order !== undefined) indexed(order);
|
|
421
|
+
if (up !== undefined) add([up.through, ...up.read ?? []]);
|
|
422
|
+
const cascaded = [...keys].some(key => { const declared = declarationOf(key); return declared?.derived === true || declared?.within !== undefined; });
|
|
423
|
+
return { keys, any, cascaded };
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
/** The keys through which `changed` may change what `touches` names; null where it can't. */
|
|
427
|
+
const affected = (changed: ReadonlySet<Key> | "any", touches: ReturnType<typeof touchesOf>): readonly Key[] | null => {
|
|
428
|
+
if (touches.keys.size === 0 && !touches.any) return null;
|
|
429
|
+
if (changed === "any") return [];
|
|
430
|
+
if (touches.any || touches.cascaded) return [...changed];
|
|
431
|
+
const keys = [...changed].filter(key => touches.keys.has(key));
|
|
432
|
+
return keys.length === 0 ? null : keys;
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* The fewest sync points that put one between the tiers of each edge, after a tier from `lo` to `hi`: each placed as
|
|
437
|
+
* late as the first edge it must cover allows, so it covers every edge open then. An edge's reason goes to the first
|
|
438
|
+
* sync point within its tiers.
|
|
439
|
+
*/
|
|
440
|
+
const syncsOf = (count: number, edges: readonly { readonly lo: number; readonly hi: number; readonly reason: SyncReason }[]): readonly (readonly SyncReason[] | null)[] => {
|
|
441
|
+
const syncs: (SyncReason[] | null)[] = new Array<SyncReason[] | null>(count).fill(null);
|
|
442
|
+
let last = -1;
|
|
443
|
+
for (const { lo, hi } of [...edges].sort((a, b) => a.hi - b.hi)) {
|
|
444
|
+
if (last >= lo) continue;
|
|
445
|
+
last = hi;
|
|
446
|
+
syncs[hi] = [];
|
|
447
|
+
}
|
|
448
|
+
for (const { lo, hi, reason } of edges) {
|
|
449
|
+
for (let t = lo; t <= hi; t++) {
|
|
450
|
+
const reasons = syncs[t];
|
|
451
|
+
if (reasons === null) continue;
|
|
452
|
+
reasons.push(reason);
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return syncs;
|
|
211
457
|
};
|
|
@@ -7,7 +7,7 @@ import { System } from '../system/system.js';
|
|
|
7
7
|
import type { Create } from '../world/create.js';
|
|
8
8
|
import type { World } from '../world/world.js';
|
|
9
9
|
import { Rule } from './rule.js';
|
|
10
|
-
import type { Writable } from '../component/value.js';
|
|
10
|
+
import type { Essential, Writable } from '../component/value.js';
|
|
11
11
|
import type { ChangeSet } from '../finalizer/body.js';
|
|
12
12
|
|
|
13
13
|
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
|
|
@@ -18,6 +18,9 @@ const hidden = Symbol("ruleTypeTest/hidden");
|
|
|
18
18
|
const slot = Symbol("ruleTypeTest/slot");
|
|
19
19
|
const parent = Symbol("ruleTypeTest/parent");
|
|
20
20
|
const time = Symbol("ruleTypeTest/time");
|
|
21
|
+
const inDocument = Symbol("ruleTypeTest/inDocument");
|
|
22
|
+
const kept = Symbol("ruleTypeTest/kept");
|
|
23
|
+
const saved = Symbol("ruleTypeTest/saved");
|
|
21
24
|
|
|
22
25
|
declare module '@adobe/ecs' {
|
|
23
26
|
export interface Components {
|
|
@@ -25,9 +28,14 @@ declare module '@adobe/ecs' {
|
|
|
25
28
|
[hidden]: { type: true };
|
|
26
29
|
[slot]: { type: number; derived: true };
|
|
27
30
|
[parent]: { type: Entity; reference: "owner" };
|
|
31
|
+
[kept]: { type: number; [inDocument]: true };
|
|
28
32
|
}
|
|
29
33
|
export interface Resources {
|
|
30
34
|
[time]: { type: number };
|
|
35
|
+
[saved]: { type: number; [inDocument]: true };
|
|
36
|
+
}
|
|
37
|
+
export interface Traits<N> {
|
|
38
|
+
readonly [inDocument]: N extends unknown ? true : never;
|
|
31
39
|
}
|
|
32
40
|
}
|
|
33
41
|
|
|
@@ -81,6 +89,20 @@ Finalizer.changes({ watch: [source, parent] }, (changes, { commands }) => {
|
|
|
81
89
|
// @ts-expect-error changes finalizers don't watch derived components
|
|
82
90
|
Finalizer.changes({ watch: [slot] }, () => {});
|
|
83
91
|
|
|
92
|
+
// A trait of your own watches every component and resource declaring it.
|
|
93
|
+
Finalizer.changes({ watch: [inDocument, source] }, changes => {
|
|
94
|
+
check<Equal<typeof changes, ChangeSet<typeof kept | typeof source, typeof saved>>>();
|
|
95
|
+
});
|
|
96
|
+
// Every component, and the resources chosen, by key or trait.
|
|
97
|
+
Finalizer.changes({ watch: { components: true, resources: [time, inDocument] } }, changes => {
|
|
98
|
+
check<Equal<typeof changes, ChangeSet<Essential, typeof time | typeof saved>>>();
|
|
99
|
+
});
|
|
100
|
+
Finalizer.changes({ watch: { components: [inDocument], resources: [] } }, changes => {
|
|
101
|
+
check<Equal<typeof changes, ChangeSet<typeof kept>>>();
|
|
102
|
+
});
|
|
103
|
+
// @ts-expect-error components lists components and traits, resources resources and traits
|
|
104
|
+
Finalizer.changes({ watch: { components: [time], resources: [source] } }, () => {});
|
|
105
|
+
|
|
84
106
|
// References change only through commands, so systems can't write them.
|
|
85
107
|
System.create({ components: { with: [parent, source] } }, row => { row[source] = row[parent]; });
|
|
86
108
|
System.create({ components: { with: [parent] } }, row => {
|
|
@@ -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
|
/**
|
|
@@ -207,6 +220,8 @@ export const checkLeaf = (leaf: Leaf): void => {
|
|
|
207
220
|
threadOf(declaration, "System", name);
|
|
208
221
|
requireRegistered("System", name, "component", components?.has);
|
|
209
222
|
requireRegistered("System", name, "component", components?.not);
|
|
223
|
+
if (declaration.before !== undefined) requireRegistered("System", name, "resource", declaration.before.filter(item => typeof item === "symbol"));
|
|
224
|
+
if (declaration.after !== undefined) requireRegistered("System", name, "resource", declaration.after.filter(item => typeof item === "symbol"));
|
|
210
225
|
for (const lookupName in lookup) {
|
|
211
226
|
const found = lookup[lookupName];
|
|
212
227
|
requireRegistered("System", name, "component", "has" in found ? found.has : undefined);
|
|
@@ -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 alive before its sync point that has each component of
|
|
69
|
+
* `with` and none of `not` once the commands issued before it apply, as a row system over the same query despawning
|
|
70
|
+
* each of its rows would; never one the sync point spawns, whenever spawned. 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,20 @@ 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
|
+
* - `update` writes each row `existing` maps onto the entity standing for it, as `commands.add` of its table's
|
|
98
|
+
* components would: adding those the entity lacks, replacing and marking changed those it has, and leaving its
|
|
99
|
+
* other components as they are. Its entity values are rewritten as a spawned row's are, but a reference naming an
|
|
100
|
+
* entity that isn't there, weak or owner, is left out, so the entity's own stands.
|
|
101
|
+
* - `resources` sets each resource it names, by key, at the sync point, as commands apply, each value checked
|
|
102
|
+
* against its resource's schema; not one only the world writes, nor one living on another thread.
|
|
83
103
|
*/
|
|
84
|
-
spawnTables(tables: readonly Table[],
|
|
104
|
+
spawnTables(tables: readonly Table[], options?: {
|
|
105
|
+
readonly resources?: { readonly [key: symbol]: unknown };
|
|
106
|
+
readonly existing?: ReadonlyMap<Entity, Entity>;
|
|
107
|
+
readonly update?: boolean;
|
|
108
|
+
}): Table.Spawned;
|
|
85
109
|
};
|
|
@@ -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
|
};
|
|
@@ -3,6 +3,7 @@ import { nameOf } from '../registry/items.js';
|
|
|
3
3
|
import { claimSystem } from '../registry/program.js';
|
|
4
4
|
import { checkLeaf, inputSchemaOf } from './check.js';
|
|
5
5
|
import type { Resolved } from './declaration.js';
|
|
6
|
+
import { noCommands } from './resolve.js';
|
|
6
7
|
import type { Leaf } from './leaf.js';
|
|
7
8
|
import type { System } from './system.js';
|
|
8
9
|
|
|
@@ -14,7 +15,7 @@ import type { System } from './system.js';
|
|
|
14
15
|
export const createResolved = (declaration: Resolved, body: Leaf["body"], input?: unknown): System<unknown> => {
|
|
15
16
|
const name = nameOf(declaration, undefined, "system");
|
|
16
17
|
const schema = inputSchemaOf(name, body, declaration.components === undefined ? 1 : 2, input);
|
|
17
|
-
const system: Leaf & System<unknown> = { kind: "system", name, declaration, input: schema, body };
|
|
18
|
+
const system: Leaf & System<unknown> = { kind: "system", name, declaration, input: schema, body, commands: noCommands };
|
|
18
19
|
checkLeaf(system);
|
|
19
20
|
claimSystem(system, undefined);
|
|
20
21
|
return system;
|