@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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import type { Entity } from '../../interface/entity/entity.js';
|
|
3
|
+
import type { Essential, ReadonlyType } from '../../interface/component/value.js';
|
|
4
|
+
import type { ChangeSet } from '../../interface/finalizer/body.js';
|
|
5
|
+
import type { Resource } from '../../interface/resource/resource.js';
|
|
6
|
+
import { isLeaf, newestKeyOf, scheduleOf, type Definition, type Leaf } from '../../interface/registry/index.js';
|
|
7
|
+
import type { Archetype } from './archetype.js';
|
|
8
|
+
import { readCell } from './cells.js';
|
|
9
|
+
import { asEntity, indexOf, locate } from './entities.js';
|
|
10
|
+
import { sameValue } from './resource-changes.js';
|
|
11
|
+
import { findResource, initialOf } from './resources.js';
|
|
12
|
+
import { metIdOf, type Key, type State } from './state.js';
|
|
13
|
+
import type { Queued } from './synchronize.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* What a run asked for its changes holds while it runs: by entity its sync points planned, the watched components it
|
|
17
|
+
* had before the run, none for one born in it, with their values then, and those its adds gave a value; and the
|
|
18
|
+
* resources its systems write or reduce into, with their values before it.
|
|
19
|
+
*/
|
|
20
|
+
export type Recording = {
|
|
21
|
+
readonly before: Map<number, { readonly ids: readonly number[] | null; readonly values: ReadonlyMap<number, unknown> }>;
|
|
22
|
+
readonly given: Map<number, Set<number>>;
|
|
23
|
+
readonly resources: ReadonlyMap<Key, unknown>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Every component but derived ones and the objects a joined thread fills, as a changes finalizer watches by default. */
|
|
27
|
+
const watched = (state: State, id: number): boolean => !state.derived[id] && !state.resources.has(id) && state.remote[id] === undefined;
|
|
28
|
+
|
|
29
|
+
const noValues: ReadonlyMap<number, unknown> = new Map();
|
|
30
|
+
|
|
31
|
+
/** The values of components `ids` of an archetype's row. */
|
|
32
|
+
const valuesAt = (archetype: Archetype, ids: readonly number[], row: number): ReadonlyMap<number, unknown> => {
|
|
33
|
+
const values = new Map<number, unknown>();
|
|
34
|
+
for (const id of ids) {
|
|
35
|
+
const storage = archetype.storageById[id];
|
|
36
|
+
if (storage !== undefined) values.set(id, readCell(storage, row));
|
|
37
|
+
}
|
|
38
|
+
return values;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Starts recording a run whose systems write or reduce into `resources`, from their values now. */
|
|
42
|
+
export const startRecording = (state: State, resources: Iterable<Key>): Recording => {
|
|
43
|
+
const values = new Map<Key, unknown>();
|
|
44
|
+
for (const key of resources) {
|
|
45
|
+
const cell = findResource(state, key);
|
|
46
|
+
values.set(key, cell === undefined ? initialOf(state, key) : readCell(cell.storage, cell.row));
|
|
47
|
+
}
|
|
48
|
+
return { before: new Map(), given: new Map(), resources: values };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* At a sync point of the run, before it applies: each entity its plan names that no earlier one did, with what it has
|
|
53
|
+
* now, and the components its adds give a value. `born` holds the rows committed from batches here, by archetype.
|
|
54
|
+
*/
|
|
55
|
+
export const recordPlan = (state: State, recording: Recording, queued: readonly Queued[], born: ReadonlyMap<number, number>): void => {
|
|
56
|
+
const { plan, archetypes, entities } = state;
|
|
57
|
+
const note = (entity: number, from: Archetype | null): void => {
|
|
58
|
+
if (recording.before.has(entity) || from?.resource === true) return;
|
|
59
|
+
if (from === null) {
|
|
60
|
+
recording.before.set(entity, { ids: null, values: noValues });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const row = entities.row[indexOf(entity)];
|
|
64
|
+
const ids = from.components.filter(id => watched(state, id));
|
|
65
|
+
recording.before.set(entity, { ids, values: valuesAt(from, ids, row) });
|
|
66
|
+
};
|
|
67
|
+
const give = (entity: number, buffer: number, start: number, end: number): void => {
|
|
68
|
+
const { ids } = queued[buffer].buffer;
|
|
69
|
+
for (let k = start; k < end; k++) {
|
|
70
|
+
if (!watched(state, ids[k])) continue;
|
|
71
|
+
let given = recording.given.get(entity);
|
|
72
|
+
if (given === undefined) recording.given.set(entity, given = new Set());
|
|
73
|
+
given.add(ids[k]);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
for (let entry = 0; entry < plan.count; entry++) {
|
|
77
|
+
const entity = plan.entity[entry];
|
|
78
|
+
const fromId = plan.from[entry];
|
|
79
|
+
const first = fromId < 0 ? undefined : born.get(fromId);
|
|
80
|
+
note(entity, fromId < 0 || (first !== undefined && entities.row[indexOf(entity)] >= first) ? null : archetypes[fromId]);
|
|
81
|
+
if (plan.start[entry] < 0) continue;
|
|
82
|
+
give(entity, plan.buffer[entry], plan.start[entry], plan.end[entry]);
|
|
83
|
+
const more = plan.more.size === 0 ? undefined : plan.more.get(entry);
|
|
84
|
+
if (more !== undefined) for (let r = 0; r < more.length; r += 3) give(entity, more[r], more[r + 1], more[r + 2]);
|
|
85
|
+
}
|
|
86
|
+
// Rows committed from batches no command touched have no entry.
|
|
87
|
+
for (const [id, first] of born) {
|
|
88
|
+
const archetype = archetypes[id];
|
|
89
|
+
for (let row = first; row < archetype.count; row++) note(archetype.entities[row], null);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** A watched component's newest key: watched components are registered and not derived, so they're essential. */
|
|
94
|
+
const keyOf = (state: State, id: number): Essential => newestKeyOf(state.keys[id]) as Essential;
|
|
95
|
+
|
|
96
|
+
/** A group of a change set, as it's built. */
|
|
97
|
+
type Grouped = { from: Essential[] | null; to: Essential[] | null; added: Essential[]; removed: Essential[]; replaced: Essential[]; entities: Entity[] };
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The run's change set once its last sync point applied: each entity grouped by the watched components it had before
|
|
101
|
+
* the run and has after, those it gained and lost, and those adds gave a value it had before and after, with their
|
|
102
|
+
* values then and now; and each resource its systems write or reduce into whose value changed.
|
|
103
|
+
*/
|
|
104
|
+
export const changesOf = (state: State, recording: Recording): ChangeSet<Essential, Resource.Key> => {
|
|
105
|
+
const { archetypes, entities } = state;
|
|
106
|
+
const groups = new Map<string, Grouped>();
|
|
107
|
+
const after = new Map<number, ReadonlyMap<number, unknown>>();
|
|
108
|
+
for (const [entity, { ids: before }] of recording.before) {
|
|
109
|
+
const index = locate(entities, entity);
|
|
110
|
+
const to = index < 0 ? null : archetypes[entities.archetype[index]];
|
|
111
|
+
const ids = to === null ? null : to.components.filter(id => watched(state, id));
|
|
112
|
+
if (before === null && ids === null) continue;
|
|
113
|
+
const added = (ids ?? []).filter(id => !(before ?? []).includes(id));
|
|
114
|
+
const removed = (before ?? []).filter(id => !(ids ?? []).includes(id));
|
|
115
|
+
const replaced = [...recording.given.get(entity) ?? []].filter(id => before?.includes(id) === true && ids?.includes(id) === true).sort((a, b) => a - b);
|
|
116
|
+
if (added.length === 0 && removed.length === 0 && replaced.length === 0) continue;
|
|
117
|
+
if (to !== null && ids !== null) {
|
|
118
|
+
after.set(entity, valuesAt(to, ids, entities.row[index]));
|
|
119
|
+
}
|
|
120
|
+
const name = `${before?.join(",") ?? "-"}|${ids?.join(",") ?? "-"}|${replaced.join(",")}`;
|
|
121
|
+
let group = groups.get(name);
|
|
122
|
+
const keys = (list: readonly number[] | null): Essential[] | null => list === null ? null : list.map(id => keyOf(state, id));
|
|
123
|
+
if (group === undefined) {
|
|
124
|
+
groups.set(name, group = { from: keys(before), to: keys(ids), added: added.map(id => keyOf(state, id)), removed: removed.map(id => keyOf(state, id)), replaced: replaced.map(id => keyOf(state, id)), entities: [] });
|
|
125
|
+
}
|
|
126
|
+
group.entities.push(asEntity(entity));
|
|
127
|
+
}
|
|
128
|
+
const resources: { key: Resource.Key; previous: unknown; next: unknown }[] = [];
|
|
129
|
+
for (const [key, previous] of recording.resources) {
|
|
130
|
+
const cell = findResource(state, key);
|
|
131
|
+
const next = cell === undefined ? previous : readCell(cell.storage, cell.row);
|
|
132
|
+
// A key the run's systems name is a resource's.
|
|
133
|
+
if (!sameValue(previous, next)) resources.push({ key: newestKeyOf(key) as Resource.Key, previous, next });
|
|
134
|
+
}
|
|
135
|
+
const valueOf = (values: ReadonlyMap<number, unknown> | undefined, key: Essential): unknown => {
|
|
136
|
+
const id = metIdOf(state, key);
|
|
137
|
+
return id !== undefined && values?.has(id) === true ? values.get(id) : null;
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
groups: [...groups.values()],
|
|
141
|
+
// Values read from storage match each key's registered schema, so they have its type.
|
|
142
|
+
resources: resources as ChangeSet<Essential, Resource.Key>["resources"],
|
|
143
|
+
previous: <K extends Essential>(entity: Entity, key: K) => valueOf(recording.before.get(entity)?.values, key) as ReadonlyType<K> | null,
|
|
144
|
+
next: <K extends Essential>(entity: Entity, key: K) => valueOf(after.get(entity), key) as ReadonlyType<K> | null,
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** A system's leaves that run in tiers, or the system itself. */
|
|
149
|
+
const leavesOf = (definition: Definition): readonly Leaf[] => isLeaf(definition) ? [definition] : scheduleOf(definition).tiers.flat();
|
|
150
|
+
|
|
151
|
+
/** The resources a system's leaves write or reduce into. */
|
|
152
|
+
export const resourcesWrittenBy = (definition: Definition): ReadonlySet<Key> =>
|
|
153
|
+
new Set(leavesOf(definition).flatMap(leaf => [...leaf.declaration.resources?.write ?? [], ...leaf.declaration.resources?.reduce ?? []]));
|
|
154
|
+
|
|
155
|
+
/** Whether a system or composite takes input, so a run's options follow it. */
|
|
156
|
+
export const takesInput = (definition: Definition): boolean =>
|
|
157
|
+
isLeaf(definition) ? definition.input !== undefined : [...scheduleOf(definition).tiers.flat(), ...scheduleOf(definition).observer].some(leaf => leaf.input !== undefined);
|
|
158
|
+
|
|
159
|
+
/** Whether a run's options ask for its changes. */
|
|
160
|
+
export const asksForChanges = (options: unknown): boolean => typeof options === "object" && options !== null && Reflect.get(options, "changes") === true;
|
|
@@ -354,6 +354,23 @@ describe("what a run gathers for tests", () => {
|
|
|
354
354
|
expect(await runRan(world, System.create([nothing, raise]))).toEqual({ systems: 2, workers: 0, joined: 0, main: true, gpu: false });
|
|
355
355
|
expect(await runRan(world, readLevel)).toEqual({ systems: 0, workers: 0, joined: 0, main: false, gpu: false });
|
|
356
356
|
});
|
|
357
|
+
|
|
358
|
+
it("counts each system of a chain once, run whole or entered after a sync point that applies something", async () => {
|
|
359
|
+
await using world = createWorld({ [workers]: 0 });
|
|
360
|
+
/** Systems each after the one before, the first after `first`. */
|
|
361
|
+
const chainOf = (length: number, first: readonly System<unknown>[]): System<unknown>[] => {
|
|
362
|
+
const links: System<unknown>[] = [];
|
|
363
|
+
for (let k = 0; k < length; k++) links.push(System.create({ after: links.length === 0 ? first : links.slice(-1) }, ({ resources }) => {
|
|
364
|
+
resources[frames] = k;
|
|
365
|
+
}));
|
|
366
|
+
return links;
|
|
367
|
+
};
|
|
368
|
+
expect(await runRan(world, System.create(chainOf(64, [])))).toEqual({ systems: 64, workers: 0, joined: 0, main: true, gpu: false });
|
|
369
|
+
const reads = System.create({ after: [spawn], components: { with: [amount] } }, row => {
|
|
370
|
+
counted += row[amount];
|
|
371
|
+
});
|
|
372
|
+
expect(await runRan(world, System.create([spawn, reads, ...chainOf(8, [reads])]))).toEqual({ systems: 10, workers: 0, joined: 0, main: true, gpu: false });
|
|
373
|
+
});
|
|
357
374
|
});
|
|
358
375
|
|
|
359
376
|
describe("a value a body returned", () => {
|
|
@@ -63,7 +63,7 @@ const noIndexParameters: readonly number[] = [];
|
|
|
63
63
|
const noAddresses: readonly number[] = [];
|
|
64
64
|
|
|
65
65
|
/** An archetype's change filters for a runner, and the WASM addresses of its columns and filters (`filterCache`). */
|
|
66
|
-
type Filtered = { readonly version: number; readonly filters: readonly Filter[]; addresses: readonly number[]; addressed: WasmKernel | null };
|
|
66
|
+
type Filtered = { readonly archetype: Archetype; readonly version: number; readonly filters: readonly Filter[]; addresses: readonly number[]; addressed: WasmKernel | null };
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* Runs one leaf system. Each run takes a new tick, which becomes the system's `lastRun`. `start` does main-thread work
|
|
@@ -470,7 +470,7 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
470
470
|
|
|
471
471
|
/** Drops match `m`, its last taking its place; with `where`, one evaluated stays among those evaluated. */
|
|
472
472
|
const removeMatch = (m: number): void => {
|
|
473
|
-
filterCache
|
|
473
|
+
filterCache[matches[m].id] = undefined;
|
|
474
474
|
matchOf[matches[m].id] = -1;
|
|
475
475
|
let hole = m;
|
|
476
476
|
if (where !== null && m < where.evaluated) {
|
|
@@ -487,7 +487,7 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
487
487
|
/** Matches `m` anew, reused for other values: its point is its last run's, and `where` evaluates it again. */
|
|
488
488
|
const renewMatch = (m: number): void => {
|
|
489
489
|
const archetype = matches[m];
|
|
490
|
-
filterCache.
|
|
490
|
+
filterCache[archetype.id] = undefined;
|
|
491
491
|
points[m] = lastRun;
|
|
492
492
|
generations[m] = archetype.generation;
|
|
493
493
|
if (where !== null && m < where.evaluated) {
|
|
@@ -563,12 +563,12 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
563
563
|
* and the WASM addresses of its columns and filters, for the WASM kernel `addressed` they were taken for, since a
|
|
564
564
|
* kernel loaded again may stamp a tick column the last one muted.
|
|
565
565
|
*/
|
|
566
|
-
const filterCache
|
|
566
|
+
const filterCache: (Filtered | undefined)[] = [];
|
|
567
567
|
const noFilters: readonly Filter[] = [];
|
|
568
568
|
|
|
569
569
|
const filteredOf = (archetype: Archetype) => {
|
|
570
|
-
const cached = filterCache.
|
|
571
|
-
if (cached !== undefined && cached.version === archetype.version) return cached;
|
|
570
|
+
const cached = filterCache[archetype.id];
|
|
571
|
+
if (cached !== undefined && cached.archetype === archetype && cached.version === archetype.version) return cached;
|
|
572
572
|
const filters = addedIds.length + changedIds.length === 0 ? noFilters : [
|
|
573
573
|
...addedIds.flatMap(id => {
|
|
574
574
|
const storage = archetype.storageById[id];
|
|
@@ -579,8 +579,8 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
579
579
|
return storage === undefined ? [] : [{ ticks: storage.changed, chunks: storage.chunkChanged, blocks: storage.blockChanged }];
|
|
580
580
|
}),
|
|
581
581
|
];
|
|
582
|
-
const entry: Filtered = { version: archetype.version, filters, addresses: noAddresses, addressed: null };
|
|
583
|
-
filterCache.
|
|
582
|
+
const entry: Filtered = { archetype, version: archetype.version, filters, addresses: noAddresses, addressed: null };
|
|
583
|
+
filterCache[archetype.id] = entry;
|
|
584
584
|
return entry;
|
|
585
585
|
};
|
|
586
586
|
|
|
@@ -614,13 +614,14 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
614
614
|
const { value } = row;
|
|
615
615
|
const values = context.value;
|
|
616
616
|
const input = runInput;
|
|
617
|
+
const { body } = system;
|
|
617
618
|
const filters = filtersOf(archetype);
|
|
618
619
|
// Every row passes, so nothing is filtered.
|
|
619
620
|
// A body may remove its own system, after which no row of it runs.
|
|
620
621
|
if (since < 0 || filters.length === 0) {
|
|
621
622
|
for (let index = 0, count = archetype.count; index < count && !holdings.removed; index++) {
|
|
622
623
|
row.index = index;
|
|
623
|
-
const returned =
|
|
624
|
+
const returned = body(value, values, input);
|
|
624
625
|
if (returned !== undefined) noPromise(returned);
|
|
625
626
|
}
|
|
626
627
|
return;
|
|
@@ -628,7 +629,7 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
628
629
|
forEachPassingRange(filters, since, 0, archetype.count, (start, end) => {
|
|
629
630
|
for (let index = start; index < end && !holdings.removed; index++) {
|
|
630
631
|
row.index = index;
|
|
631
|
-
const returned =
|
|
632
|
+
const returned = body(value, values, input);
|
|
632
633
|
if (returned !== undefined) noPromise(returned);
|
|
633
634
|
}
|
|
634
635
|
});
|
|
@@ -714,10 +715,11 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
714
715
|
const upContext = context.value;
|
|
715
716
|
const input = runInput;
|
|
716
717
|
upContext.up = parentRow === null ? null : parentRow.value;
|
|
718
|
+
const { body } = system;
|
|
717
719
|
for (let n = start; n < end && !holdings.removed; n++) {
|
|
718
720
|
row.index = rows[n];
|
|
719
721
|
if (parentRow !== null) parentRow.index = parents[n];
|
|
720
|
-
const returned =
|
|
722
|
+
const returned = body(row.value, upContext, input);
|
|
721
723
|
if (returned !== undefined) noPromise(returned);
|
|
722
724
|
}
|
|
723
725
|
};
|
|
@@ -748,9 +750,10 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
748
750
|
}
|
|
749
751
|
const row = bindRow(archetype);
|
|
750
752
|
const { value } = row;
|
|
753
|
+
const { body } = system;
|
|
751
754
|
for (let n = run.start; n < run.end && !holdings.removed; n++) {
|
|
752
755
|
row.index = run.rows[n];
|
|
753
|
-
const returned =
|
|
756
|
+
const returned = body(value, contextValue, input);
|
|
754
757
|
if (returned !== undefined) noPromise(returned);
|
|
755
758
|
}
|
|
756
759
|
}
|
|
@@ -762,6 +765,7 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
762
765
|
const { entries, stride, words, lengths, blocks } = sorted;
|
|
763
766
|
const contextValue = context.value;
|
|
764
767
|
const input = runInput;
|
|
768
|
+
const { body } = system;
|
|
765
769
|
let current = -1;
|
|
766
770
|
let row: ReturnType<typeof bindRow> | null = null;
|
|
767
771
|
let value: object | null = null;
|
|
@@ -782,7 +786,7 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
782
786
|
current = owner;
|
|
783
787
|
}
|
|
784
788
|
row.index = entries[at + 2];
|
|
785
|
-
const returned =
|
|
789
|
+
const returned = body(value, contextValue, input);
|
|
786
790
|
if (returned !== undefined) noPromise(returned);
|
|
787
791
|
}
|
|
788
792
|
}
|
|
@@ -8,17 +8,25 @@ import { scheduleOf } from "../../interface/registry/schedule.js";
|
|
|
8
8
|
|
|
9
9
|
const value = Symbol("scheduleTest/value");
|
|
10
10
|
const other = Symbol("scheduleTest/other");
|
|
11
|
+
const voxel = Symbol("scheduleTest/voxel");
|
|
12
|
+
const mark = Symbol("scheduleTest/mark");
|
|
13
|
+
const size = Symbol("scheduleTest/size");
|
|
11
14
|
|
|
12
15
|
declare module "@adobe/ecs" {
|
|
13
16
|
export interface Components {
|
|
14
17
|
[value]: { type: number };
|
|
15
18
|
[other]: { type: number };
|
|
19
|
+
[voxel]: { type: true };
|
|
20
|
+
[mark]: { type: true };
|
|
21
|
+
}
|
|
22
|
+
export interface Resources {
|
|
23
|
+
[size]: { type: number };
|
|
16
24
|
}
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
|
|
20
28
|
/** Systems built as values, not registered, so each test's are its own. */
|
|
21
|
-
const leaf = (declaration: Resolved & { readonly name: string }): Leaf => ({ kind: "system", name: declaration.name, declaration, input: undefined, body: () => {} });
|
|
29
|
+
const leaf = (declaration: Resolved & { readonly name: string }): Leaf => ({ kind: "system", name: declaration.name, declaration, input: undefined, body: () => {}, commands: () => undefined });
|
|
22
30
|
const composite = (declaration: Composite["declaration"] & { readonly name: string }): Composite => ({ kind: "composite", name: declaration.name, declaration });
|
|
23
31
|
|
|
24
32
|
const names = (system: Definition): string[][] => scheduleOf(system).tiers.map(tier => tier.map(leaf => leaf.name));
|
|
@@ -113,6 +121,39 @@ describe("scheduleOf", () => {
|
|
|
113
121
|
.toEqual([["a", "c"], ["b"]]);
|
|
114
122
|
});
|
|
115
123
|
|
|
124
|
+
it("doesn't order two systems on a key both reach through rows that never match one entity", () => {
|
|
125
|
+
const voxels = leaf({ name: "voxels", components: { write: [value], has: [voxel], not: [mark] } });
|
|
126
|
+
const marks = leaf({ name: "marks", components: { write: [value], has: [mark] } });
|
|
127
|
+
const watching = leaf({ name: "watching", components: { read: [value], changed: [value], has: [mark] } });
|
|
128
|
+
expect(names(composite({ name: "root", systems: [voxels, marks] }))).toEqual([["voxels", "marks"]]);
|
|
129
|
+
expect(names(composite({ name: "root", systems: [voxels, watching] }))).toEqual([["voxels", "watching"]]);
|
|
130
|
+
});
|
|
131
|
+
it("orders two systems whose rows may match one entity, or that reach the key other than through their rows", () => {
|
|
132
|
+
const voxels = leaf({ name: "voxels", components: { write: [value], has: [voxel] } });
|
|
133
|
+
const marks = leaf({ name: "marks", components: { write: [value], has: [mark] } });
|
|
134
|
+
expect(() => scheduleOf(composite({ name: "root", systems: [voxels, marks] })))
|
|
135
|
+
.toThrow(`Systems "voxels" and "marks" both access "scheduleTest/value" and one writes it; order them with before or after, or, if they never match one entity, add a component of one to the other's not.`);
|
|
136
|
+
const apart = leaf({ name: "apart", components: { write: [value], has: [voxel], not: [mark] } });
|
|
137
|
+
const looking = leaf({ name: "looking", components: { has: [mark] }, lookup: { others: { read: [value] } } });
|
|
138
|
+
expect(() => scheduleOf(composite({ name: "root", systems: [apart, looking] })))
|
|
139
|
+
.toThrow(`Systems "apart" and "looking" both access "scheduleTest/value" and one writes it; order them with before or after.`);
|
|
140
|
+
// An optional component is read where an entity has it, so it excludes nothing it doesn't require.
|
|
141
|
+
const optional = leaf({ name: "optional", components: { read: [value, mark], optional: [mark] } });
|
|
142
|
+
expect(() => scheduleOf(composite({ name: "root", systems: [apart, optional] }))).toThrow('"apart" and "optional"');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("orders a system before or after every other that uses a resource it lists, but those already ordered and those listing it too", () => {
|
|
146
|
+
const resize = leaf({ name: "resize", resources: { write: [size] }, before: [size] });
|
|
147
|
+
const frame = leaf({ name: "frame", resources: { read: [size] } });
|
|
148
|
+
const pick = leaf({ name: "pick", components: { read: [value] }, resources: { is: [size] } });
|
|
149
|
+
const old = leaf({ name: "old", resources: { read: [size] }, before: [resize] });
|
|
150
|
+
expect(names(composite({ name: "root", systems: [frame, pick, resize, old] }))).toEqual([["old"], ["resize"], ["frame", "pick"]]);
|
|
151
|
+
const last = leaf({ name: "last", resources: { read: [size] }, after: [size] });
|
|
152
|
+
expect(names(composite({ name: "root", systems: [last, frame, resize] }))).toEqual([["resize"], ["frame"], ["last"]]);
|
|
153
|
+
const again = leaf({ name: "again", resources: { write: [size] }, before: [size] });
|
|
154
|
+
expect(() => scheduleOf(composite({ name: "root", systems: [resize, again, frame] }))).toThrow(`Systems "resize" and "again" both access "scheduleTest/size"`);
|
|
155
|
+
});
|
|
156
|
+
|
|
116
157
|
it("schedules two systems of one name as two, and one listed twice as one", () => {
|
|
117
158
|
const a = leaf({ name: "a", components: { write: [value] } });
|
|
118
159
|
const copy = leaf({ name: "a", components: { write: [value] }, after: [a] });
|
|
@@ -195,10 +195,10 @@ describe("a shared component", () => {
|
|
|
195
195
|
}));
|
|
196
196
|
expect(looked.sort()).toEqual(["1 sees red", "2 sees red"]);
|
|
197
197
|
const assign: (row: object) => void = row => { Reflect.set(row, team, "blue"); };
|
|
198
|
-
|
|
198
|
+
// Taken whole, the row is written: every member but shared ones and references.
|
|
199
|
+
const assigning = System.create({ kernel: false, components: { with: [team, health], whole: true } }, row => {
|
|
199
200
|
if (row[health] === 1) assign(row);
|
|
200
201
|
});
|
|
201
|
-
// The build takes the row as written whole, since it can't follow it: every member but shared ones and references.
|
|
202
202
|
expect((await runReported(world, assigning)).failures.map(failure => failure.message)).toEqual([
|
|
203
203
|
expect.stringContaining(`"sharedTest/team" is shared: its value is its archetype's, so give it another with commands.add rather than assigning it`),
|
|
204
204
|
]);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import type { Recording } from './run-changes.js';
|
|
2
3
|
import { createHolds, type Holds } from './stop-lists.js';
|
|
3
4
|
import type { Entity } from '../../interface/entity/entity.js';
|
|
4
5
|
import type { Leaf } from '../../interface/system/leaf.js';
|
|
@@ -77,6 +78,14 @@ export const countRan = (state: State, runners: readonly RanRunner[]): void => {
|
|
|
77
78
|
for (let k = 0; k < tier.logs.length; k++) ran.joined.add(tier.logs[k]);
|
|
78
79
|
};
|
|
79
80
|
|
|
81
|
+
/** Counts runners into `ran` that all ran here, none on a joined thread: on `main` unless a joined thread claims it. */
|
|
82
|
+
export const countRanHere = (state: State, systems: number, gpu: boolean): void => {
|
|
83
|
+
const { ran } = state;
|
|
84
|
+
ran.systems += systems;
|
|
85
|
+
if (systems !== 0 && state.claims.get("main") === undefined) ran.main = true;
|
|
86
|
+
if (gpu) ran.gpu = true;
|
|
87
|
+
};
|
|
88
|
+
|
|
80
89
|
/** Starts gathering what a run ran. */
|
|
81
90
|
export const startRan = (ran: Ran): void => {
|
|
82
91
|
ran.systems = 0;
|
|
@@ -126,6 +135,13 @@ export type State = {
|
|
|
126
135
|
reshapes: number;
|
|
127
136
|
/** Sync points applied, which reclaiming counts. */
|
|
128
137
|
syncs: number;
|
|
138
|
+
/**
|
|
139
|
+
* By id, each resource a changes finalizer watches: its value and change tick when its finalizers last ran, which a
|
|
140
|
+
* later write's report gives as its previous value. Empty while none watches one.
|
|
141
|
+
*/
|
|
142
|
+
readonly resourcesSeen: Map<number, { value: unknown; tick: number }>;
|
|
143
|
+
/** What the run in progress records of its changes, when it was asked for them; null otherwise. */
|
|
144
|
+
recording: Recording | null;
|
|
129
145
|
/** The records strings' columns name, in shared memory; null without it, where strings are held as objects. */
|
|
130
146
|
readonly strings: StringStore | null;
|
|
131
147
|
/** Whether each component is derived, so only rules add and remove it. */
|
|
@@ -317,6 +333,8 @@ export const createState = (shared?: boolean, wasm = true, simd = true, gpu = fa
|
|
|
317
333
|
membership: { log: [], base: 0 },
|
|
318
334
|
reshapes: 0,
|
|
319
335
|
syncs: 0,
|
|
336
|
+
resourcesSeen: new Map(),
|
|
337
|
+
recording: null,
|
|
320
338
|
strings: memory.shared ? createStrings(memory) : null,
|
|
321
339
|
derived: [],
|
|
322
340
|
transfers: [],
|
|
@@ -504,6 +522,15 @@ export const commandIdOf = (state: State, key: unknown): number => {
|
|
|
504
522
|
return id;
|
|
505
523
|
};
|
|
506
524
|
|
|
525
|
+
/** The id of a component a query names, derived ones included. Throws for a key that isn't a registered component. */
|
|
526
|
+
export const queryIdOf = (state: State, key: unknown): number => {
|
|
527
|
+
const known = typeof key === "symbol" ? state.ids.get(key) : undefined;
|
|
528
|
+
if (known !== undefined && !state.resources.has(known)) return known;
|
|
529
|
+
if (typeof key !== "symbol") throw new Error(`"${labelOf(key)}" isn't a component: a component's key is a symbol.`);
|
|
530
|
+
if (kindOf(key) === "resource") throw new Error(`"${labelOf(key)}" is a resource, not a component.`);
|
|
531
|
+
return idOf(state, key);
|
|
532
|
+
};
|
|
533
|
+
|
|
507
534
|
/**
|
|
508
535
|
* An archetype's key: its sorted component ids, each shared one with the number of its value, so archetypes of one
|
|
509
536
|
* component set but other values are others.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { Entity } from "../../interface/entity/entity.js";
|
|
4
|
+
import { Index } from "../../interface/indexes/index-type.js";
|
|
5
|
+
import { System } from "../../interface/system/system.js";
|
|
6
|
+
import { workers } from "../../interface/resources/workers.js";
|
|
7
|
+
import { createWorld } from "./create.js";
|
|
8
|
+
import { count as countOf, readEach, select } from "./reads.test-helper.js";
|
|
9
|
+
|
|
10
|
+
const position = Symbol("syncPointsTest/position");
|
|
11
|
+
const cell = Symbol("syncPointsTest/cell");
|
|
12
|
+
const found = Symbol("syncPointsTest/found");
|
|
13
|
+
const team = Symbol("syncPointsTest/team");
|
|
14
|
+
const member = Symbol("syncPointsTest/member");
|
|
15
|
+
const local = Symbol("syncPointsTest/local");
|
|
16
|
+
const total = Symbol("syncPointsTest/total");
|
|
17
|
+
const parent = Symbol("syncPointsTest/parent");
|
|
18
|
+
const seed = Symbol("syncPointsTest/seed");
|
|
19
|
+
const grown = Symbol("syncPointsTest/grown");
|
|
20
|
+
const children = Symbol("syncPointsTest/children");
|
|
21
|
+
|
|
22
|
+
declare module "@adobe/ecs" {
|
|
23
|
+
export interface Components {
|
|
24
|
+
[position]: { type: number };
|
|
25
|
+
[cell]: { type: number };
|
|
26
|
+
[team]: { type: true };
|
|
27
|
+
[member]: { type: Entity; reference: "owner" };
|
|
28
|
+
[local]: { type: number };
|
|
29
|
+
[total]: { type: number };
|
|
30
|
+
[parent]: { type: Entity; reference: "weak" };
|
|
31
|
+
[seed]: { type: number };
|
|
32
|
+
[grown]: { type: number };
|
|
33
|
+
}
|
|
34
|
+
export interface Resources {
|
|
35
|
+
[found]: { type: number };
|
|
36
|
+
[children]: { type: number; default: 0 };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const byCell = Index.create({ key: [cell] });
|
|
41
|
+
|
|
42
|
+
const spawn = System.create(({ commands }) => { for (let k = 0; k < 3; k++) commands.spawn([position])({ [position]: k }); });
|
|
43
|
+
const counted: number[] = [];
|
|
44
|
+
const count = System.create({ components: { with: [position] }, after: [spawn] }, row => { counted.push(row[position]); });
|
|
45
|
+
const changes: number[] = [];
|
|
46
|
+
const watchChanges = System.create({ components: { with: [position], changed: [position] }, after: [count] }, row => { changes.push(row[position]); });
|
|
47
|
+
const deriveCells = System.create({ components: { with: [position], not: [cell] }, after: [watchChanges] }, (row, { commands }) => { commands.add(row.entity, cell, row[position] % 2); });
|
|
48
|
+
const pick = System.create({ lookup: { cells: { index: byCell } }, after: [deriveCells] }, ({ lookup, resources }) => { resources[found] = lookup.cells(0).length; });
|
|
49
|
+
const readFound = System.create({ readonly: true }, ({ resources }) => resources[found]);
|
|
50
|
+
const frame = System.create([spawn, count, watchChanges, deriveCells, pick]);
|
|
51
|
+
|
|
52
|
+
const spawnTeams = System.create(({ commands }) => {
|
|
53
|
+
for (let k = 0; k < 3; k++) {
|
|
54
|
+
const owner = commands.spawn([team])();
|
|
55
|
+
for (let m = 0; m < 2; m++) commands.spawn([member])({ [member]: owner });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
const disband = System.create({ components: { with: [team] }, after: [spawnTeams] }, (row, { commands }) => { commands.despawn(row.entity); });
|
|
59
|
+
const round = System.create([spawnTeams, disband]);
|
|
60
|
+
|
|
61
|
+
/** Spawns a root, `count` children of it and a child of each. */
|
|
62
|
+
const spawnTree = System.create({}, ({ commands }, input: { readonly count: number }) => {
|
|
63
|
+
const root = commands.spawn([local, total])({ [local]: 1, [total]: 0 });
|
|
64
|
+
for (let k = 0; k < input.count; k++) {
|
|
65
|
+
const child = commands.spawn([local, total, parent])({ [local]: 1, [total]: 0, [parent]: root });
|
|
66
|
+
commands.spawn([local, total, parent])({ [local]: 1, [total]: 0, [parent]: child });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
const propagate = System.create({ components: { with: [local, total], changed: [local] }, up: { through: parent }, after: [spawnTree] }, (row, { up }) => {
|
|
70
|
+
row[total] = (up === null ? 0 : up[total]) + row[local];
|
|
71
|
+
});
|
|
72
|
+
const grow = System.create([spawnTree, propagate]);
|
|
73
|
+
|
|
74
|
+
const plant = System.create(({ commands }) => { for (let k = 0; k < 64; k++) commands.spawn([seed])({ [seed]: k }); });
|
|
75
|
+
const sprout = System.create({ kernel: true, components: { with: [seed] }, after: [plant] }, (row, { commands }) => {
|
|
76
|
+
commands.spawn([grown])({ [grown]: row[seed] * 2 });
|
|
77
|
+
});
|
|
78
|
+
const tally = System.create({ components: { with: [grown] }, after: [sprout] }, (row, { resources }) => { resources[children] += row[grown]; });
|
|
79
|
+
const garden = System.create([plant, sprout, tally]);
|
|
80
|
+
const readChildren = System.create({ readonly: true }, ({ resources }) => resources[children]);
|
|
81
|
+
|
|
82
|
+
const spawnSeedlings = System.create(({ commands }) => { commands.spawn(Seedling)({ [seed]: 1, [grown]: 2 }); });
|
|
83
|
+
const countSeedlings = System.create({ components: { with: [seed, grown] }, after: [spawnSeedlings] }, (row, { resources }) => { resources[children] += row[grown]; });
|
|
84
|
+
/** Declared below the systems whose commands name it. */
|
|
85
|
+
const Seedling = [seed, grown] as const;
|
|
86
|
+
|
|
87
|
+
describe("sync points within a run", () => {
|
|
88
|
+
it("apply commands before a later system that depends on what they change, which then sees them in the same run", async () => {
|
|
89
|
+
const world = createWorld({ [workers]: 0 });
|
|
90
|
+
await world.run(frame);
|
|
91
|
+
expect(counted).toEqual([0, 1, 2]);
|
|
92
|
+
expect(await world.run(readFound)).toBe(2);
|
|
93
|
+
await world.dispose();
|
|
94
|
+
});
|
|
95
|
+
it("keep each change seen once by change filters across runs", async () => {
|
|
96
|
+
const world = createWorld({ [workers]: 0 });
|
|
97
|
+
changes.length = 0;
|
|
98
|
+
await world.run(frame);
|
|
99
|
+
expect(changes).toEqual([0, 1, 2]);
|
|
100
|
+
changes.length = 0;
|
|
101
|
+
counted.length = 0;
|
|
102
|
+
await world.run(frame);
|
|
103
|
+
// The second run's spawns are new; the first run's three are seen again by count alone.
|
|
104
|
+
expect([counted.length, changes]).toEqual([6, [0, 1, 2]]);
|
|
105
|
+
expect(await world.run(readFound)).toBe(4);
|
|
106
|
+
await world.dispose();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("despawn what rows spawned at an earlier sync point of the run own, once a later tier despawns their owners", async () => {
|
|
110
|
+
expect(System.describe(round).tiers.map(tier => tier.sync === null)).toEqual([false, true]);
|
|
111
|
+
await using world = createWorld({ [workers]: 0 });
|
|
112
|
+
await world.run(round);
|
|
113
|
+
expect([await countOf(world, { required: [team] }), await countOf(world, { required: [member] })]).toEqual([0, 0]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("bring an `up` tree up to date at each, built again or patched, for a tier after it in the same run", async () => {
|
|
117
|
+
expect(System.describe(grow).tiers.map(tier => tier.sync === null)).toEqual([false, true]);
|
|
118
|
+
await using world = createWorld({ [workers]: 0 });
|
|
119
|
+
let spawned = 0;
|
|
120
|
+
// Past the journal's worth a tree patches, then within it.
|
|
121
|
+
for (const count of [5_000, 3_000, 10]) {
|
|
122
|
+
await world.run(grow, { count });
|
|
123
|
+
spawned += count;
|
|
124
|
+
const totals = await readEach(world, await select(world, { required: [total] }), total);
|
|
125
|
+
const byDepth = [1, 2, 3].map(depth => totals.filter(value => value === depth).length);
|
|
126
|
+
expect([byDepth, totals.length]).toEqual([[totals.length - 2 * spawned, spawned, spawned], totals.length]);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("follow commands naming an archetype declared below their system", async () => {
|
|
131
|
+
expect(System.describe(System.create([spawnSeedlings, countSeedlings])).tiers.map(tier => tier.sync?.map(reason => reason.keys) ?? null)).toEqual([[[seed, grown]], null]);
|
|
132
|
+
await using world = createWorld({ [workers]: 0 });
|
|
133
|
+
await world.run(System.create([spawnSeedlings, countSeedlings]));
|
|
134
|
+
expect(await world.run(readChildren)).toBe(2);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("commit a kernel's staged spawns at a sync point within the run, for a later tier", async () => {
|
|
138
|
+
expect(System.describe(garden).tiers.map(tier => tier.sync === null)).toEqual([false, false, true]);
|
|
139
|
+
await using world = createWorld({ [workers]: 0 });
|
|
140
|
+
const sum = 63 * 64;
|
|
141
|
+
await world.run(garden);
|
|
142
|
+
expect(await world.run(readChildren)).toBe(sum);
|
|
143
|
+
// The second run's seeds are both runs' plantings, and its tally reads the first run's grown rows too.
|
|
144
|
+
await world.run(garden);
|
|
145
|
+
expect(await world.run(readChildren)).toBe(sum + sum + 2 * sum);
|
|
146
|
+
});
|
|
147
|
+
});
|