@adobe/ecs 0.2.4 → 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/inferred.js +3 -2
- package/dist/implementation/compiler/inferred.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 +19 -13
- package/dist/implementation/world/activate.js.map +1 -1
- package/dist/implementation/world/commands.js +3 -2
- package/dist/implementation/world/commands.js.map +1 -1
- package/dist/implementation/world/create.js +4 -4
- package/dist/implementation/world/create.js.map +1 -1
- package/dist/implementation/world/execute.js +97 -11
- package/dist/implementation/world/execute.js.map +1 -1
- package/dist/implementation/world/finalizers.d.ts +10 -2
- package/dist/implementation/world/finalizers.js +42 -13
- package/dist/implementation/world/finalizers.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/state.d.ts +2 -0
- package/dist/implementation/world/state.js +9 -0
- package/dist/implementation/world/state.js.map +1 -1
- package/dist/implementation/world/synchronize.js +16 -9
- package/dist/implementation/world/synchronize.js.map +1 -1
- package/dist/implementation/world/tables.d.ts +2 -1
- package/dist/implementation/world/tables.js +55 -2
- package/dist/implementation/world/tables.js.map +1 -1
- package/dist/interface/finalizer/body.d.ts +24 -3
- package/dist/interface/finalizer/create.d.ts +3 -3
- package/dist/interface/finalizer/create.js +43 -11
- package/dist/interface/finalizer/create.js.map +1 -1
- package/dist/interface/finalizer/declaration.d.ts +25 -5
- 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 +1 -0
- package/dist/interface/registry/schedule.js +86 -11
- package/dist/interface/registry/schedule.js.map +1 -1
- package/dist/interface/system/check.js +4 -0
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +8 -3
- package/dist/interface/system/create-resolved.js +2 -1
- package/dist/interface/system/create-resolved.js.map +1 -1
- package/dist/interface/system/create.js +2 -2
- package/dist/interface/system/create.js.map +1 -1
- package/dist/interface/system/declaration.d.ts +11 -4
- package/dist/interface/system/describe.js +1 -1
- package/dist/interface/system/describe.js.map +1 -1
- package/dist/interface/system/leaf.d.ts +4 -3
- package/dist/interface/system/render.js +2 -2
- package/dist/interface/system/render.js.map +1 -1
- package/dist/interface/system/resolve.d.ts +12 -3
- package/dist/interface/system/resolve.js +35 -4
- package/dist/interface/system/resolve.js.map +1 -1
- package/package.json +1 -1
- 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 +14 -6
- package/src/guide/06-sync-points.out +2 -0
- package/src/guide/06-sync-points.ts +15 -0
- package/src/guide/10-finalizers.md +5 -0
- package/src/guide/18-identity.out +1 -1
- package/src/guide/guide.test.ts +4 -4
- package/src/implementation/compiler/augmentations.test.ts +28 -1
- package/src/implementation/compiler/augmentations.ts +5 -1
- package/src/implementation/compiler/inferred.ts +3 -2
- package/src/implementation/compiler/transform.test.ts +4 -4
- package/src/implementation/compiler/type-schemas.ts +28 -2
- package/src/implementation/world/activate.ts +24 -11
- package/src/implementation/world/commands.ts +3 -2
- package/src/implementation/world/create.ts +4 -4
- package/src/implementation/world/defaults.test.ts +24 -1
- package/src/implementation/world/despawn-where.test.ts +31 -8
- package/src/implementation/world/execute.ts +98 -9
- package/src/implementation/world/finalizers.ts +43 -14
- package/src/implementation/world/lifetime.test-helper.ts +1 -1
- package/src/implementation/world/passes.test.ts +61 -0
- package/src/implementation/world/perf.md +9 -0
- package/src/implementation/world/program.ts +4 -1
- package/src/implementation/world/run-results.test.ts +17 -0
- package/src/implementation/world/schedule.test.ts +42 -1
- package/src/implementation/world/state.ts +8 -0
- package/src/implementation/world/sync-points.test.ts +12 -0
- package/src/implementation/world/synchronize.ts +15 -9
- package/src/implementation/world/tables.test.ts +36 -3
- package/src/implementation/world/tables.ts +48 -3
- 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/interface/finalizer/body.ts +22 -3
- package/src/interface/finalizer/create.test.ts +4 -1
- package/src/interface/finalizer/create.ts +40 -12
- package/src/interface/finalizer/declaration.ts +30 -5
- package/src/interface/registry/index.ts +1 -1
- package/src/interface/registry/names.ts +19 -0
- package/src/interface/registry/schedule.ts +89 -16
- package/src/interface/rule/rule.type-test.ts +23 -1
- package/src/interface/system/check.ts +2 -0
- package/src/interface/system/commands.ts +8 -3
- package/src/interface/system/create-resolved.ts +2 -1
- package/src/interface/system/create.test.ts +3 -3
- package/src/interface/system/create.ts +2 -2
- package/src/interface/system/declaration.ts +10 -4
- package/src/interface/system/describe.test.ts +19 -0
- package/src/interface/system/describe.ts +1 -1
- package/src/interface/system/leaf.ts +4 -3
- package/src/interface/system/render.ts +2 -2
- package/src/interface/system/resolve.test.ts +17 -1
- package/src/interface/system/resolve.ts +40 -5
- package/src/interface/system/system.type-test.ts +6 -0
- package/src/perftest/benchmarks.ts +2 -0
- package/src/perftest/chained.ts +108 -0
|
@@ -6,13 +6,13 @@ import { addFinalizer, programFinalizers } from '../registry/program.js';
|
|
|
6
6
|
import { requireRegistered, threadOf } from '../registry/threads.js';
|
|
7
7
|
import { checkWorldWritten } from '../registry/written.js';
|
|
8
8
|
import { checkLookup } from '../registry/indexes.js';
|
|
9
|
-
import type {
|
|
9
|
+
import type { Property } from '../system/property.js';
|
|
10
10
|
import type { ThreadOfComponent } from '../component/value.js';
|
|
11
11
|
import type { ThreadOfResource } from '../resource/value.js';
|
|
12
12
|
import type { Resource } from '../resource/resource.js';
|
|
13
13
|
import type { Component } from '../component/component.js';
|
|
14
14
|
import type { Elsewhere, OneThread, ThreadedBy, ThreadsOf } from '../system/threads.js';
|
|
15
|
-
import type { ChangeSet, Context, Row, Watched, WatchedResources } from './body.js';
|
|
15
|
+
import type { ChangeSet, Context, Row, Watched, WatchedResources, WatchListed } from './body.js';
|
|
16
16
|
import type { Changes, Leaf } from './created.js';
|
|
17
17
|
import type { ChangesDeclaration, Declaration, Resolved, ResolvedChanges } from './declaration.js';
|
|
18
18
|
import { accessOf, compact, lookupsLists, resourceUses, type Access } from '../system/resolve.js';
|
|
@@ -28,7 +28,7 @@ type Remote<D> = true extends Elsewhere<ThreadsOf<D>>
|
|
|
28
28
|
: {};
|
|
29
29
|
|
|
30
30
|
/** A changes finalizer runs on the world's thread, in the sync point: it watches and reads no other thread's objects. */
|
|
31
|
-
type OnWorld<D> = true extends Elsewhere<ThreadsOf<D> | ThreadOfComponent<
|
|
31
|
+
type OnWorld<D extends ChangesDeclaration> = true extends Elsewhere<ThreadsOf<D> | ThreadOfComponent<Extract<WatchListed<D, "components">, Component.Key>> | ThreadOfResource<Extract<WatchListed<D, "resources">, Resource.Key>>>
|
|
32
32
|
? { readonly "error: a changes finalizer watches and reads only the world's thread's objects": true }
|
|
33
33
|
: {};
|
|
34
34
|
|
|
@@ -127,6 +127,39 @@ export const create = <const D extends Declaration>(
|
|
|
127
127
|
return addFinalizer(finalizer, id);
|
|
128
128
|
};
|
|
129
129
|
|
|
130
|
+
const isList = (watch: NonNullable<ChangesDeclaration["watch"]>): watch is Extract<ChangesDeclaration["watch"], readonly unknown[]> => Array.isArray(watch);
|
|
131
|
+
|
|
132
|
+
/** What a changes finalizer watches, sorted by kind. Throws for a key that's neither of the kinds it's listed as, or derived. */
|
|
133
|
+
const watchOf = (name: string, watch: ChangesDeclaration["watch"]): ResolvedChanges["watch"] => {
|
|
134
|
+
const components: symbol[] = [];
|
|
135
|
+
const componentTraits: symbol[] = [];
|
|
136
|
+
const resources: symbol[] = [];
|
|
137
|
+
const resourceTraits: symbol[] = [];
|
|
138
|
+
const sort = (key: unknown, kinds: readonly ("component" | "resource")[]): void => {
|
|
139
|
+
const kind = kindOf(key);
|
|
140
|
+
if (typeof key !== "symbol" || kind === undefined || (kind !== "trait" && !kinds.includes(kind))) {
|
|
141
|
+
const among = kinds.length === 2 ? "" : ` among its ${kinds[0]}s`;
|
|
142
|
+
throw new Error(`Finalizer "${name}" watches "${labelOf(key)}"${among}, which isn't a registered ${kinds.length === 2 ? "component, resource" : kinds[0]} or trait: ${whyUnregistered(key)}.`);
|
|
143
|
+
}
|
|
144
|
+
if (kind === "trait") {
|
|
145
|
+
if (kinds.includes("component")) componentTraits.push(key);
|
|
146
|
+
if (kinds.includes("resource")) resourceTraits.push(key);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (declarationOf(key)?.derived === true) throw new Error(`Finalizer "${name}" watches "${labelOf(key)}", which is derived; it sees only components user code adds and removes.`);
|
|
150
|
+
(kind === "component" ? components : resources).push(key);
|
|
151
|
+
};
|
|
152
|
+
if (watch === undefined) return { components: true, componentTraits, resources, resourceTraits };
|
|
153
|
+
if (isList(watch)) {
|
|
154
|
+
for (const key of watch) sort(key, ["component", "resource"]);
|
|
155
|
+
return { components, componentTraits, resources, resourceTraits };
|
|
156
|
+
}
|
|
157
|
+
const every = watch.components === true;
|
|
158
|
+
if (!every) for (const key of watch.components) sort(key, ["component"]);
|
|
159
|
+
for (const key of watch.resources) sort(key, ["resource"]);
|
|
160
|
+
return { components: every ? true : components, componentTraits: every ? [] : componentTraits, resources, resourceTraits };
|
|
161
|
+
};
|
|
162
|
+
|
|
130
163
|
/**
|
|
131
164
|
* Declares a finalizer of each sync point's structural changes, which applies in every world, and returns it. It runs
|
|
132
165
|
* on the world's thread. Throws as `create` does, for a watched component that isn't registered or is derived, a
|
|
@@ -139,7 +172,7 @@ export const changes = <const D extends ChangesDeclaration>(
|
|
|
139
172
|
const id = takeIdOfDeclaration(declared);
|
|
140
173
|
const name = nameOf(declared, id, "finalizer");
|
|
141
174
|
const what = `Finalizer "${name}"`;
|
|
142
|
-
const declaration: ResolvedChanges = { ...resolveShared(what, declared, accessOf(what, declared)),
|
|
175
|
+
const declaration: ResolvedChanges = { ...resolveShared(what, declared, accessOf(what, declared)), watch: watchOf(name, declared.watch) };
|
|
143
176
|
const finalizer: Changes = { kind: "changes", name, declaration, body };
|
|
144
177
|
checkResources(name, declaration.resources);
|
|
145
178
|
requireRegistered("Finalizer", name, "resource", declaration.resources?.read);
|
|
@@ -149,14 +182,9 @@ export const changes = <const D extends ChangesDeclaration>(
|
|
|
149
182
|
checkLookup("Finalizer", name, lookupName, lookup);
|
|
150
183
|
for (const keys of [lookup.read, "has" in lookup ? lookup.has : undefined, "not" in lookup ? lookup.not : undefined]) requireRegistered("Finalizer", name, "component", keys);
|
|
151
184
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (declarationOf(key)?.derived === true) throw new Error(`Finalizer "${name}" watches "${labelOf(key)}", which is derived; it sees only components user code adds and removes.`);
|
|
156
|
-
}
|
|
157
|
-
const watched = (kind: "component" | "resource"): readonly symbol[] => (declaration.watch ?? []).filter(key => kindOf(key) === kind);
|
|
158
|
-
const read = [...declaration.resources?.read ?? [], ...watched("resource")];
|
|
159
|
-
const thread = threadOf({ components: { read: watched("component") }, resources: { ...declaration.resources, read }, lookup: declaration.lookup }, "Finalizer", name);
|
|
185
|
+
const { watch } = declaration;
|
|
186
|
+
const read = [...declaration.resources?.read ?? [], ...watch.resources];
|
|
187
|
+
const thread = threadOf({ components: { read: watch.components === true ? [] : watch.components }, resources: { ...declaration.resources, read }, lookup: declaration.lookup }, "Finalizer", name);
|
|
160
188
|
if (thread !== null && thread !== "world") throw new Error(`Finalizer "${name}" watches or reads values on thread "${thread}"; a changes finalizer runs on the world's thread, in the sync point.`);
|
|
161
189
|
checkOrder(name, "world", declaration);
|
|
162
190
|
return addFinalizer(finalizer, id);
|
|
@@ -5,6 +5,8 @@ import type { Lookup } from '../system/access.js';
|
|
|
5
5
|
import type { Finalizer } from './finalizer.js';
|
|
6
6
|
import type { Essential } from '../component/value.js';
|
|
7
7
|
import type { Resource } from '../resource/resource.js';
|
|
8
|
+
import type { Traits } from '../trait/types.js';
|
|
9
|
+
import type { BuiltIn } from '../trait/key.js';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* What every finalizer declares. What its body reads of its row and lookups, and reads and writes of resources, the
|
|
@@ -25,12 +27,23 @@ export type Declaration = Shared & {
|
|
|
25
27
|
readonly components: { readonly with?: readonly Component.Key[]; readonly optional?: readonly Component.Key[]; readonly not?: readonly Component.Key[] };
|
|
26
28
|
};
|
|
27
29
|
|
|
30
|
+
/** A trait of your own, which watches every component and resource declaring it. */
|
|
31
|
+
export type OwnTrait = Exclude<keyof Traits<never>, BuiltIn>;
|
|
32
|
+
|
|
28
33
|
/**
|
|
29
|
-
* A finalizer that sees each sync point's structural changes to the components it watches
|
|
30
|
-
*
|
|
31
|
-
*
|
|
34
|
+
* A finalizer that sees each sync point's structural changes to the components it watches, and the resources it
|
|
35
|
+
* watches written since the last sync point's finalizers ran. By default it watches every component that isn't derived
|
|
36
|
+
* or an object a joined thread fills, and no resource. `watch` lists components, resources and traits of your own, or,
|
|
37
|
+
* as an object, every component (`components: true`) or those listed, beside the resources listed. A trait watches each
|
|
38
|
+
* component and resource declaring it, as each registers, but those the default leaves out and resources on another
|
|
39
|
+
* thread.
|
|
32
40
|
*/
|
|
33
|
-
export type ChangesDeclaration = Shared & {
|
|
41
|
+
export type ChangesDeclaration = Shared & {
|
|
42
|
+
readonly watch?: readonly (Essential | Resource.Key | OwnTrait)[] | {
|
|
43
|
+
readonly components: true | readonly (Essential | OwnTrait)[];
|
|
44
|
+
readonly resources: readonly (Resource.Key | OwnTrait)[];
|
|
45
|
+
};
|
|
46
|
+
};
|
|
34
47
|
|
|
35
48
|
type Key = symbol;
|
|
36
49
|
|
|
@@ -47,4 +60,16 @@ export type Resolved = ResolvedShared & {
|
|
|
47
60
|
readonly components: { readonly read?: readonly Key[]; readonly has?: readonly Key[]; readonly optional?: readonly Key[]; readonly not?: readonly Key[] };
|
|
48
61
|
};
|
|
49
62
|
|
|
50
|
-
|
|
63
|
+
/**
|
|
64
|
+
* A changes finalizer's declaration as the world takes it. It watches the components listed, or every one (`true`),
|
|
65
|
+
* and those declaring a trait of `componentTraits`; the resources listed, and those declaring a trait of
|
|
66
|
+
* `resourceTraits`.
|
|
67
|
+
*/
|
|
68
|
+
export type ResolvedChanges = ResolvedShared & {
|
|
69
|
+
readonly watch: {
|
|
70
|
+
readonly components: true | readonly Key[];
|
|
71
|
+
readonly componentTraits: readonly Key[];
|
|
72
|
+
readonly resources: readonly Key[];
|
|
73
|
+
readonly resourceTraits: readonly Key[];
|
|
74
|
+
};
|
|
75
|
+
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
export type { AnyDeclaration } from './declarations.js';
|
|
4
4
|
export { schemaOf, threadOfDeclaration } from './declarations.js';
|
|
5
5
|
export type { Kind } from './names.js';
|
|
6
|
-
export { declarationOf, firstKeyOf, keyOfId, kindOf, labelOf, newestKeyOf, whyUnregistered } from './names.js';
|
|
6
|
+
export { declarationOf, declaresTrait, declaredOf, firstKeyOf, keyOfId, keysVersion, kindOf, labelOf, newestKeyOf, whyUnregistered } from './names.js';
|
|
7
7
|
export type { Item } from './program.js';
|
|
8
8
|
export { definitionOf, programFinalizers, programRules, programVersion, requireDefinition } from './program.js';
|
|
9
9
|
export type { Schedule } from './schedule.js';
|
|
@@ -22,6 +22,9 @@ type Entry = {
|
|
|
22
22
|
/** Every component, resource and user trait, by symbol identity: symbols of one id share an entry. */
|
|
23
23
|
const entries = new Map<symbol, Entry>();
|
|
24
24
|
|
|
25
|
+
/** How many keys were registered, counting a symbol registered under another's id. */
|
|
26
|
+
let registered = 0;
|
|
27
|
+
|
|
25
28
|
/** Entries by the ids the plugin gave their keys. */
|
|
26
29
|
const identified = new Map<string, Entry>();
|
|
27
30
|
|
|
@@ -155,12 +158,28 @@ export const claimKey = (key: unknown, kind: Kind, declaration?: AnyDeclaration)
|
|
|
155
158
|
}
|
|
156
159
|
named.newest = symbol;
|
|
157
160
|
entries.set(symbol, named);
|
|
161
|
+
registered++;
|
|
158
162
|
return;
|
|
159
163
|
}
|
|
160
164
|
const description = descriptionOf(symbol);
|
|
161
165
|
const entry: Entry = { kind, declaration, description, id, first: symbol, newest: symbol };
|
|
162
166
|
if (id !== undefined) identified.set(id, entry);
|
|
163
167
|
entries.set(symbol, entry);
|
|
168
|
+
registered++;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/** How many keys are registered so far, so a world can tell when one registered since it last looked. */
|
|
172
|
+
export const keysVersion = (): number => {
|
|
173
|
+
catchUp();
|
|
174
|
+
return registered;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/** Whether a component's or resource's declaration declares one of `traits`, keys of traits of your own. */
|
|
178
|
+
export const declaresTrait = (key: unknown, traits: readonly symbol[]): boolean => {
|
|
179
|
+
const declaration = declarationOf(key);
|
|
180
|
+
if (declaration === undefined) return false;
|
|
181
|
+
for (const declared of Object.getOwnPropertySymbols(declaration)) for (const trait of traits) if (sameKey(declared, trait)) return true;
|
|
182
|
+
return false;
|
|
164
183
|
};
|
|
165
184
|
|
|
166
185
|
/** The symbol last registered under an id, by which a world gives values, or undefined where none was. */
|
|
@@ -53,8 +53,8 @@ type Graph = {
|
|
|
53
53
|
readonly successors: readonly Set<number>[];
|
|
54
54
|
/** Each unit's members, by unit: none for a leaf. */
|
|
55
55
|
readonly members: readonly (readonly number[])[];
|
|
56
|
-
/** The units a list of systems names, readonly systems left out. */
|
|
57
|
-
readonly resolve: (systems: readonly
|
|
56
|
+
/** The units a list of systems names, readonly systems and resources left out. */
|
|
57
|
+
readonly resolve: (systems: readonly unknown[] | undefined) => readonly number[];
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
/**
|
|
@@ -79,8 +79,9 @@ const graphOf = (root: Definition): Graph => {
|
|
|
79
79
|
visit(root);
|
|
80
80
|
const successors = units.flatMap(() => [new Set<number>(), new Set<number>()]);
|
|
81
81
|
// A readonly system is ordered with nothing, so constraints naming one are ignored.
|
|
82
|
-
const resolve = (systems: readonly
|
|
82
|
+
const resolve = (systems: readonly unknown[] | undefined): readonly number[] =>
|
|
83
83
|
(systems ?? []).flatMap(system => {
|
|
84
|
+
if (typeof system !== "object" || system === null) return [];
|
|
84
85
|
const unit = indexOf.get(firstItemOf(system));
|
|
85
86
|
return unit === undefined || isObserver(units[unit]) ? [] : [unit];
|
|
86
87
|
});
|
|
@@ -149,7 +150,7 @@ const depthsOf = ({ units, successors }: Graph, order: readonly number[]): numbe
|
|
|
149
150
|
return depth;
|
|
150
151
|
};
|
|
151
152
|
|
|
152
|
-
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 };
|
|
153
154
|
|
|
154
155
|
/** The resources a leaf reduces into. */
|
|
155
156
|
export const reducedBy = (leaf: Leaf): readonly Key[] => {
|
|
@@ -168,21 +169,53 @@ export const accessOf = (leaf: Leaf): Access => {
|
|
|
168
169
|
for (const key of orderKeysOf(leaf.declaration)) reads.add(firstKeyOf(key));
|
|
169
170
|
if (up !== undefined) for (const key of [up.through, ...up.read ?? []]) reads.add(firstKeyOf(key));
|
|
170
171
|
const writes = new Set<Key>([...components?.write ?? [], ...resources?.write ?? []].map(firstKeyOf));
|
|
171
|
-
return { reads, writes, reduces: new Set(reducedBy(leaf).map(firstKeyOf)) };
|
|
172
|
+
return { reads, writes, reduces: new Set(reducedBy(leaf).map(firstKeyOf)), leaf };
|
|
172
173
|
};
|
|
173
174
|
|
|
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));
|
|
184
|
+
};
|
|
185
|
+
|
|
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
|
+
*/
|
|
175
208
|
const conflictOf = (a: Access, b: Access): Key | undefined => {
|
|
176
|
-
for (const key of a.writes) if (b.writes.has(key) || b.reads.has(key) || b.reduces.has(key)) return key;
|
|
177
|
-
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;
|
|
178
211
|
return undefined;
|
|
179
212
|
};
|
|
180
213
|
|
|
181
|
-
/** Every key two accesses conflict on. */
|
|
214
|
+
/** Every key two accesses conflict on, as `conflictOf` finds the first. */
|
|
182
215
|
const conflictsOf = (a: Access, b: Access): readonly Key[] => [...new Set([
|
|
183
216
|
...[...a.writes].filter(key => b.writes.has(key) || b.reads.has(key) || b.reduces.has(key)),
|
|
184
217
|
...[...b.writes].filter(key => a.reads.has(key) || a.reduces.has(key)),
|
|
185
|
-
])];
|
|
218
|
+
])].filter(key => !waived(a, b, key));
|
|
186
219
|
|
|
187
220
|
/** A system's leaves in its tiers, with their units and accesses, and its graph with readers of folds after their contributors. */
|
|
188
221
|
const orderedOf = (system: Definition) => {
|
|
@@ -199,7 +232,46 @@ const orderedOf = (system: Definition) => {
|
|
|
199
232
|
});
|
|
200
233
|
}
|
|
201
234
|
});
|
|
202
|
-
return { graph, leaves, accesses, reductions };
|
|
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;
|
|
203
275
|
};
|
|
204
276
|
|
|
205
277
|
/**
|
|
@@ -214,7 +286,7 @@ export type Placement = { readonly leaf: Leaf; readonly because: "after" | "befo
|
|
|
214
286
|
* leaf of its tiers.
|
|
215
287
|
*/
|
|
216
288
|
export const placementsOf = (system: Definition): ReadonlyMap<Leaf, readonly Placement[]> => {
|
|
217
|
-
const { graph, leaves, accesses } = orderedOf(system);
|
|
289
|
+
const { graph, leaves, accesses, keyed } = orderedOf(system);
|
|
218
290
|
const reach = reachability(topologicalOrder(graph), graph.successors);
|
|
219
291
|
const ancestors = graph.units.map(() => [] as number[]);
|
|
220
292
|
graph.members.forEach((members, unit) => {
|
|
@@ -225,7 +297,7 @@ export const placementsOf = (system: Definition): ReadonlyMap<Leaf, readonly Pla
|
|
|
225
297
|
for (const member of members) visit(member);
|
|
226
298
|
});
|
|
227
299
|
/** Whether `systems` names `unit` or a composite holding it. */
|
|
228
|
-
const names = (systems: readonly
|
|
300
|
+
const names = (systems: readonly unknown[] | undefined, unit: number): boolean => {
|
|
229
301
|
const named = graph.resolve(systems);
|
|
230
302
|
return named.includes(unit) || ancestors[unit].some(holder => named.includes(holder));
|
|
231
303
|
};
|
|
@@ -239,7 +311,7 @@ export const placementsOf = (system: Definition): ReadonlyMap<Leaf, readonly Pla
|
|
|
239
311
|
const other = leaves[a];
|
|
240
312
|
const because = constrains(unit, "after", other.unit) ? "after"
|
|
241
313
|
: constrains(other.unit, "before", unit) ? "before"
|
|
242
|
-
: [...accesses[a].reduces].some(key => accesses[b].reads.has(key)) ? "reduction" : "during";
|
|
314
|
+
: keyed.get(`${other.unit} ${unit}`) ?? ([...accesses[a].reduces].some(key => accesses[b].reads.has(key)) ? "reduction" : "during");
|
|
243
315
|
return { leaf: other.leaf, because, conflicts: conflictsOf(accesses[a], accesses[b]) };
|
|
244
316
|
}));
|
|
245
317
|
});
|
|
@@ -271,7 +343,8 @@ const createSchedule = (system: Definition): Schedule => {
|
|
|
271
343
|
if (key !== undefined && !ordered(reach, leaves[a].unit, leaves[b].unit)) {
|
|
272
344
|
const whole = [leaves[a].leaf, leaves[b].leaf].filter(leaf => leaf.declaration.components?.whole === true)
|
|
273
345
|
.map(leaf => ` "${leaf.name}" takes its row whole (components.whole), so it writes every member.`).join("");
|
|
274
|
-
|
|
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}`);
|
|
275
348
|
}
|
|
276
349
|
}
|
|
277
350
|
}
|
|
@@ -305,7 +378,7 @@ export type SyncReason = { readonly from: Leaf; readonly to: Leaf; readonly keys
|
|
|
305
378
|
|
|
306
379
|
/** The components a leaf's commands may add or remove, by spawn, add or remove; `"any"` once it despawns or isn't followed. */
|
|
307
380
|
const structuralOf = (leaf: Leaf): ReadonlySet<Key> | "any" | null => {
|
|
308
|
-
const
|
|
381
|
+
const commands = leaf.commands();
|
|
309
382
|
if (commands === undefined) return null;
|
|
310
383
|
if (commands === null || commands.despawn === true) return "any";
|
|
311
384
|
const keys = new Set([...commands.spawn ?? [], ...commands.add ?? [], ...commands.remove ?? []].map(firstKeyOf));
|
|
@@ -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 => {
|
|
@@ -220,6 +220,8 @@ export const checkLeaf = (leaf: Leaf): void => {
|
|
|
220
220
|
threadOf(declaration, "System", name);
|
|
221
221
|
requireRegistered("System", name, "component", components?.has);
|
|
222
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"));
|
|
223
225
|
for (const lookupName in lookup) {
|
|
224
226
|
const found = lookup[lookupName];
|
|
225
227
|
requireRegistered("System", name, "component", "has" in found ? found.has : undefined);
|
|
@@ -65,9 +65,9 @@ export type Commands = {
|
|
|
65
65
|
): (...values: SpawnValues<C, P>) => Entity;
|
|
66
66
|
despawn(entity: Entity): void;
|
|
67
67
|
/**
|
|
68
|
-
* Despawns, as a `despawn` of each would, every entity that has each component of
|
|
69
|
-
* commands issued before it apply,
|
|
70
|
-
* Derived components may be named. `where` keeps it to the archetypes holding these values of shared components,
|
|
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
71
|
* each one required as `with` requires it, as a system's `where` of values does. Never the resource entity. Only on
|
|
72
72
|
* the world's thread: a joined thread's throws.
|
|
73
73
|
*/
|
|
@@ -94,11 +94,16 @@ export type Commands = {
|
|
|
94
94
|
* - `existing` maps entities to ones already alive in this world, which stand for them: a row whose entity it maps
|
|
95
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
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.
|
|
97
101
|
* - `resources` sets each resource it names, by key, at the sync point, as commands apply, each value checked
|
|
98
102
|
* against its resource's schema; not one only the world writes, nor one living on another thread.
|
|
99
103
|
*/
|
|
100
104
|
spawnTables(tables: readonly Table[], options?: {
|
|
101
105
|
readonly resources?: { readonly [key: symbol]: unknown };
|
|
102
106
|
readonly existing?: ReadonlyMap<Entity, Entity>;
|
|
107
|
+
readonly update?: boolean;
|
|
103
108
|
}): Table.Spawned;
|
|
104
109
|
};
|
|
@@ -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, commands:
|
|
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;
|
|
@@ -49,7 +49,7 @@ describe("System.create", () => {
|
|
|
49
49
|
const body = (): void => {};
|
|
50
50
|
// A body the build doesn't see is given what it uses.
|
|
51
51
|
const move = System.create(giveAccess({ components: { with: [position] } }, { write: [position], resources: { read: [time] } }), body);
|
|
52
|
-
expect(definitionOf(move)).toEqual({ kind: "system", name: "move", declaration: { components: { write: [position] }, resources: { read: [time] } }, body });
|
|
52
|
+
expect(definitionOf(move)).toEqual({ kind: "system", name: "move", declaration: { components: { write: [position] }, resources: { read: [time] } }, body, commands: expect.any(Function) });
|
|
53
53
|
expect(move.name).toBe("move");
|
|
54
54
|
expect(definitionOf({ name: "move" })).toBe(undefined);
|
|
55
55
|
});
|
|
@@ -71,7 +71,7 @@ describe("System.create", () => {
|
|
|
71
71
|
it("labels a system made with neither an id nor a binding by its kind numbered", () => {
|
|
72
72
|
const body = ({ commands }: { readonly commands: System.Commands }): void => { commands.spawn([position])({ [position]: 0 }); };
|
|
73
73
|
const [alone] = [System.create(giveAccess(identify(body, "setup.ts#spawnOnce")))];
|
|
74
|
-
expect(definitionOf(alone)).toEqual({ kind: "system", name: "spawnOnce", declaration: {}, body });
|
|
74
|
+
expect(definitionOf(alone)).toEqual({ kind: "system", name: "spawnOnce", declaration: {}, body, commands: expect.any(Function) });
|
|
75
75
|
const [first, second] = [System.create(() => {}), System.create({ systems: [alone] })].map(system => system.name);
|
|
76
76
|
const number = Number(first?.slice("system #".length));
|
|
77
77
|
expect([first, second]).toEqual([`system #${number}`, `system #${number + 1}`]);
|
|
@@ -177,7 +177,7 @@ describe("System.create of a composite", () => {
|
|
|
177
177
|
const reader = System.create({ components: { with: [position] } }, row => { void row[position]; });
|
|
178
178
|
const definition = definitionOf(whole);
|
|
179
179
|
expect(definition?.kind === "system" ? definition.declaration.components : null).toEqual({ read: [parent], write: [position], whole: true });
|
|
180
|
-
expect(() => System.create([whole, reader])).toThrow(
|
|
180
|
+
expect(() => System.create([whole, reader])).toThrow(`both access "typesSystemTest/position" and one writes it; order them with before or after, or, if they never match one entity, add a component of one to the other's not. "whole" takes its row whole (components.whole), so it writes every member.`);
|
|
181
181
|
});
|
|
182
182
|
it("runs readonly members after its sync point, in no tier, never conflicting and ignoring constraints naming them", () => {
|
|
183
183
|
const write = System.create({ components: { with: [position] } }, row => { row[position] += 1; });
|
|
@@ -18,7 +18,7 @@ import type { OneThread, ThreadedBy, ThreadsOf } from './threads.js';
|
|
|
18
18
|
import type { HeldBy } from './values.js';
|
|
19
19
|
import type { SharedOnly } from './shared-only.js';
|
|
20
20
|
import type { Shared } from '../component/value.js';
|
|
21
|
-
import { accessOf,
|
|
21
|
+
import { accessOf, commandListsOf, compact, componentLists, lookupsLists, resourceUses, upReads, whereReadsOf } from './resolve.js';
|
|
22
22
|
|
|
23
23
|
type Lookups<D> = Property<D, "lookup">;
|
|
24
24
|
|
|
@@ -181,7 +181,7 @@ export function create(first: Declaration | Leaf["body"] | readonly System<unkno
|
|
|
181
181
|
const access = accessOf(`System "${name}"`, first);
|
|
182
182
|
const declaration = resolve(`System "${name}"`, declared, access);
|
|
183
183
|
const schema = inputSchemaOf(name, body, declaration.components === undefined ? 1 : 2, input);
|
|
184
|
-
const system: Leaf & System<unknown> = { kind: "system", name, declaration, input: schema, body, commands:
|
|
184
|
+
const system: Leaf & System<unknown> = { kind: "system", name, declaration, input: schema, body, commands: commandListsOf(`System "${name}"`, access) };
|
|
185
185
|
checkLeaf(system);
|
|
186
186
|
claimSystem(system, id);
|
|
187
187
|
return system;
|
|
@@ -52,10 +52,16 @@ export type Declaration = {
|
|
|
52
52
|
readonly target?: string;
|
|
53
53
|
/** Makes this a render system (see `render`): its body records GPU work over the GPU copy of the components it names. */
|
|
54
54
|
readonly shader?: Shader;
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Hard constraint: runs before each system listed, and, for a resource listed, before every other system of the
|
|
57
|
+
* composite that uses it, but those its constraints already order before it and those listing it in `before` too.
|
|
58
|
+
*/
|
|
59
|
+
readonly before?: readonly (System<unknown> | Resource.Key)[];
|
|
60
|
+
/**
|
|
61
|
+
* Hard constraint: runs after each system listed, and, for a resource listed, after every other system of the
|
|
62
|
+
* composite that uses it, but those its constraints already order after it and those listing it in `after` too.
|
|
63
|
+
*/
|
|
64
|
+
readonly after?: readonly (System<unknown> | Resource.Key)[];
|
|
59
65
|
/** Runs inside the span of the systems listed: after what they follow and before what follows them. */
|
|
60
66
|
readonly during?: readonly System<unknown>[];
|
|
61
67
|
/**
|
|
@@ -56,6 +56,25 @@ describe("System.describe", () => {
|
|
|
56
56
|
const tally = System.create({ after: [count] }, ({ resources }) => { resources[hover] = resources[speed]; });
|
|
57
57
|
expect(System.describe(System.create([clear, count, tally])).tiers.map(tier => tier.sync)).toEqual([[{ from: clear, to: count, keys: [] }], null, null]);
|
|
58
58
|
});
|
|
59
|
+
it("lists no conflict on a key two systems reach through rows that never match one entity", () => {
|
|
60
|
+
const placeCells = System.create({ components: { with: [position, cell] } }, row => { row[cell] = row[position]; });
|
|
61
|
+
const clearCells = System.create({ components: { with: [cell], not: [position] }, after: [placeCells] }, row => { row[cell] = 0; });
|
|
62
|
+
expect(System.describe(System.create([placeCells, clearCells])).tiers[1]?.systems[0]?.after).toEqual([{ system: placeCells, name: "placeCells", because: "after", conflicts: [] }]);
|
|
63
|
+
});
|
|
64
|
+
it("says a system follows another by a resource one lists in before or after", () => {
|
|
65
|
+
const setSpeed = System.create({ before: [speed] }, ({ resources }) => { resources[speed] = 2; });
|
|
66
|
+
const readSpeed = System.create({ components: { with: [position] } }, (row, { resources }) => { row[position] = resources[speed]; });
|
|
67
|
+
const readHover = System.create({ after: [hover] }, ({ resources }) => resources[hover] + resources[speed]);
|
|
68
|
+
const setHover = System.create({ after: [setSpeed] }, ({ resources }) => { resources[hover] = 1; });
|
|
69
|
+
const described = System.describe(System.create([readHover, readSpeed, setHover, setSpeed]));
|
|
70
|
+
expect(described.tiers.map(tier => tier.systems.map(system => [system.name, system.after.map(placement => [placement.name, placement.because])]))).toEqual([
|
|
71
|
+
[["setSpeed", []]],
|
|
72
|
+
[["readSpeed", [["setSpeed", "before"]]], ["setHover", [["setSpeed", "after"]]]],
|
|
73
|
+
[["readHover", [["setHover", "after"]]]],
|
|
74
|
+
]);
|
|
75
|
+
// @ts-expect-error ordering takes resources, not components
|
|
76
|
+
expect(() => System.create({ after: [position] }, () => {})).toThrow(`uses resource "describeTest/position", which isn't a registered resource`);
|
|
77
|
+
});
|
|
59
78
|
it("gives a text form to read", () => {
|
|
60
79
|
expect(System.describe(frame).text).toBe([
|
|
61
80
|
"tier 0 spawnCells commands spawn describeTest/position",
|
|
@@ -16,7 +16,7 @@ const asSystem = (leaf: Leaf): System<unknown, unknown> => leaf;
|
|
|
16
16
|
/** What the build inferred one system uses, and what placed it, as plain data. */
|
|
17
17
|
const describeLeaf = (leaf: Leaf, placements: readonly Placement[]) => {
|
|
18
18
|
const { components, resources, lookup, up, order } = leaf.declaration;
|
|
19
|
-
const
|
|
19
|
+
const commands = leaf.commands();
|
|
20
20
|
return {
|
|
21
21
|
/** The system, the value its constructor returned. */
|
|
22
22
|
system: asSystem(leaf),
|
|
@@ -19,8 +19,9 @@ export type Leaf = {
|
|
|
19
19
|
readonly input: InputSchema | undefined;
|
|
20
20
|
readonly body: (...parameters: never[]) => void;
|
|
21
21
|
/**
|
|
22
|
-
* What its body's commands name, as the build inferred them: null where it couldn't follow
|
|
23
|
-
* none. Apart from its declaration, so a module evaluated again with other commands declares the
|
|
22
|
+
* What its body's commands name, as the build inferred them, read at first need: null where it couldn't follow
|
|
23
|
+
* them, undefined for none. Apart from its declaration, so a module evaluated again with other commands declares the
|
|
24
|
+
* same system.
|
|
24
25
|
*/
|
|
25
|
-
readonly commands
|
|
26
|
+
readonly commands: () => CommandLists | null | undefined;
|
|
26
27
|
};
|