@adobe/ecs 0.2.0 → 0.2.3
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/README.md +2 -2
- package/dist/implementation/compiler/describe.d.ts +1 -1
- package/dist/implementation/compiler/describe.js +3 -3
- package/dist/implementation/compiler/describe.js.map +1 -1
- package/dist/implementation/compiler/signature.js +1 -1
- package/dist/implementation/compiler/transform.js +3 -3
- package/dist/implementation/compiler/transform.js.map +1 -1
- package/dist/implementation/world/commands.js +50 -1
- package/dist/implementation/world/commands.js.map +1 -1
- package/dist/implementation/world/create.js.map +1 -1
- package/dist/implementation/world/entities.d.ts +2 -0
- package/dist/implementation/world/entities.js +6 -0
- package/dist/implementation/world/entities.js.map +1 -1
- package/dist/implementation/world/finalizers.js.map +1 -1
- package/dist/implementation/world/join.js +10 -2
- package/dist/implementation/world/join.js.map +1 -1
- package/dist/implementation/world/register.js +2 -2
- package/dist/implementation/world/rows.d.ts +1 -1
- package/dist/implementation/world/rows.js +5 -0
- package/dist/implementation/world/rows.js.map +1 -1
- package/dist/implementation/world/runner.js +3 -3
- package/dist/implementation/world/runner.js.map +1 -1
- package/dist/implementation/world/sharing.js +1 -1
- package/dist/implementation/world/staging.d.ts +4 -0
- package/dist/implementation/world/staging.js +15 -3
- package/dist/implementation/world/staging.js.map +1 -1
- package/dist/implementation/world/tables-read.d.ts +7 -0
- package/dist/implementation/world/tables-read.js +60 -0
- package/dist/implementation/world/tables-read.js.map +1 -0
- package/dist/implementation/world/tables.d.ts +9 -0
- package/dist/implementation/world/tables.js +355 -0
- package/dist/implementation/world/tables.js.map +1 -0
- 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 +1 -0
- package/dist/implementation/world/thread-systems.js.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +4 -9
- package/dist/index.js.map +1 -1
- package/dist/interface/component/component.d.ts +7 -1
- package/dist/interface/component/component.js +0 -1
- package/dist/interface/component/component.js.map +1 -1
- package/dist/interface/component/declarations.d.ts +4 -0
- package/dist/interface/component/declarations.js +8 -0
- package/dist/interface/component/declarations.js.map +1 -0
- package/dist/interface/component/declared.d.ts +12 -0
- package/dist/interface/component/declared.js +2 -0
- package/dist/interface/component/declared.js.map +1 -0
- package/dist/interface/component/index.d.ts +3 -1
- package/dist/interface/component/index.js +1 -1
- package/dist/interface/component/index.js.map +1 -1
- package/dist/interface/component/value.d.ts +5 -5
- package/dist/interface/component/values.d.ts +2 -2
- package/dist/interface/finalizer/body.d.ts +5 -5
- package/dist/interface/registry/names.d.ts +2 -0
- package/dist/interface/registry/names.js +9 -0
- package/dist/interface/registry/names.js.map +1 -1
- package/dist/interface/registry/schedule.d.ts +3 -3
- package/dist/interface/registry/schedule.js +3 -3
- package/dist/interface/registry/schedule.js.map +1 -1
- package/dist/interface/resource/condition.d.ts +3 -3
- package/dist/interface/resource/declarations.d.ts +4 -0
- package/dist/interface/resource/declarations.js +8 -0
- package/dist/interface/resource/declarations.js.map +1 -0
- package/dist/interface/resource/index.d.ts +2 -1
- package/dist/interface/resource/index.js +1 -1
- package/dist/interface/resource/index.js.map +1 -1
- package/dist/interface/resource/resource.d.ts +4 -1
- package/dist/interface/resource/resource.js +0 -1
- package/dist/interface/resource/resource.js.map +1 -1
- package/dist/interface/resource/value.d.ts +5 -5
- package/dist/interface/resources/cadence.d.ts +1 -1
- package/dist/interface/system/access.d.ts +1 -1
- package/dist/interface/system/body.d.ts +13 -8
- package/dist/interface/system/check.d.ts +1 -1
- package/dist/interface/system/check.js +25 -9
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +20 -4
- package/dist/interface/system/create.d.ts +21 -9
- package/dist/interface/system/create.js +3 -3
- package/dist/interface/system/create.js.map +1 -1
- package/dist/interface/system/declaration.d.ts +9 -2
- package/dist/interface/system/struct.d.ts +2 -2
- package/dist/interface/system/tables.d.ts +20 -0
- package/dist/interface/system/tables.js +2 -0
- package/dist/interface/system/tables.js.map +1 -0
- package/dist/interface/system/values-of.d.ts +4 -4
- package/dist/interface/system/values.d.ts +5 -5
- package/dist/interface/trait/types.d.ts +4 -4
- package/dist/interface/world/create.d.ts +3 -7
- package/dist/interface/world/declarations.d.ts +2 -2
- package/dist/interface/world/index.d.ts +1 -1
- package/dist/interface/world/join.d.ts +2 -10
- package/dist/interface/world/table/index.d.ts +26 -0
- package/dist/interface/world/table/index.js +3 -0
- package/dist/interface/world/table/index.js.map +1 -0
- package/dist/interface/world/table/table.d.ts +11 -0
- package/dist/interface/world/table/table.js +2 -0
- package/dist/interface/world/table/table.js.map +1 -0
- package/dist/interface/world/world.d.ts +10 -10
- package/dist/world.d.ts +27 -0
- package/dist/world.js +27 -0
- package/dist/world.js.map +1 -0
- package/package.json +1 -1
- package/src/exports.test.ts +3 -3
- package/src/guide/00-keys.ts +1 -1
- package/src/guide/01-resources.md +1 -1
- package/src/guide/01-resources.ts +1 -1
- package/src/guide/02-entities.ts +2 -2
- package/src/guide/03-systems.md +2 -2
- package/src/guide/03-systems.out +1 -1
- package/src/guide/03-systems.ts +3 -3
- package/src/guide/04-ticks.md +1 -1
- package/src/guide/04-ticks.ts +3 -3
- package/src/guide/05-scheduling.ts +1 -1
- package/src/guide/06-sync-points.md +9 -2
- package/src/guide/06-sync-points.ts +2 -2
- package/src/guide/07-change-filters.ts +2 -2
- package/src/guide/08-input.ts +1 -1
- package/src/guide/09-rules.ts +1 -1
- package/src/guide/11-traits.ts +3 -3
- package/src/guide/11-traits.type-test.ts +1 -1
- package/src/guide/12-lookups.ts +2 -2
- package/src/guide/13-indexes.ts +4 -4
- package/src/guide/14-kernels.ts +1 -1
- package/src/guide/15-reductions.ts +1 -1
- package/src/guide/16-gpu.ts +1 -1
- package/src/guide/17-hierarchies.ts +1 -1
- package/src/guide/18-identity.out +1 -1
- package/src/guide/19-threads.md +4 -2
- package/src/guide/19-threads.ts +2 -2
- package/src/guide/20-shared.ts +3 -3
- package/src/guide/21-dom.md +2 -2
- package/src/guide/21-dom.ts +1 -1
- package/src/guide/22-canvas.md +1 -1
- package/src/guide/22-canvas.ts +1 -1
- package/src/guide/CLAUDE.md +1 -1
- package/src/guide/guide.test.ts +7 -5
- package/src/implementation/compiler/describe.test-helper.ts +1 -1
- package/src/implementation/compiler/describe.test.ts +1 -1
- package/src/implementation/compiler/describe.ts +3 -3
- package/src/implementation/compiler/hygiene.test.ts +1 -1
- package/src/implementation/compiler/plugin.test.ts +2 -2
- package/src/implementation/compiler/signature.ts +1 -1
- package/src/implementation/compiler/transform.test.ts +3 -3
- package/src/implementation/compiler/transform.ts +3 -3
- package/src/implementation/world/change-blocks.test.ts +1 -1
- package/src/implementation/world/commands.ts +44 -1
- package/src/implementation/world/create.ts +3 -3
- package/src/implementation/world/defaults.test.ts +1 -1
- package/src/implementation/world/entities.ts +6 -0
- package/src/implementation/world/finalizers.test.ts +1 -1
- package/src/implementation/world/finalizers.ts +3 -3
- package/src/implementation/world/index-changed.test.ts +2 -2
- package/src/implementation/world/index-joined.test.ts +5 -5
- package/src/implementation/world/index-order.test.ts +1 -1
- package/src/implementation/world/join.test.ts +4 -4
- package/src/implementation/world/join.ts +10 -2
- package/src/implementation/world/joined-observer.test.ts +1 -1
- package/src/implementation/world/joining-worker.fixture.ts +1 -1
- package/src/implementation/world/joining.fixture.ts +1 -1
- package/src/implementation/world/joining.test-helper.ts +1 -1
- package/src/implementation/world/kernel-spawns.test.ts +1 -1
- package/src/implementation/world/observe-factories.test.ts +2 -2
- package/src/implementation/world/observe-ticks-cases.test.ts +1 -1
- package/src/implementation/world/observe.test-helper.ts +1 -1
- package/src/implementation/world/observer-contained-worker.fixture.ts +1 -1
- package/src/implementation/world/observer-contained.test-helper.ts +1 -1
- package/src/implementation/world/observer-contained.test.ts +2 -2
- package/src/implementation/world/observer-joined.test-helper.ts +4 -4
- package/src/implementation/world/observer-joined.test.ts +23 -15
- package/src/implementation/world/observer.browser.test.ts +2 -2
- package/src/implementation/world/observer.test.ts +8 -8
- package/src/implementation/world/optional-workers.fixture.ts +1 -1
- package/src/implementation/world/optional.test.ts +7 -7
- package/src/implementation/world/pool-size.cases.ts +3 -1
- package/src/implementation/world/reads.test-helper.ts +6 -6
- package/src/implementation/world/reclaim.test.ts +3 -3
- package/src/implementation/world/register.ts +2 -2
- package/src/implementation/world/remote-objects.test-helper.ts +1 -1
- package/src/implementation/world/rows.ts +5 -1
- package/src/implementation/world/run-results.test.ts +10 -10
- package/src/implementation/world/runner.ts +3 -3
- package/src/implementation/world/shared-gpu.browser.test.ts +2 -2
- package/src/implementation/world/shared-kernels.test.ts +2 -2
- package/src/implementation/world/shared-workers.fixture.ts +2 -2
- package/src/implementation/world/shared.test.ts +8 -8
- package/src/implementation/world/sharing.ts +1 -1
- package/src/implementation/world/staging.ts +16 -3
- package/src/implementation/world/tables-read.ts +59 -0
- package/src/implementation/world/tables.test.ts +267 -0
- package/src/implementation/world/tables.ts +330 -0
- package/src/implementation/world/tails.test.ts +1 -1
- package/src/implementation/world/target.test.ts +2 -2
- package/src/implementation/world/thread-observer.ts +1 -1
- package/src/implementation/world/thread-systems.test-helper.ts +3 -3
- package/src/implementation/world/thread-systems.ts +1 -0
- package/src/implementation/world/where.test.ts +4 -4
- package/src/implementation/world/workers.fixture.ts +2 -2
- package/src/index.ts +4 -6
- package/src/interface/component/component.ts +7 -1
- package/src/interface/component/declarations.ts +11 -0
- package/src/interface/component/declared.ts +9 -0
- package/src/interface/component/index.ts +3 -1
- package/src/interface/component/value.ts +5 -5
- package/src/interface/component/values.ts +2 -2
- package/src/interface/finalizer/body.ts +5 -5
- package/src/interface/index.type-test.ts +3 -3
- package/src/interface/indexes/index.type-test.ts +1 -1
- package/src/interface/registry/names.ts +8 -0
- package/src/interface/registry/schedule.ts +4 -4
- package/src/interface/resource/condition.ts +3 -3
- package/src/interface/resource/declarations.ts +11 -0
- package/src/interface/resource/index.ts +2 -1
- package/src/interface/resource/resource.ts +4 -1
- package/src/interface/resource/value.ts +5 -5
- package/src/interface/resources/cadence.ts +1 -1
- package/src/interface/system/access.ts +1 -1
- package/src/interface/system/body.ts +9 -7
- package/src/interface/system/check.ts +22 -8
- package/src/interface/system/commands.ts +18 -4
- package/src/interface/system/create.test.ts +13 -13
- package/src/interface/system/create.ts +15 -10
- package/src/interface/system/declaration.ts +9 -2
- package/src/interface/system/lists.type-test.ts +1 -1
- package/src/interface/system/optional.type-test.ts +1 -1
- package/src/interface/system/shared.type-test.ts +1 -1
- package/src/interface/system/struct.ts +2 -2
- package/src/interface/system/system.type-test.ts +26 -20
- package/src/interface/system/tables.ts +21 -0
- package/src/interface/system/up.type-test.ts +3 -3
- package/src/interface/system/values-of.ts +4 -4
- package/src/interface/system/values.ts +5 -5
- package/src/interface/thread/thread.type-test.ts +1 -1
- package/src/interface/trait/trait.type-test.ts +4 -4
- package/src/interface/trait/types.ts +4 -4
- package/src/interface/world/create.ts +3 -7
- package/src/interface/world/declarations.ts +2 -2
- package/src/interface/world/index.ts +2 -2
- package/src/interface/world/join.ts +2 -10
- package/src/interface/world/loop.type-test.ts +6 -6
- package/src/interface/world/run.type-test.ts +3 -3
- package/src/interface/world/table/index.ts +19 -0
- package/src/interface/world/table/table.ts +12 -0
- package/src/interface/world/world.ts +10 -10
- package/src/observe/entity.ts +2 -2
- package/src/observe/members.ts +3 -3
- package/src/observe/query.ts +1 -1
- package/src/observe/resource.ts +2 -2
- package/src/observe/select.ts +5 -5
- package/src/observe/values.ts +2 -2
- package/src/observe/world-state.ts +3 -3
- package/src/perftest/benchmarks.ts +1 -1
- package/src/perftest/indexes.ts +3 -3
- package/src/perftest/loop.ts +1 -1
- package/src/perftest/observer.ts +4 -4
- package/src/perftest/reads.ts +2 -2
- package/src/perftest/remote.ts +5 -5
- package/src/perftest/strings.ts +1 -1
- package/src/perftest/suite.ts +1 -1
- package/src/world.test.ts +2 -2
- package/src/world.ts +32 -0
|
@@ -5,7 +5,7 @@ import { Component } from "../index.js";
|
|
|
5
5
|
export const instanced = Symbol();
|
|
6
6
|
declare module "@adobe/ecs" {
|
|
7
7
|
interface Traits<N> {
|
|
8
|
-
readonly [instanced]: N extends Component.Key ? ([Component
|
|
8
|
+
readonly [instanced]: N extends Component.Key ? ([Component<N>] extends [readonly number[]] ? true : never) : never;
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
|
package/src/guide/12-lookups.ts
CHANGED
|
@@ -25,7 +25,7 @@ const track = System.create({
|
|
|
25
25
|
|
|
26
26
|
const refreshed: boolean[] = [];
|
|
27
27
|
const watch = System.create({
|
|
28
|
-
|
|
28
|
+
readonly: true,
|
|
29
29
|
components: { with: [target] },
|
|
30
30
|
lookup: { enemy: { with: [health], changed: [health] } },
|
|
31
31
|
}, (row, { lookup }) => {
|
|
@@ -34,7 +34,7 @@ const watch = System.create({
|
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
const aims: number[] = [];
|
|
37
|
-
const readAims = System.create({
|
|
37
|
+
const readAims = System.create({ readonly: true, components: { with: [aim] } }, row => {
|
|
38
38
|
aims.push(row[aim]);
|
|
39
39
|
});
|
|
40
40
|
|
package/src/guide/13-indexes.ts
CHANGED
|
@@ -26,14 +26,14 @@ const count = System.create({ components: { with: [cell, crowded] }, lookup: { a
|
|
|
26
26
|
|
|
27
27
|
const byScore = Index.create({ key: [score], sorted: true, components: { with: [player] } });
|
|
28
28
|
|
|
29
|
-
const leaders = System.create({
|
|
29
|
+
const leaders = System.create({ readonly: true, lookup: { scores: { index: byScore } } }, ({ lookup }) => {
|
|
30
30
|
const winners: string[] = [];
|
|
31
31
|
const high = lookup.scores({ ">=": 90 });
|
|
32
32
|
for (let i = 0; i < high.length; i++) winners.push(high.at(i)[player]);
|
|
33
33
|
return winners;
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
const list = System.create({
|
|
36
|
+
const list = System.create({ readonly: true, components: { with: [player, score] }, order: byScore }, row => {
|
|
37
37
|
console.log(row[player], row[score]);
|
|
38
38
|
});
|
|
39
39
|
|
|
@@ -42,12 +42,12 @@ const find = System.create({ lookup: { byId: { index: byServerId } } }, ({ looku
|
|
|
42
42
|
lookup.byId(input.id)?.[player] ?? null);
|
|
43
43
|
|
|
44
44
|
const players = Index.create({ key: [], components: { with: [player] } });
|
|
45
|
-
const countPlayers = System.create({
|
|
45
|
+
const countPlayers = System.create({ readonly: true, lookup: { players: { index: players, changed: true } } }, ({ lookup }) => {
|
|
46
46
|
console.log("players", lookup.players().length);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
const crowds: number[] = [];
|
|
50
|
-
const readCrowds = System.create({
|
|
50
|
+
const readCrowds = System.create({ readonly: true, components: { with: [crowded] } }, row => {
|
|
51
51
|
crowds.push(row[crowded]);
|
|
52
52
|
});
|
|
53
53
|
|
package/src/guide/14-kernels.ts
CHANGED
|
@@ -29,7 +29,7 @@ const slow = System.create({ kernel: false, components: { with: [velocity] } },
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
const moved: string[] = [];
|
|
32
|
-
const read = System.create({
|
|
32
|
+
const read = System.create({ readonly: true, components: { with: [position, velocity] } }, row => {
|
|
33
33
|
moved.push(`at ${row[position][0]}, moving at ${row[velocity][0]}`);
|
|
34
34
|
});
|
|
35
35
|
|
|
@@ -23,7 +23,7 @@ const weigh = System.create({ components: { with: [mass] } }, (row, { reduce })
|
|
|
23
23
|
reduce[count](1);
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
const report = System.create({
|
|
26
|
+
const report = System.create({ readonly: true }, ({ resources }) =>
|
|
27
27
|
`${resources[count]} weigh ${resources[total]}, at most ${resources[heaviest]}`);
|
|
28
28
|
|
|
29
29
|
const spawn = System.create(({ commands }) => {
|
package/src/guide/16-gpu.ts
CHANGED
|
@@ -32,7 +32,7 @@ const draw = System.render({ shader: { accessors: { dotPosition: position, dotSi
|
|
|
32
32
|
});
|
|
33
33
|
const shaderCode = Gpu.wgsl(draw);
|
|
34
34
|
|
|
35
|
-
const read = System.create({
|
|
35
|
+
const read = System.create({ readonly: true, components: { with: [position] } }, row => {
|
|
36
36
|
console.log("at", row[position]);
|
|
37
37
|
});
|
|
38
38
|
|
|
@@ -21,7 +21,7 @@ const place = System.create({ components: { with: [offset, placed] }, up: { thro
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
const where: string[] = [];
|
|
24
|
-
const read = System.create({
|
|
24
|
+
const read = System.create({ readonly: true, components: { with: [label, placed] } }, row => {
|
|
25
25
|
where.push(`${row[label]} at ${row[placed]}`);
|
|
26
26
|
});
|
|
27
27
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Error: System "explode" (@adobe/ecs@0.2.
|
|
1
|
+
Error: System "explode" (@adobe/ecs@0.2.3/src/guide/18-identity.ts#explode) failed: boom
|
package/src/guide/19-threads.md
CHANGED
|
@@ -11,6 +11,8 @@ A *logical thread* is where objects live and the code touching them runs: the pa
|
|
|
11
11
|
`World.join(port, { threads: ["audio"] })`, claiming the threads it fills. Usually the other end of the channel goes
|
|
12
12
|
to a worker; [here both ends](19-threads.ts#L44-L54) are on one thread.
|
|
13
13
|
|
|
14
|
-
- `share` resolves once the thread joined, with the
|
|
15
|
-
anything else. Those
|
|
14
|
+
- `share` resolves once the thread joined, with the readonly systems it added as `join` resolved, before awaiting
|
|
15
|
+
anything else. Those readonly systems run there after the world's sync points: `listen` hears what `mix` wrote.
|
|
16
|
+
- A joined thread adds only [readonly systems](03-systems.md), which run on it; a system that writes is added on the
|
|
17
|
+
world's thread.
|
|
16
18
|
- A joined world runs systems as the world does: [`audio.run`](19-threads.ts#L60-L61) spawns and mixes too.
|
package/src/guide/19-threads.ts
CHANGED
|
@@ -22,7 +22,7 @@ const mix = System.create({ components: { with: [volume] } }, (row, { resources
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
let heard = 0;
|
|
25
|
-
const listen = System.create({
|
|
25
|
+
const listen = System.create({ readonly: true }, ({ resources }) => {
|
|
26
26
|
heard = resources[levels].size;
|
|
27
27
|
});
|
|
28
28
|
|
|
@@ -31,7 +31,7 @@ const spawn = System.create(({ commands }) => {
|
|
|
31
31
|
commands.spawn([volume])({ [volume]: 0.3 });
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
const countVoices = System.create({
|
|
34
|
+
const countVoices = System.create({ readonly: true }, ({ resources }) => resources[levels].size);
|
|
35
35
|
|
|
36
36
|
/** Waits a turn of the event loop at a time until `done`, for two seconds at most. */
|
|
37
37
|
const until = async (done: () => boolean): Promise<void> => {
|
package/src/guide/20-shared.ts
CHANGED
|
@@ -56,7 +56,7 @@ let drawn = 0;
|
|
|
56
56
|
let left = Infinity;
|
|
57
57
|
let right = -Infinity;
|
|
58
58
|
const draw = System.create({
|
|
59
|
-
|
|
59
|
+
readonly: true,
|
|
60
60
|
components: { with: [position], where: { [cell]: (at, { resources }) => resources[camera]?.sees(at) === true } },
|
|
61
61
|
}, row => {
|
|
62
62
|
drawn += 1;
|
|
@@ -66,7 +66,7 @@ const draw = System.create({
|
|
|
66
66
|
|
|
67
67
|
let refreshed = 0;
|
|
68
68
|
const refresh = System.create({
|
|
69
|
-
|
|
69
|
+
readonly: true,
|
|
70
70
|
components: { with: [position], changed: [position], where: { [cell]: (at, { resources }) => resources[camera]?.sees(at) === true } },
|
|
71
71
|
}, () => {
|
|
72
72
|
refreshed += 1;
|
|
@@ -74,7 +74,7 @@ const refresh = System.create({
|
|
|
74
74
|
|
|
75
75
|
// Only chunk [4, 0], which no unit is in until one crosses into it.
|
|
76
76
|
let east = 0;
|
|
77
|
-
const countEast = System.create({
|
|
77
|
+
const countEast = System.create({ readonly: true, components: { with: [position], where: { [cell]: [4, 0] } } }, () => {
|
|
78
78
|
east += 1;
|
|
79
79
|
});
|
|
80
80
|
|
package/src/guide/21-dom.md
CHANGED
|
@@ -13,9 +13,9 @@ same.
|
|
|
13
13
|
Each tick runs one composite:
|
|
14
14
|
|
|
15
15
|
1. [`move`](21-dom.ts#L38-L44) moves every particle and turns it back from the walls.
|
|
16
|
-
2. [`create`](21-dom.ts#L46-L51) gives each particle without a dot an element, with `commands.add`.
|
|
16
|
+
2. [`create`](21-dom.ts#L46-L51) gives each particle without a dot an element, with `commands.add`. Readonly systems can't
|
|
17
17
|
issue commands, so it ticks, and its condition `is: [stage]` waits for a stage.
|
|
18
|
-
3. [`draw`](21-dom.ts#L52-L60),
|
|
18
|
+
3. [`draw`](21-dom.ts#L52-L60), a readonly system, sets each moved dot's `style.translate`, a transform, which the page
|
|
19
19
|
doesn't lay out again. `changed: [position]` skips the dots that didn't move.
|
|
20
20
|
|
|
21
21
|
[A finalizer](21-dom.ts#L61-L63) removes a particle's element when the particle goes, and every element when the
|
package/src/guide/21-dom.ts
CHANGED
|
@@ -51,7 +51,7 @@ const create = System.create({ components: { with: [position], not: [dot] }, res
|
|
|
51
51
|
});
|
|
52
52
|
// Moves each dot whose particle moved, by a transform, which the page doesn't lay out again.
|
|
53
53
|
const draw = System.create({
|
|
54
|
-
|
|
54
|
+
readonly: true,
|
|
55
55
|
components: { with: [position, dot], optional: [traced], changed: [position] },
|
|
56
56
|
}, (row, { resources }) => {
|
|
57
57
|
const [x, y] = row[position];
|
package/src/guide/22-canvas.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The same particles, drawn in immediate mode: each frame, the canvas is cleared and drawn again whole.
|
|
4
4
|
|
|
5
|
-
[`draw`](22-canvas.ts#L52-L80) is one
|
|
5
|
+
[`draw`](22-canvas.ts#L52-L80) is one readonly system without components, so it runs once per tick, not per particle.
|
|
6
6
|
|
|
7
7
|
- `changed: [clock]` runs it after every tick, and `is: [pen]` waits for a 2D context.
|
|
8
8
|
- It reads every particle through [an index without a key](22-canvas.ts#L50-L51), `key: []`, and the traced one
|
package/src/guide/22-canvas.ts
CHANGED
|
@@ -51,7 +51,7 @@ const particles = Index.create({ key: [], components: { with: [position] } });
|
|
|
51
51
|
const tracedOnes = Index.create({ key: [], components: { with: [position, traced] } });
|
|
52
52
|
// Clears the canvas and draws every particle, after each tick.
|
|
53
53
|
const draw = System.create({
|
|
54
|
-
|
|
54
|
+
readonly: true,
|
|
55
55
|
resources: { is: [pen], changed: [clock] },
|
|
56
56
|
lookup: { particles: { index: particles }, traced: { index: tracedOnes } },
|
|
57
57
|
}, ({ resources, lookup }) => {
|
package/src/guide/CLAUDE.md
CHANGED
|
@@ -45,5 +45,5 @@ it in `NN-name.type-test.ts`, under a `// @ts-expect-error` that the package's b
|
|
|
45
45
|
before they're printed.
|
|
46
46
|
- Keys are `Symbol()` with no description: the build's id names each by its binding.
|
|
47
47
|
- Archetypes are types, so their names are PascalCase: `const Particle = [position, velocity] as const`.
|
|
48
|
-
- Use the public API only, as a consumer imports it, not internal modules. Read a world through
|
|
48
|
+
- Use the public API only, as a consumer imports it, not internal modules. Read a world through readonly systems.
|
|
49
49
|
- Lines change as code does: the test of links fails on any that no longer covers whole code, so move them with it.
|
package/src/guide/guide.test.ts
CHANGED
|
@@ -69,12 +69,12 @@ const listed = [...read("README.md").matchAll(/^(?:- |(\d+)\. )\[([^\]]+)\]\(([^
|
|
|
69
69
|
/** What each chapter teaches, which its prose names as code and its program does; 0 is the unnumbered keys chapter's. */
|
|
70
70
|
const teaches: { readonly [chapter: number]: readonly string[] } = {
|
|
71
71
|
0: ["Symbol()", "declare module \"@adobe/ecs\"", "Components", "Resources", "row[score]", "resources[bonus]", "default"],
|
|
72
|
-
1: ["Resources", "default", "World.create", "
|
|
72
|
+
1: ["Resources", "default", "World.create", "readonly: true", "run", "ReadonlyMap", "Entity"],
|
|
73
73
|
2: ["Components", "type: true", "as const", "commands.spawn", "commands.remove", "with: [...Particle]"],
|
|
74
|
-
3: ["System.create", "with", "not", "
|
|
74
|
+
3: ["System.create", "with", "not", "readonly: true", "optional: [velocity]"],
|
|
75
75
|
4: ["clock", "delta", "cadence", "always", "resources: { changed: [elapsed] }", "wake", "reduce[wake]", "world.add", "world.remove"],
|
|
76
76
|
5: ["after", "during", "System.create([heal, hit])", "during: [update]"],
|
|
77
|
-
6: ["commands.spawn", "commands.add", "commands.despawn", "after: [spawn]"],
|
|
77
|
+
6: ["commands.spawn", "commands.add", "commands.despawn", "after: [spawn]", "readonly: true"],
|
|
78
78
|
7: ["resources: { changed: [level] }", "changed: [score]", "resources: { is: [map], not: [paused] }", "resources[map]"],
|
|
79
79
|
8: ["world.run(hit, { amount: 3 })", "target: \"entity\""],
|
|
80
80
|
9: ["Rule.create", "derived: true"],
|
|
@@ -106,10 +106,12 @@ const mutations: { readonly [chapter: number]: readonly (readonly [string, strin
|
|
|
106
106
|
],
|
|
107
107
|
1: [["World.create({ [gravity]: -1.6 })", "World.create()"], ["default: -9.8 ", ""], ["{ default: -Infinity }", "{}"]],
|
|
108
108
|
2: [["commands.remove(row.entity, enemy);", ""], ["{ [position]: [0, 10, 0] }", "{}"]],
|
|
109
|
-
3: [
|
|
109
|
+
3: [
|
|
110
|
+
["not: [frozen]", "not: []"], ["System.create([count, fail])", "System.create([fail])"],
|
|
111
|
+
],
|
|
110
112
|
4: [["world.remove(advance);", ""]],
|
|
111
113
|
5: [["after: [hit] ", ""], ["during: [update] ", ""]],
|
|
112
|
-
6: [["commands.add(hero, health, 5);", ""], ["commands.despawn(commands.spawn([health])({ [health]: 1 }));", "commands.spawn([health])({ [health]: 1 });"], ["watched.add(watch);", ""]],
|
|
114
|
+
6: [["commands.add(hero, health, 5);", ""], ["commands.despawn(commands.spawn([health])({ [health]: 1 }));", "commands.spawn([health])({ [health]: 1 });"], ["watched.add(watch);", ""], ["const watch = System.create({ readonly: true, ", "const watch = System.create({ "]],
|
|
113
115
|
7: [["resources: { changed: [level] } ", ""], [", changed: [score]", ""], [", not: [paused]", ""], ["is: [map], ", ""]],
|
|
114
116
|
8: [["target: \"entity\",", ""]],
|
|
115
117
|
9: [["with: [mesh], not: [hidden]", "with: [mesh]"], ["Rule.create({ components: { with: [sprite], not: [hidden] }, derive: [drawn] });", ""], ["Rule.create({ components: { with: [drawn] }, derive: [layer] });", ""]],
|
|
@@ -51,7 +51,7 @@ export const spawn = System.create(({ commands }) => {
|
|
|
51
51
|
commands.spawn([position, velocity])({ [position]: [0, 0, 0], [velocity]: [1, 0, 0] });
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
export const observe = System.create({
|
|
54
|
+
export const observe = System.create({ readonly: true, components: { with: [position] } }, row => {
|
|
55
55
|
console.log(row[position]);
|
|
56
56
|
});
|
|
57
57
|
|
|
@@ -109,7 +109,7 @@ describe("a module's systems described", () => {
|
|
|
109
109
|
it("says why systems that run as written have no code: per-run systems, observers, render systems, kernel: false and one declared in a function", () => {
|
|
110
110
|
expect(described("spawn")).toMatchObject({ kind: "per-run", runs: `Its body as written, on the "world" thread` });
|
|
111
111
|
for (const backend of ["js", "wasm", "wgsl"] as const) expect(reasonOf(described("spawn")[backend])).toBe("per-run systems run as written; only row systems compile");
|
|
112
|
-
expect(described("observe")).toMatchObject({ kind: "
|
|
112
|
+
expect(described("observe")).toMatchObject({ kind: "readonly", js: { kind: "none", reason: expect.stringMatching(/^a readonly system runs as written/) } });
|
|
113
113
|
expect(described("draw")).toMatchObject({ kind: "render", runs: expect.stringContaining("while the world has a GPU device"), wgsl: { kind: "none", reason: expect.stringMatching(/^a render system runs as written/) } });
|
|
114
114
|
const unflagged = described("unflagged");
|
|
115
115
|
expect(unflagged).toMatchObject({ kernel: false, js: { kind: "none", reason: "as written, because `kernel: false`" }, wasm: { kind: "none", reason: "as written, because `kernel: false`" }, wgsl: { kind: "eligible" } });
|
|
@@ -30,7 +30,7 @@ export type Backend =
|
|
|
30
30
|
* What runs a system: over every matched row, once per run, as an observer, on one entity its input names (`target`),
|
|
31
31
|
* or as a render system.
|
|
32
32
|
*/
|
|
33
|
-
export type SystemKind = "row" | "per-run" | "
|
|
33
|
+
export type SystemKind = "row" | "per-run" | "readonly" | "target" | "render";
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* A system of a module as the build compiles it: its call's offsets in the code transformed, the binding it's
|
|
@@ -427,7 +427,7 @@ export const describeSystems = async (sites: TransformedSites, format: (code: st
|
|
|
427
427
|
const kernel: boolean | "gpu" | null = literal === true || literal === false ? literal : literal === "gpu" ? "gpu" : null;
|
|
428
428
|
const kind: SystemKind = created === "render" ? "render"
|
|
429
429
|
: declaration === undefined ? "per-run"
|
|
430
|
-
: propertyOf(declaration, "
|
|
430
|
+
: propertyOf(declaration, "readonly") !== null ? "readonly"
|
|
431
431
|
: propertyOf(declaration, "target") !== null ? "target"
|
|
432
432
|
: propertyOf(declaration, "components") !== null ? "row" : "per-run";
|
|
433
433
|
const base = { start: node.start, end: node.end, binding: bindings.get(node) ?? null, kind, kernel };
|
|
@@ -442,7 +442,7 @@ export const describeSystems = async (sites: TransformedSites, format: (code: st
|
|
|
442
442
|
? threadOf(declaration, body, kind === "render" ? positionsOf("render", declaration) : positionsOf("system", declaration)) : "world";
|
|
443
443
|
if (kind === "render") return asWritten("a render system runs as written, recording GPU work with shaders of your own; `Gpu.wgsl` gives them their accessors", "world");
|
|
444
444
|
if (kind === "per-run") return asWritten("per-run systems run as written; only row systems compile", written());
|
|
445
|
-
if (kind === "
|
|
445
|
+
if (kind === "readonly") return asWritten("a readonly system runs as written, reading the settled state after the run's last sync point; only row systems that aren't readonly compile", written());
|
|
446
446
|
if (kind === "target") return asWritten("a system with `target` runs as written, once, on the entity its input names; only systems over every matched row compile", written());
|
|
447
447
|
if (declaration === undefined || body === undefined || body.type === "SpreadElement" || !isFunction(body)) {
|
|
448
448
|
return asWritten(declaration === undefined ? "its declaration isn't written in its call, so the build can't read whether it's a kernel" : "its body isn't a function written in place or bound at module scope, so the build can't read it", "world");
|
|
@@ -34,7 +34,7 @@ const spawn = System.create(({ commands }) => {
|
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
const read: number[][] = [];
|
|
37
|
-
const readAll = System.create({
|
|
37
|
+
const readAll = System.create({ readonly: true, components: { with: [p, c, l, s, e, v0, v1] } }, row => {
|
|
38
38
|
read.push([row[p], row[c], row[l], row[s], row[e], row[v0], row[v1]]);
|
|
39
39
|
});
|
|
40
40
|
|
|
@@ -645,7 +645,7 @@ export { World as OtherWorld };
|
|
|
645
645
|
export const runOther = async (key) => {
|
|
646
646
|
const spawn = System.create(({ commands }) => { commands.spawn([key].slice())({ [key]: 10 }); });
|
|
647
647
|
let total = 0;
|
|
648
|
-
const count = System.create({
|
|
648
|
+
const count = System.create({ readonly: true, components: { with: [key] } }, (row) => { total += row[key]; });
|
|
649
649
|
const world = World.create();
|
|
650
650
|
await world.run(spawn);
|
|
651
651
|
await world.run(spawn);
|
|
@@ -662,7 +662,7 @@ declare$(score, "component", { type: { type: "number" } });
|
|
|
662
662
|
${recorder}
|
|
663
663
|
const spawn = System.create(({ commands }) => { commands.spawn([score].slice())({ [score]: 1 }); });
|
|
664
664
|
let counted = 0;
|
|
665
|
-
const count = System.create({
|
|
665
|
+
const count = System.create({ readonly: true, components: { with: [score] } }, (row) => { counted += row[score]; });
|
|
666
666
|
export const copies = World === OtherWorld ? 1 : 2;
|
|
667
667
|
export const run = async () => {
|
|
668
668
|
const world = World.create();
|
|
@@ -120,7 +120,7 @@ export const kernelOf = (declaration: ObjectExpression): KernelRequest | null =>
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
if (declaration.properties.some(property => property.type === "SpreadElement")) return null;
|
|
123
|
-
if (!declares(declaration, "components") || declares(declaration, "
|
|
123
|
+
if (!declares(declaration, "components") || declares(declaration, "readonly") || declares(declaration, "target")) return null;
|
|
124
124
|
return { target: "cpu", required: false };
|
|
125
125
|
};
|
|
126
126
|
|
|
@@ -335,7 +335,7 @@ describe("kernels by default", () => {
|
|
|
335
335
|
test("runs as written, without failing, a body that leaves the subset or a declaration a kernel can't have", async () => {
|
|
336
336
|
expect(await attached(`const log = []; const kernel = System.create({ components: { with: [mass] } }, row => { log.push(row[mass]); });`)).toBe(false);
|
|
337
337
|
expect(await attached(`const kernel = System.create({ components: { with: [mass] } }, (row, { resources }) => { resources[delta] = row[mass]; });`)).toBe(false);
|
|
338
|
-
expect(await attached(`const kernel = System.create({
|
|
338
|
+
expect(await attached(`const kernel = System.create({ readonly: true, components: { with: [mass] } }, row => { void row[mass]; });`)).toBe(false);
|
|
339
339
|
expect(await attached(`const kernel = System.create({ target: "entity", components: { with: [score] } }, row => { row[score] = 1; });`)).toBe(false);
|
|
340
340
|
expect(await attached(`const make = () => System.create({ components: { with: [score] } }, row => { row[score] = 1; });`)).toBe(false);
|
|
341
341
|
});
|
|
@@ -1418,11 +1418,11 @@ describe("inferred access", () => {
|
|
|
1418
1418
|
});
|
|
1419
1419
|
|
|
1420
1420
|
test("an observer's row it can't follow as unknown without a warning, since the world takes an observer's as read", async () => {
|
|
1421
|
-
const result = await transform(system("
|
|
1421
|
+
const result = await transform(system("readonly: true, components: { with: [position] }", "(row) => { helper(row); }"));
|
|
1422
1422
|
expect(result !== null && "code" in result ? [result.code.includes("components: null"), result.warnings] : null).toEqual([true, []]);
|
|
1423
1423
|
});
|
|
1424
1424
|
|
|
1425
|
-
test("a row it can't follow with a warning unless the declaration says
|
|
1425
|
+
test("a row it can't follow with a warning unless the declaration says readonly: true, as the world reads it", async () => {
|
|
1426
1426
|
for (const observer of ["observer: false, ", "observer: watching, "]) {
|
|
1427
1427
|
const result = await transform(`const watching = true;\n${system(`${observer}components: { with: [position] }`, "(row) => { helper(row); }")}`);
|
|
1428
1428
|
expect(result !== null && "code" in result ? result.warnings.length : null).toBe(1);
|
|
@@ -141,7 +141,7 @@ export const functionOf = (binding: ModuleBinding | undefined): Node | undefined
|
|
|
141
141
|
const empty: ObjectExpression = { type: "ObjectExpression", properties: [], start: 0, end: 0 };
|
|
142
142
|
|
|
143
143
|
/** Whether a declaration has property `name`, written out. */
|
|
144
|
-
/** Whether a declaration gives `name` the literal `true`, as the world takes `
|
|
144
|
+
/** Whether a declaration gives `name` the literal `true`, as the world takes `readonly`. */
|
|
145
145
|
const declaresTrue = (declaration: ObjectExpression, name: string): boolean => declaration.properties.some(property =>
|
|
146
146
|
property.type === "Property" && !property.computed && property.key.type === "Identifier" && property.key.name === name
|
|
147
147
|
&& property.value.type === "Literal" && property.value.value === true);
|
|
@@ -313,9 +313,9 @@ export const transformKernels = async (
|
|
|
313
313
|
const used = kernel?.used ?? inOrder(usedBy(declared, body, module.bindings, positions), declared);
|
|
314
314
|
requireRequired(used, declared, body);
|
|
315
315
|
const where = at(body);
|
|
316
|
-
//
|
|
316
|
+
// A readonly system's row, and a lookup's rows, it can't follow are taken as read whole, which is what their
|
|
317
317
|
// declarations already say; only a row taken as written whole costs what the author should know of.
|
|
318
|
-
if (used.query === null && !declaresTrue(declared, "
|
|
318
|
+
if (used.query === null && !declaresTrue(declared, "readonly")) warnings.push(`${where}: the build can't follow how this body uses its row, so it takes every component in with, added and changed as written: index the row by key directly, or destructure it by key, to let it tell what the body reads and writes.`);
|
|
319
319
|
accessing = true;
|
|
320
320
|
const access = accessText(used, where, whereReads.has(node) ? whereReads.get(node) ?? null : undefined);
|
|
321
321
|
if (kernel !== undefined) {
|
|
@@ -89,7 +89,7 @@ const setGroup = System.create({ target: "entity", components: { with: [group] }
|
|
|
89
89
|
// Readers.
|
|
90
90
|
let found: number[] = [];
|
|
91
91
|
const onMain = System.create({ components: { with: [at], changed: [value] } }, row => { found.push(row[at]); });
|
|
92
|
-
const observed = System.create({
|
|
92
|
+
const observed = System.create({ readonly: true, components: { with: [at], changed: [value] } }, row => { found.push(row[at]); });
|
|
93
93
|
const ofTwo = System.create({ components: { with: [at], changed: [group, value] } }, row => { found.push(row[at]); });
|
|
94
94
|
const inKernel = System.create({ kernel: true, components: { with: [seen], changed: [value] } }, row => { row[seen] += 1; });
|
|
95
95
|
const inUp = System.create({ components: { with: [at], changed: [value] }, up: { through: parent } }, row => { found.push(row[at]); });
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
2
|
import type { Commands } from '../../interface/system/commands.js';
|
|
3
|
-
import { addCommand, despawnCommand, pushCommand, pushComponent, removeCommand, type CommandBuffer } from './buffer.js';
|
|
3
|
+
import { addCommand, createBuffer, despawnCommand, pushCommand, pushComponent, removeCommand, spawnCommand, type CommandBuffer } from './buffer.js';
|
|
4
|
+
import { reserve } from './entities.js';
|
|
4
5
|
import { listShapeOf, presetSpawnerOf, spawnerOf, type ListShape } from './lists.js';
|
|
5
6
|
import { commandIdOf, idOf, storedShared, type Key, type State } from './state.js';
|
|
7
|
+
import { spawnTables } from './tables.js';
|
|
8
|
+
import { declarationOf, isWorldWritten, kindOf, labelOf, threadOfDeclaration, whyUnregistered } from '../../interface/registry/index.js';
|
|
9
|
+
import { matchesSchema } from '../../interface/registry/schemas.js';
|
|
6
10
|
|
|
7
11
|
const collect = (state: State, buffer: CommandBuffer, key: Key, value: unknown): void => {
|
|
8
12
|
if (value !== undefined) pushComponent(buffer, idOf(state, key), value);
|
|
@@ -44,6 +48,38 @@ const addShared = (state: State, buffer: CommandBuffer, entity: number, shape: L
|
|
|
44
48
|
for (let k = 0; k < keys.length; k++) pushComponent(buffer, ids[k], values[k]);
|
|
45
49
|
};
|
|
46
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Throws for a key of `resources` that isn't a resource, is one only the world writes or one living on another thread,
|
|
53
|
+
* or is given a value its schema doesn't take.
|
|
54
|
+
*/
|
|
55
|
+
const checkResources = (resources: object): void => {
|
|
56
|
+
for (const key of Object.getOwnPropertySymbols(resources)) {
|
|
57
|
+
const fail = (message: string): never => { throw new Error(`commands.spawnTables: "${labelOf(key)}" ${message}.`); };
|
|
58
|
+
if (kindOf(key) !== "resource") fail(`isn't a registered resource: ${whyUnregistered(key)}`);
|
|
59
|
+
if (isWorldWritten(key)) fail("is written only by the world");
|
|
60
|
+
const declaration = declarationOf(key);
|
|
61
|
+
if (declaration === undefined) continue;
|
|
62
|
+
const thread = threadOfDeclaration(declaration);
|
|
63
|
+
if (thread !== null && thread !== "world") fail(`lives on thread "${thread}", not the world's`);
|
|
64
|
+
if (declaration.type !== undefined && !matchesSchema(declaration.type, Reflect.get(resources, key))) fail("is given a value its type doesn't take");
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Queues `resources`' values for the sync point as the world's own change to its resource entity, which it reserves
|
|
70
|
+
* when the world has none yet: a buffer of their own, since the sync point skips systems' commands to that entity.
|
|
71
|
+
*/
|
|
72
|
+
const queueResources = (state: State, resources: object): void => {
|
|
73
|
+
if (Object.getOwnPropertySymbols(resources).length === 0) return;
|
|
74
|
+
const buffer = createBuffer();
|
|
75
|
+
if (state.resourceEntity < 0) {
|
|
76
|
+
state.resourceEntity = reserve(state.entities);
|
|
77
|
+
pushValues(state, buffer, spawnCommand, state.resourceEntity, resources);
|
|
78
|
+
}
|
|
79
|
+
else pushValues(state, buffer, addCommand, state.resourceEntity, resources);
|
|
80
|
+
state.queued.push({ buffer, guarded: false });
|
|
81
|
+
};
|
|
82
|
+
|
|
47
83
|
/** Values of a list add given none, which only an untyped caller does. */
|
|
48
84
|
const empty = {};
|
|
49
85
|
|
|
@@ -83,6 +119,13 @@ export const createCommands = (state: State, target: { readonly buffer: CommandB
|
|
|
83
119
|
pushComponent(buffer, ids[k], value === undefined ? starts[k] : value);
|
|
84
120
|
}
|
|
85
121
|
},
|
|
122
|
+
spawnTables: (tables, resources) => {
|
|
123
|
+
if (resources === undefined) return spawnTables(state, tables);
|
|
124
|
+
checkResources(resources);
|
|
125
|
+
const spawned = spawnTables(state, tables);
|
|
126
|
+
queueResources(state, resources);
|
|
127
|
+
return spawned;
|
|
128
|
+
},
|
|
86
129
|
remove: (entity: number, components: symbol | readonly symbol[]) => {
|
|
87
130
|
const { buffer } = target;
|
|
88
131
|
if (typeof components === "symbol") {
|
|
@@ -46,7 +46,7 @@ import { createState, defaultOf, idOf, reside, startTicking, type Key } from './
|
|
|
46
46
|
import { indexOf } from './entities.js';
|
|
47
47
|
import { touchedHeld } from './up.js';
|
|
48
48
|
import { isHeld } from './transfers.js';
|
|
49
|
-
import type {
|
|
49
|
+
import type { ReadonlyType as ReadonlyResourceType } from '../../interface/resource/value.js';
|
|
50
50
|
import { labelOf, whyUnregistered } from '../../interface/registry/index.js';
|
|
51
51
|
|
|
52
52
|
type Queue = <T>(task: () => T | Promise<T>) => Promise<T>;
|
|
@@ -425,12 +425,12 @@ export const createWorld = (resources: InitialResources = {}, options: WorldOpti
|
|
|
425
425
|
const deviceId = idOf(state, gpuDevice);
|
|
426
426
|
const deviceDefault = defaultOf(gpuDevice);
|
|
427
427
|
/** The `gpuDevice` resource's value, read each run, so without allocating. */
|
|
428
|
-
const readDevice = ():
|
|
428
|
+
const readDevice = (): ReadonlyResourceType<typeof gpuDevice> => {
|
|
429
429
|
const index = state.resourceEntity < 0 ? -1 : indexOf(state.resourceEntity);
|
|
430
430
|
const storage = index < 0 ? undefined : state.archetypes[state.entities.archetype[index]]?.storageById[deviceId];
|
|
431
431
|
const value = storage !== undefined ? readCell(storage, state.entities.row[index]) : state.initial.has(gpuDevice) ? state.initial.get(gpuDevice) : deviceDefault;
|
|
432
432
|
// The resource's value matches its registered schema, `GPUDevice | null`.
|
|
433
|
-
return value as
|
|
433
|
+
return value as ReadonlyResourceType<typeof gpuDevice>;
|
|
434
434
|
};
|
|
435
435
|
|
|
436
436
|
/**
|
|
@@ -56,7 +56,7 @@ describe("a derived component declared without a default", () => {
|
|
|
56
56
|
commands.spawn([tagged])({});
|
|
57
57
|
});
|
|
58
58
|
const seen: unknown[] = [];
|
|
59
|
-
const read = System.create({
|
|
59
|
+
const read = System.create({ readonly: true, components: { with: [index] } }, row => { seen.push(row[index]); });
|
|
60
60
|
await using first = createWorld();
|
|
61
61
|
await using second = createWorld();
|
|
62
62
|
for (const world of [first, second]) {
|
|
@@ -70,6 +70,12 @@ export const createEntities = (memory: Memory): Entities => {
|
|
|
70
70
|
/** Reserves an entity. Safe to call from any thread between sync points. */
|
|
71
71
|
export const reserve = ({ cursor, free, generation }: Reservations): Entity => entityAt(cursor, free, generation, Atomics.add(cursor, taken, 1));
|
|
72
72
|
|
|
73
|
+
/** Reserves an entity for each of `into`'s elements, with one atomic add. Safe to call from any thread between sync points. */
|
|
74
|
+
export const reserveRun = ({ cursor, free, generation }: Reservations, into: Float64Array): void => {
|
|
75
|
+
const first = Atomics.add(cursor, taken, into.length);
|
|
76
|
+
for (let i = 0; i < into.length; i++) into[i] = entityAt(cursor, free, generation, first + i);
|
|
77
|
+
};
|
|
78
|
+
|
|
73
79
|
/**
|
|
74
80
|
* Reservations a thread takes in blocks of positions, so threads spawning together don't contend for the cursor.
|
|
75
81
|
* Positions a block leaves unused are reserved entities that never spawn, which the sync point releases.
|
|
@@ -137,7 +137,7 @@ describe("finalizers", () => {
|
|
|
137
137
|
const reads = System.create({}, ({ resources }) => { void resources[level]; });
|
|
138
138
|
// Read after every run, as a read through a run of its own would apply the commands left for it.
|
|
139
139
|
let seen: string | null = null;
|
|
140
|
-
const watch = System.create({
|
|
140
|
+
const watch = System.create({ readonly: true, lookup: { labels: { with: [label] } } }, ({ lookup }) => {
|
|
141
141
|
seen = lookup.labels(other)?.[label] ?? null;
|
|
142
142
|
});
|
|
143
143
|
const world = createWorld({ [workers]: 0 });
|
|
@@ -12,7 +12,7 @@ import { asEntity, indexOf, locate } from './entities.js';
|
|
|
12
12
|
import { createContext, rowBinder, type Context } from './rows.js';
|
|
13
13
|
import { idOf, type Key, type State } from './state.js';
|
|
14
14
|
import type { Queued } from './synchronize.js';
|
|
15
|
-
import type { Essential,
|
|
15
|
+
import type { Essential, ReadonlyType } from '../../interface/component/value.js';
|
|
16
16
|
import type { Leaf } from '../../interface/finalizer/created.js';
|
|
17
17
|
import { newestKeyOf, threadOf } from '../../interface/registry/index.js';
|
|
18
18
|
import type { Layout } from '../../interface/registry/layout.js';
|
|
@@ -369,8 +369,8 @@ const changesOf = (state: State, runner: FinalizerRunner, queued: readonly Queue
|
|
|
369
369
|
return {
|
|
370
370
|
groups: [...groups.values()],
|
|
371
371
|
// Stored and commanded values match each component's registered schema, so they have its type.
|
|
372
|
-
previous: <K extends Essential>(entity: Entity, key: K) => (bornHere(entity) ? null : live(entity, idOf(state, key))) as
|
|
373
|
-
next: <K extends Essential>(entity: Entity, key: K) => next(entity, idOf(state, key)) as
|
|
372
|
+
previous: <K extends Essential>(entity: Entity, key: K) => (bornHere(entity) ? null : live(entity, idOf(state, key))) as ReadonlyType<K> | null,
|
|
373
|
+
next: <K extends Essential>(entity: Entity, key: K) => next(entity, idOf(state, key)) as ReadonlyType<K> | null,
|
|
374
374
|
};
|
|
375
375
|
};
|
|
376
376
|
|
|
@@ -109,7 +109,7 @@ describe("an index lookup's changed: true", () => {
|
|
|
109
109
|
await using world = inline();
|
|
110
110
|
spawned = [];
|
|
111
111
|
const orders: string[] = [];
|
|
112
|
-
const ordering = System.create({
|
|
112
|
+
const ordering = System.create({ readonly: true, lookup: { inOrder: { index: inOrder, changed: true } } }, ({ lookup }) => {
|
|
113
113
|
const all = lookup.inOrder();
|
|
114
114
|
const groups: number[] = [];
|
|
115
115
|
for (let i = 0; i < all.length; i++) groups.push(all.at(i).entity === spawned[0] ? -1 : 0);
|
|
@@ -205,7 +205,7 @@ describe("an index lookup's changed: true", () => {
|
|
|
205
205
|
await using world = inline();
|
|
206
206
|
spawned = [];
|
|
207
207
|
const totals: number[] = [];
|
|
208
|
-
const readTotals = System.create({
|
|
208
|
+
const readTotals = System.create({ readonly: true, components: { with: [total] } }, row => {
|
|
209
209
|
totals.push(row[total]);
|
|
210
210
|
});
|
|
211
211
|
const totalsOf = async (): Promise<number[]> => {
|
|
@@ -87,7 +87,7 @@ const stirBands = System.create({ components: { with: [serial, band] } }, (row,
|
|
|
87
87
|
/** An observer system of the joined thread's labels: the serials of each reader's band, in entity order, by reader. */
|
|
88
88
|
let observedBands: string[] = [];
|
|
89
89
|
const observeBands = System.create({
|
|
90
|
-
|
|
90
|
+
readonly: true,
|
|
91
91
|
components: { with: [target, bandReader, label] },
|
|
92
92
|
lookup: { inBand: { index: bands } },
|
|
93
93
|
}, (row, { lookup }) => {
|
|
@@ -219,14 +219,14 @@ const inOrder = System.create({ components: { with: [rank, label] }, order: rank
|
|
|
219
219
|
const countWhenMoved = System.create({ components: { with: [cell, label] }, lookup: { near: { index: grid, changed: true } } }, (row, { lookup }) => {
|
|
220
220
|
row[label] = { text: String(lookup.near(row[cell]).length) };
|
|
221
221
|
});
|
|
222
|
-
const readWhenMoved = System.create({
|
|
222
|
+
const readWhenMoved = System.create({ readonly: true, components: { with: [label] }, lookup: { near: { index: grid, changed: true } } }, row => {
|
|
223
223
|
void row[label];
|
|
224
224
|
});
|
|
225
|
-
const ownWhenMoved = System.create({
|
|
225
|
+
const ownWhenMoved = System.create({ readonly: true, lookup: { near: { index: grid, changed: true } } }, () => {});
|
|
226
226
|
|
|
227
227
|
/** The entities `readLabels` reads, and their labels. */
|
|
228
228
|
const reading: { entities: readonly Entity[]; labels: readonly string[] } = { entities: [], labels: [] };
|
|
229
|
-
const readLabels = System.create({
|
|
229
|
+
const readLabels = System.create({ readonly: true, lookup: { labeled: { with: [label] } } }, ({ lookup }) => {
|
|
230
230
|
reading.labels = reading.entities.map(entity => lookup.labeled(entity)?.[label].text ?? "missing");
|
|
231
231
|
});
|
|
232
232
|
|
|
@@ -279,7 +279,7 @@ const heldSerials = Index.create({ key: [serial], unique: true, components: { wi
|
|
|
279
279
|
let observedHeld = "";
|
|
280
280
|
/** An observer system reading every held row through the keyless index, and serial 3's through the unique one. */
|
|
281
281
|
const observeHeld = System.create({
|
|
282
|
-
|
|
282
|
+
readonly: true,
|
|
283
283
|
lookup: { all: { index: held }, bySerial: { index: heldSerials } },
|
|
284
284
|
}, ({ lookup }) => {
|
|
285
285
|
const all = lookup.all();
|
|
@@ -72,7 +72,7 @@ const expected = async (world: CreatedWorld, only?: (weighed: number) => boolean
|
|
|
72
72
|
};
|
|
73
73
|
|
|
74
74
|
const markerValues: number[] = [];
|
|
75
|
-
const readMarkers = System.create({
|
|
75
|
+
const readMarkers = System.create({ readonly: true, components: { with: [marker] } }, row => { markerValues.push(row[marker]); });
|
|
76
76
|
|
|
77
77
|
/** Markers' values in the order they were spawned: their archetype's rows', in storage order. */
|
|
78
78
|
const markers = async (world: CreatedWorld): Promise<number[]> => {
|