@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
|
@@ -30,11 +30,11 @@ const spawn = System.create(({ commands }) => {
|
|
|
30
30
|
const raise = System.create(({ resources }) => {
|
|
31
31
|
resources[level] += 1;
|
|
32
32
|
});
|
|
33
|
-
const count = System.create({
|
|
33
|
+
const count = System.create({ readonly: true, components: { with: [amount] } }, () => {
|
|
34
34
|
counted++;
|
|
35
35
|
});
|
|
36
36
|
let counted = 0;
|
|
37
|
-
const readLevel = System.create({
|
|
37
|
+
const readLevel = System.create({ readonly: true }, ({ resources }) => resources[level]);
|
|
38
38
|
const broken = System.create(() => {
|
|
39
39
|
throw new Error("broken");
|
|
40
40
|
});
|
|
@@ -57,7 +57,7 @@ describe("a run", () => {
|
|
|
57
57
|
expect(await world.run(System.create({ components: { with: [amount] } }, row => row[amount]))).toBeUndefined();
|
|
58
58
|
expect(await world.run(System.create([readLevel, raise]))).toBeUndefined();
|
|
59
59
|
expect(await world.run(raise)).toBeUndefined();
|
|
60
|
-
const whenChanged = System.create({
|
|
60
|
+
const whenChanged = System.create({ readonly: true, resources: { changed: [level] } }, ({ resources }) => resources[level]);
|
|
61
61
|
expect(await world.run(whenChanged)).toBe(2);
|
|
62
62
|
// Unchanged since it last ran: skipped.
|
|
63
63
|
expect(await world.run(whenChanged)).toBeUndefined();
|
|
@@ -94,7 +94,7 @@ describe("a run", () => {
|
|
|
94
94
|
it("rejects when its own observer system fails, and reports one added out of band", async () => {
|
|
95
95
|
using reported = catchOutOfBand();
|
|
96
96
|
await using world = inline();
|
|
97
|
-
const failingObserver = System.create({
|
|
97
|
+
const failingObserver = System.create({ readonly: true }, () => {
|
|
98
98
|
throw new Error("observer failed");
|
|
99
99
|
});
|
|
100
100
|
await expect(world.run(failingObserver)).rejects.toThrow(/observer failed/);
|
|
@@ -144,7 +144,7 @@ const spawnTwins = System.create(({ commands }) => {
|
|
|
144
144
|
commands.spawn([serial])({ [serial]: 1 });
|
|
145
145
|
commands.spawn([serial])({ [serial]: 1 });
|
|
146
146
|
});
|
|
147
|
-
const findTwin = System.create({
|
|
147
|
+
const findTwin = System.create({ readonly: true, lookup: { twins: { index: bySerial } } }, ({ lookup }) => lookup.twins(1)?.entity ?? null);
|
|
148
148
|
const setGate = System.create({}, ({ resources }, input: { readonly open: boolean }) => {
|
|
149
149
|
resources[gate] = input.open;
|
|
150
150
|
});
|
|
@@ -207,10 +207,10 @@ describe("a run's observer pass", () => {
|
|
|
207
207
|
it("fails the run for its own observer system, and reports one added out of band, in one run", async () => {
|
|
208
208
|
using reported = catchOutOfBand();
|
|
209
209
|
await using world = inline();
|
|
210
|
-
const ownFails = System.create({
|
|
210
|
+
const ownFails = System.create({ readonly: true }, () => {
|
|
211
211
|
throw new Error("own failed");
|
|
212
212
|
});
|
|
213
|
-
const addedFails = System.create({
|
|
213
|
+
const addedFails = System.create({ readonly: true }, () => {
|
|
214
214
|
throw new Error("added failed");
|
|
215
215
|
});
|
|
216
216
|
world.add(addedFails);
|
|
@@ -255,7 +255,7 @@ describe("out-of-band reports", () => {
|
|
|
255
255
|
it("aren't made again for a system skipped between its failures", async () => {
|
|
256
256
|
using reported = catchOutOfBand();
|
|
257
257
|
await using world = inline();
|
|
258
|
-
const failsWhenChanged = System.create({
|
|
258
|
+
const failsWhenChanged = System.create({ readonly: true, resources: { changed: [level] } }, () => {
|
|
259
259
|
throw new Error("changed failed");
|
|
260
260
|
});
|
|
261
261
|
world.add(failsWhenChanged);
|
|
@@ -294,7 +294,7 @@ describe("out-of-band reports", () => {
|
|
|
294
294
|
using reported = catchOutOfBand();
|
|
295
295
|
const world = inline();
|
|
296
296
|
let failing = false;
|
|
297
|
-
const failsAdded = System.create({
|
|
297
|
+
const failsAdded = System.create({ readonly: true }, () => {
|
|
298
298
|
if (failing) throw new Error("added failed");
|
|
299
299
|
});
|
|
300
300
|
world.add(failsAdded);
|
|
@@ -331,7 +331,7 @@ describe("out-of-band reports", () => {
|
|
|
331
331
|
it("aren't made again for a row system whose change filter passed no row between its failures", async () => {
|
|
332
332
|
using reported = catchOutOfBand();
|
|
333
333
|
await using world = inline();
|
|
334
|
-
const failsOnChange = System.create({
|
|
334
|
+
const failsOnChange = System.create({ readonly: true, components: { with: [amount], changed: [amount] } }, () => {
|
|
335
335
|
throw new Error("row failed");
|
|
336
336
|
});
|
|
337
337
|
const bump = System.create({ components: { with: [amount] } }, row => {
|
|
@@ -260,7 +260,7 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
260
260
|
: createOrderedRender(indexLookups.ordered, ordered, accessors.map(accessor => idOf(state, accessor.component)), declaration.shader?.group ?? 0, `${leafNumber}:`);
|
|
261
261
|
const touches = [...new Set([...query?.read ?? [], ...query?.write ?? []])].map(key => idOf(state, key));
|
|
262
262
|
const writes = (query?.write ?? []).map(key => idOf(state, key));
|
|
263
|
-
const looks = [...new Set([...Object.values(declaration.lookup ?? {}).flatMap(lookup => lookup.read ?? []), ...declaration.up?.read ?? []])].map(key => idOf(state, key));
|
|
263
|
+
const looks = [...new Set([...Object.values(declaration.lookup ?? {}).flatMap(lookup => lookup.read ?? []), ...declaration.up?.read ?? [], ...declaration.tables?.components ?? []])].map(key => idOf(state, key));
|
|
264
264
|
/** Record keys by archetype. */
|
|
265
265
|
const keys = new Map<Archetype, string>();
|
|
266
266
|
const keyOf = (archetype: Archetype): string => {
|
|
@@ -339,7 +339,7 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
339
339
|
let runInput: unknown;
|
|
340
340
|
const context = createContext(state, declaration, {
|
|
341
341
|
targets: kernel === null ? targets : [],
|
|
342
|
-
commands: leaf.kind === "render" || declaration.kernel === "gpu" || declaration.
|
|
342
|
+
commands: leaf.kind === "render" || declaration.kernel === "gpu" || declaration.readonly === true ? undefined : commands,
|
|
343
343
|
indexes: indexLookups.byName,
|
|
344
344
|
since: lookupSince,
|
|
345
345
|
});
|
|
@@ -1343,7 +1343,7 @@ export const createRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
|
1343
1343
|
return taken;
|
|
1344
1344
|
},
|
|
1345
1345
|
didRun: () => ran && (query === undefined || bodyRanOnRow()),
|
|
1346
|
-
restart: declaration.
|
|
1346
|
+
restart: declaration.readonly !== true || !holdsNothing(holdings) ? null : since => {
|
|
1347
1347
|
lastRun = since;
|
|
1348
1348
|
holdings.removed = false;
|
|
1349
1349
|
holdings.released = false;
|
|
@@ -59,7 +59,7 @@ const fractions = System.create({ kernel: "gpu", components: { with: [scale, val
|
|
|
59
59
|
});
|
|
60
60
|
const fractionsOf = async (world: ReturnType<typeof createWorld>): Promise<readonly (readonly number[])[]> => {
|
|
61
61
|
const read: [number, number[]][] = [];
|
|
62
|
-
await world.run(System.create({
|
|
62
|
+
await world.run(System.create({ readonly: true, components: { with: [value, copied, product, sum, compound] } }, row => { read.push([row[value], [row[copied], row[product], row[sum], row[compound], row[value]]]); }));
|
|
63
63
|
return read.sort((a, b) => a[0] - b[0]).map(([, values]) => values);
|
|
64
64
|
};
|
|
65
65
|
|
|
@@ -71,7 +71,7 @@ const device = async (): Promise<GPUDevice> => {
|
|
|
71
71
|
|
|
72
72
|
const results = async (world: ReturnType<typeof createWorld>): Promise<readonly string[]> => {
|
|
73
73
|
const read: [number, string][] = [];
|
|
74
|
-
await world.run(System.create({
|
|
74
|
+
await world.run(System.create({ readonly: true, components: { with: [value, out, steps] } }, row => { read.push([row[value], `${row[out]} ${row[steps]}`]); }));
|
|
75
75
|
return read.sort((a, b) => a[0] - b[0]).map(([, text]) => text);
|
|
76
76
|
};
|
|
77
77
|
|
|
@@ -62,7 +62,7 @@ const wideOnGpu = System.create({ kernel: "gpu", components: { with: [wide, out]
|
|
|
62
62
|
/** Every row's results, by its value. */
|
|
63
63
|
const results = async (world: ReturnType<typeof createWorld>): Promise<readonly string[]> => {
|
|
64
64
|
const read: [number, string][] = [];
|
|
65
|
-
await world.run(System.create({
|
|
65
|
+
await world.run(System.create({ readonly: true, components: { with: [value, out, steps] } }, row => { read.push([row[value], `${row[out]} ${row[steps]}`]); }));
|
|
66
66
|
return read.sort((a, b) => a[0] - b[0]).map(([, text]) => text);
|
|
67
67
|
};
|
|
68
68
|
|
|
@@ -98,7 +98,7 @@ describe("a kernel reading a shared component", () => {
|
|
|
98
98
|
await world.run(spawn);
|
|
99
99
|
expect((await runReported(world, moving)).failures).toEqual([]);
|
|
100
100
|
const scales: number[] = [];
|
|
101
|
-
await world.run(System.create({
|
|
101
|
+
await world.run(System.create({ readonly: true, components: { with: [scale] } }, row => { scales.push(row[scale]); }));
|
|
102
102
|
expect(new Set(scales)).toEqual(new Set([2]));
|
|
103
103
|
// A kernel built without knowing a component is shared, naming it in a command, fails where it registers.
|
|
104
104
|
const description = kernelOf(compute);
|
|
@@ -39,7 +39,7 @@ const written = System.create({ kernel: false, components: { with: [scale, cell,
|
|
|
39
39
|
row[steps] = x * 1000 + y;
|
|
40
40
|
});
|
|
41
41
|
const read: [number, string][] = [];
|
|
42
|
-
const results = System.create({
|
|
42
|
+
const results = System.create({ readonly: true, components: { with: [value, out, steps] } }, row => { read.push([row[value], `${row[out]} ${row[steps]}`]); });
|
|
43
43
|
|
|
44
44
|
const outcome = async (system: typeof compute, wasm: boolean): Promise<readonly string[]> => {
|
|
45
45
|
await using world = createWorld({ [workers]: 2 }, { minimumChunk: 2, wasm });
|
|
@@ -66,7 +66,7 @@ export const whereOnWorkers = async (wasm: boolean): Promise<{ readonly written:
|
|
|
66
66
|
await world.run(negative);
|
|
67
67
|
let written = 0;
|
|
68
68
|
let left = 0;
|
|
69
|
-
await world.run(System.create({
|
|
69
|
+
await world.run(System.create({ readonly: true, components: { with: [value, out] } }, row => {
|
|
70
70
|
if (row[out] === 0 && row[value] !== 0) left++;
|
|
71
71
|
else written++;
|
|
72
72
|
}));
|
|
@@ -39,7 +39,7 @@ const inline = () => createWorld({ [workers]: 0 });
|
|
|
39
39
|
|
|
40
40
|
// Systems a joined thread runs, which it names by the ids the build gives them at the top level.
|
|
41
41
|
const heard: string[] = [];
|
|
42
|
-
export const listen = System.create({
|
|
42
|
+
export const listen = System.create({ readonly: true, components: { with: [team, health] } }, row => { heard.push(`${row[team]} ${row[health]}`); });
|
|
43
43
|
export const spawnBanners = System.create(({ commands }) => {
|
|
44
44
|
commands.spawn([team, health, banner], { [team]: "red" })({ [health]: 1 });
|
|
45
45
|
commands.spawn([team, health, banner], { [team]: "blue" })({ [health]: 2 });
|
|
@@ -60,11 +60,11 @@ export const spawnThere = System.create({ components: { with: [banner] } }, (row
|
|
|
60
60
|
commands.spawn([health, marker], base)({ [marker]: 1 });
|
|
61
61
|
commands.spawn([health], base)({});
|
|
62
62
|
});
|
|
63
|
-
export const readBanners = System.create({
|
|
63
|
+
export const readBanners = System.create({ readonly: true, components: { with: [banner] } }, row => { banners.push(row[banner]?.text ?? "none"); });
|
|
64
64
|
|
|
65
65
|
/** Every entity's team and health, and whether it's armed, sorted by health. */
|
|
66
66
|
const rowsOf = async (world: ReturnType<typeof inline>): Promise<readonly string[]> => {
|
|
67
|
-
const read = System.create({
|
|
67
|
+
const read = System.create({ readonly: true, components: { with: [team, health], optional: [armed] } }, (row) => {
|
|
68
68
|
found.push(`${row[team]} ${row[health]}${row[armed] === true ? " armed" : ""}`);
|
|
69
69
|
});
|
|
70
70
|
const found: string[] = [];
|
|
@@ -116,7 +116,7 @@ describe("a shared component", () => {
|
|
|
116
116
|
// Three cells' worth of [1, 2] and [2, 1], three weights and one style.
|
|
117
117
|
expect((await world.tables()).archetypes - before).toBe(2 + 3 + 1);
|
|
118
118
|
const read: string[] = [];
|
|
119
|
-
await world.run(System.create({
|
|
119
|
+
await world.run(System.create({ readonly: true, components: { with: [health], optional: [cell, weight, style] } }, row => {
|
|
120
120
|
if (row[health] === 0) return;
|
|
121
121
|
read.push(`${row[health]}: ${JSON.stringify(row[cell] ?? row[style] ?? null)} ${String(row[weight])}`);
|
|
122
122
|
}));
|
|
@@ -216,8 +216,8 @@ describe("a shared component", () => {
|
|
|
216
216
|
commands.spawn([team, health], { [team]: "red" })({ [health]: 2 });
|
|
217
217
|
}));
|
|
218
218
|
const changes: string[] = [];
|
|
219
|
-
const watch = System.create({
|
|
220
|
-
const adds = System.create({
|
|
219
|
+
const watch = System.create({ readonly: true, components: { with: [health], changed: [team] } }, row => { changes.push(`changed ${row[health]}`); });
|
|
220
|
+
const adds = System.create({ readonly: true, components: { with: [health], added: [team] } }, row => { changes.push(`added ${row[health]}`); });
|
|
221
221
|
world.add(watch);
|
|
222
222
|
world.add(adds);
|
|
223
223
|
await world.run(System.create(() => {}));
|
|
@@ -294,7 +294,7 @@ describe("a shared component", () => {
|
|
|
294
294
|
}));
|
|
295
295
|
await world.run(System.create({ components: { with: [weight, health] } }, row => { row[health] *= row[weight]; }));
|
|
296
296
|
const read: number[] = [];
|
|
297
|
-
await world.run(System.create({
|
|
297
|
+
await world.run(System.create({ readonly: true, components: { with: [health] } }, row => { read.push(row[health]); }));
|
|
298
298
|
expect(read.sort()).toEqual([2, 3]);
|
|
299
299
|
});
|
|
300
300
|
|
|
@@ -338,7 +338,7 @@ describe("a shared component", () => {
|
|
|
338
338
|
await world.run(writeBanners);
|
|
339
339
|
await world.run(spawnThere);
|
|
340
340
|
const found: string[] = [];
|
|
341
|
-
await world.run(System.create({
|
|
341
|
+
await world.run(System.create({ readonly: true, components: { with: [health], optional: [marker, team] } }, row => {
|
|
342
342
|
if (row[team] === undefined) found.push(`${row[health]} ${row[marker] ?? "none"}`);
|
|
343
343
|
}));
|
|
344
344
|
expect(found.sort()).toEqual(["7 1", "7 1", "7 none", "7 none"]);
|
|
@@ -613,7 +613,7 @@ export const createSharing = (host: SharingHost) => {
|
|
|
613
613
|
const { components, resources } = declared;
|
|
614
614
|
// Unnamed, so other systems' labels stay as they are; its own is the thread's.
|
|
615
615
|
const declaration: Resolved = {
|
|
616
|
-
|
|
616
|
+
readonly: true,
|
|
617
617
|
// Its `where` runs on its thread; here, an empty one says it has one, which every archetype passes.
|
|
618
618
|
...components === null ? {} : { components: { ...listed([["read", components.read], ["has", components.has], ["optional", components.optional], ["not", components.not], ["added", components.added], ["changed", components.changed]]), ...components.where ? { where: {} } : {} } },
|
|
619
619
|
...resources === null ? {} : { resources: listed([["read", resources.read], ["changed", resources.changed], ["is", resources.is], ["not", resources.not]]) },
|
|
@@ -69,14 +69,15 @@ export const createBatch = (layouts: readonly Layout[], ids: readonly number[],
|
|
|
69
69
|
return { ids: sorted, shared, archetype: null, columns: sorted.map(id => columnOf(layouts[id], id, initialCapacity)), entities: new Float64Array(initialCapacity), count: 0, capacity: initialCapacity, revive: null, generation: 0 };
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
/** Grows a batch to hold at least `needed` rows: twice its capacity, or `needed` where that's more. */
|
|
73
|
+
const grow = (batch: SpawnBatch, needed = batch.count + 1): void => {
|
|
73
74
|
const { revive } = batch;
|
|
74
75
|
if (revive !== null) {
|
|
75
76
|
batch.revive = null;
|
|
76
77
|
revive();
|
|
77
|
-
if (
|
|
78
|
+
if (needed <= batch.capacity) return;
|
|
78
79
|
}
|
|
79
|
-
const capacity = batch.capacity * 2;
|
|
80
|
+
const capacity = Math.max(batch.capacity * 2, needed);
|
|
80
81
|
for (const column of batch.columns) {
|
|
81
82
|
const views = viewsOf(new ArrayBuffer(capacity * column.stride), 0, capacity * column.stride);
|
|
82
83
|
views.u8.set(column.views.u8.subarray(0, batch.count * column.stride));
|
|
@@ -146,6 +147,18 @@ export const openRow = (batch: SpawnBatch): number => {
|
|
|
146
147
|
return batch.count;
|
|
147
148
|
};
|
|
148
149
|
|
|
150
|
+
/** The first of the `n` rows the batch's next spawns write, growing it first to hold them. */
|
|
151
|
+
export const openRows = (batch: SpawnBatch, n: number): number => {
|
|
152
|
+
if (batch.count + n > batch.capacity) grow(batch, batch.count + n);
|
|
153
|
+
return batch.count;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** Claims the open rows for `entities`, once all their values are written. */
|
|
157
|
+
export const claimRows = (batch: SpawnBatch, entities: Float64Array): void => {
|
|
158
|
+
batch.entities.set(entities, batch.count);
|
|
159
|
+
batch.count += entities.length;
|
|
160
|
+
};
|
|
161
|
+
|
|
149
162
|
/** Claims the open row for `entity`, once all its values are written. */
|
|
150
163
|
export const claim = (batch: SpawnBatch, entity: number): void => {
|
|
151
164
|
batch.entities[batch.count] = entity;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { labelOf } from '../../interface/registry/names.js';
|
|
3
|
+
import type { Tables, TablesDeclaration } from '../../interface/system/tables.js';
|
|
4
|
+
import type { Table } from '../../interface/world/table/table.js';
|
|
5
|
+
import type { Storage } from './archetype.js';
|
|
6
|
+
import { readCell } from './cells.js';
|
|
7
|
+
import { resourceCell } from './resources.js';
|
|
8
|
+
import { idOf, type Key, type State } from './state.js';
|
|
9
|
+
|
|
10
|
+
// Reading tables, apart from spawning them: rows' contexts import it, and what spawning imports would make a cycle.
|
|
11
|
+
|
|
12
|
+
/** What an archetype's storage of a component gives a table: a copy of its rows `0` to `count`. */
|
|
13
|
+
const columnOf = (key: Key, storage: Storage, count: number): Table.Column => {
|
|
14
|
+
const { layout } = storage;
|
|
15
|
+
switch (layout.kind) {
|
|
16
|
+
case "linear": return { kind: "bytes", bytes: storage.views.u8.slice(0, count * layout.stride), stride: layout.stride };
|
|
17
|
+
case "constant": return { kind: "value", value: layout.value };
|
|
18
|
+
case "object":
|
|
19
|
+
if (storage.remote !== null) throw new Error(`Tables can't read "${labelOf(key)}": a joined thread fills its objects.`);
|
|
20
|
+
return { kind: "values", values: storage.values.slice(0, count) };
|
|
21
|
+
case "string": {
|
|
22
|
+
const values: unknown[] = new Array(count);
|
|
23
|
+
for (let row = 0; row < count; row++) values[row] = readCell(storage, row);
|
|
24
|
+
return { kind: "values", values };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A readonly system's `tables`: of each archetype matching `with` and `not` and holding any of `components`, its
|
|
31
|
+
* entities and a copy of the column of each of them it holds; and the value of each resource of `resources`.
|
|
32
|
+
*/
|
|
33
|
+
export const readTables = (state: State, declaration: TablesDeclaration): Tables => {
|
|
34
|
+
const asked = declaration.components.map(key => ({ key, id: idOf(state, key) }));
|
|
35
|
+
const required = (declaration.with ?? []).map(key => idOf(state, key));
|
|
36
|
+
const excluded = (declaration.not ?? []).map(key => idOf(state, key));
|
|
37
|
+
const archetypes: Table[] = [];
|
|
38
|
+
for (const archetype of state.archetypes) {
|
|
39
|
+
const { count } = archetype;
|
|
40
|
+
if (count === 0 || archetype.reclaimed) continue;
|
|
41
|
+
const { storageById } = archetype;
|
|
42
|
+
if (required.some(id => storageById[id] === undefined) || excluded.some(id => storageById[id] !== undefined)) continue;
|
|
43
|
+
const columns: { [key: symbol]: Table.Column } = {};
|
|
44
|
+
let held = false;
|
|
45
|
+
for (const { key, id } of asked) {
|
|
46
|
+
const storage = storageById[id];
|
|
47
|
+
if (storage === undefined) continue;
|
|
48
|
+
columns[key] = columnOf(key, storage, count);
|
|
49
|
+
held = true;
|
|
50
|
+
}
|
|
51
|
+
if (held) archetypes.push({ count, entities: archetype.entities.slice(0, count), columns });
|
|
52
|
+
}
|
|
53
|
+
const resources: { [key: symbol]: unknown } = {};
|
|
54
|
+
for (const key of declaration.resources ?? []) {
|
|
55
|
+
const cell = resourceCell(state, key);
|
|
56
|
+
resources[key] = readCell(cell.storage, cell.row);
|
|
57
|
+
}
|
|
58
|
+
return { archetypes, resources };
|
|
59
|
+
};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { clock, Component, Entity, Resource, System, workers } from "../../index.js";
|
|
4
|
+
import type { Vec3 } from "../../interface/math/index.js";
|
|
5
|
+
import type { Tables } from "../../interface/system/tables.js";
|
|
6
|
+
import type { Table } from "../../interface/world/table/table.js";
|
|
7
|
+
import { createWorld } from "./create.js";
|
|
8
|
+
|
|
9
|
+
const position = Symbol("tablesTest/position");
|
|
10
|
+
const label = Symbol("tablesTest/label");
|
|
11
|
+
const tags = Symbol("tablesTest/tags");
|
|
12
|
+
const team = Symbol("tablesTest/team");
|
|
13
|
+
const pinned = Symbol("tablesTest/pinned");
|
|
14
|
+
const parent = Symbol("tablesTest/parent");
|
|
15
|
+
const target = Symbol("tablesTest/target");
|
|
16
|
+
const link = Symbol("tablesTest/link");
|
|
17
|
+
const friends = Symbol("tablesTest/friends");
|
|
18
|
+
const hidden = Symbol("tablesTest/hidden");
|
|
19
|
+
const buddy = Symbol("tablesTest/buddy");
|
|
20
|
+
const score = Symbol("tablesTest/score");
|
|
21
|
+
const title = Symbol("tablesTest/title");
|
|
22
|
+
declare module "@adobe/ecs" {
|
|
23
|
+
interface Components {
|
|
24
|
+
[position]: { type: Vec3 };
|
|
25
|
+
[label]: { type: string };
|
|
26
|
+
[tags]: { type: readonly string[] };
|
|
27
|
+
[team]: { type: "red" | "blue"; shared: true };
|
|
28
|
+
[pinned]: { type: true };
|
|
29
|
+
[parent]: { type: Entity; reference: "owner" };
|
|
30
|
+
[target]: { type: Entity; reference: "weak" };
|
|
31
|
+
[link]: { type: { readonly to: Entity; readonly weight: number } };
|
|
32
|
+
[friends]: { type: readonly Entity[] };
|
|
33
|
+
[hidden]: { type: true };
|
|
34
|
+
[buddy]: { type: Entity };
|
|
35
|
+
}
|
|
36
|
+
interface Resources {
|
|
37
|
+
[score]: { type: number; default: 0 };
|
|
38
|
+
[title]: { type: string; default: "" };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const inline = () => createWorld({ [workers]: 0 });
|
|
43
|
+
|
|
44
|
+
/** A readonly system's tables of these components. */
|
|
45
|
+
const tablesOf = (components: readonly Component.Key[], resources: readonly Resource.Key[] = [], not: readonly Component.Key[] = []) =>
|
|
46
|
+
System.create({ readonly: true, tables: { components, resources, not } }, ({ tables }): Tables => tables);
|
|
47
|
+
|
|
48
|
+
const spawnedBy: { spawned: Table.Spawned } = { spawned: { entities: [], dropped: 0 } };
|
|
49
|
+
const given: { tables: readonly Table[]; resources?: { readonly [key: symbol]: unknown } } = { tables: [] };
|
|
50
|
+
const spawnGiven = System.create(({ commands }) => {
|
|
51
|
+
spawnedBy.spawned = given.resources === undefined ? commands.spawnTables(given.tables) : commands.spawnTables(given.tables, given.resources);
|
|
52
|
+
});
|
|
53
|
+
const spawnTables = async (world: ReturnType<typeof inline>, tables: readonly Table[]): Promise<Table.Spawned> => {
|
|
54
|
+
given.tables = tables;
|
|
55
|
+
await world.run(spawnGiven);
|
|
56
|
+
return spawnedBy.spawned;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const values = System.create({ readonly: true, components: { with: [label], optional: [position, parent, target, link, friends, tags, buddy] } }, row => {
|
|
60
|
+
found.push({ entity: row.entity, label: row[label], position: row[position], parent: row[parent], target: row[target], link: row[link], friends: row[friends], tags: row[tags], buddy: row[buddy] });
|
|
61
|
+
});
|
|
62
|
+
type Found = { entity: Entity; label: string; position?: Vec3; parent?: Entity; target?: Entity; link?: { readonly to: Entity; readonly weight: number }; friends?: readonly Entity[]; tags?: readonly string[]; buddy?: Entity };
|
|
63
|
+
const found: Found[] = [];
|
|
64
|
+
const readAll = async (world: ReturnType<typeof inline>): Promise<Found[]> => {
|
|
65
|
+
found.length = 0;
|
|
66
|
+
await world.run(values);
|
|
67
|
+
return [...found].sort((a, b) => a.label.localeCompare(b.label));
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
describe("tables", () => {
|
|
71
|
+
it("give each archetype holding a component asked for, its entities, and a copy of each column in the world's layout", async () => {
|
|
72
|
+
await using world = inline();
|
|
73
|
+
await world.run(System.create(({ commands }) => {
|
|
74
|
+
commands.spawn([position, label])({ [position]: [1, 2, 3], [label]: "a" });
|
|
75
|
+
commands.spawn([position, label, tags])({ [position]: [4, 5, 6], [label]: "b", [tags]: ["x", "y"] });
|
|
76
|
+
commands.spawn([team, label], { [team]: "red" })({ [label]: "c" });
|
|
77
|
+
commands.spawn([pinned])();
|
|
78
|
+
}));
|
|
79
|
+
const { archetypes } = await world.run(tablesOf([position, label, tags, team, pinned]));
|
|
80
|
+
expect(archetypes.map(table => table.count)).toEqual([1, 1, 1, 1]);
|
|
81
|
+
const plain = archetypes.find(table => table.columns[tags] === undefined && table.columns[position] !== undefined);
|
|
82
|
+
const bytes = plain?.columns[position];
|
|
83
|
+
expect(bytes?.kind === "bytes" && [...new Float32Array(bytes.bytes.buffer, bytes.bytes.byteOffset, 3)]).toEqual([1, 2, 3]);
|
|
84
|
+
expect(bytes?.kind === "bytes" && bytes.stride).toBe(12);
|
|
85
|
+
const withTags = archetypes.find(table => table.columns[tags] !== undefined);
|
|
86
|
+
expect(withTags?.columns[tags]).toEqual({ kind: "values", values: [["x", "y"]] });
|
|
87
|
+
expect(withTags?.columns[label]).toEqual({ kind: "values", values: ["b"] });
|
|
88
|
+
const shared = archetypes.find(table => table.columns[team] !== undefined);
|
|
89
|
+
expect(shared?.columns[team]).toEqual({ kind: "value", value: "red" });
|
|
90
|
+
expect(archetypes.find(table => table.columns[pinned] !== undefined)?.columns[pinned]).toEqual({ kind: "value", value: true });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("leave out archetypes not matching with and not, and give the values of resources asked for", async () => {
|
|
94
|
+
await using world = createWorld({ [workers]: 0, [score]: 7, [title]: "won" });
|
|
95
|
+
await world.run(System.create(({ commands }) => {
|
|
96
|
+
commands.spawn([label])({ [label]: "shown" });
|
|
97
|
+
commands.spawn([label, hidden])({ [label]: "hidden" });
|
|
98
|
+
}));
|
|
99
|
+
const { archetypes, resources } = await world.run(tablesOf([label], [score, title], [hidden]));
|
|
100
|
+
expect(archetypes.map(table => table.columns[label])).toEqual([{ kind: "values", values: ["shown"] }]);
|
|
101
|
+
expect(resources[score]).toBe(7);
|
|
102
|
+
expect(resources[title]).toBe("won");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("spawn back as they were read, shared values and strings included", async () => {
|
|
106
|
+
await using from = inline();
|
|
107
|
+
await from.run(System.create(({ commands }) => {
|
|
108
|
+
commands.spawn([position, label, tags])({ [position]: [1, 2, 3], [label]: "a", [tags]: ["t"] });
|
|
109
|
+
commands.spawn([team, label], { [team]: "blue" })({ [label]: "b" });
|
|
110
|
+
}));
|
|
111
|
+
const { archetypes } = await from.run(tablesOf([position, label, tags, team]));
|
|
112
|
+
await using into = inline();
|
|
113
|
+
const { entities, dropped } = await spawnTables(into, archetypes);
|
|
114
|
+
expect([entities.map(list => list.length), dropped]).toEqual([[1, 1], 0]);
|
|
115
|
+
const again = await into.run(tablesOf([position, label, tags, team]));
|
|
116
|
+
expect(again.archetypes.map(table => ({ ...table, entities: [] }))).toEqual(archetypes.map(table => ({ ...table, entities: [] })));
|
|
117
|
+
expect(await readAll(into)).toMatchObject([{ label: "a", position: [1, 2, 3], tags: ["t"] }, { label: "b" }]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("rewrite entities in references, struct fields and arrays to those spawned for them, self references and cycles included", async () => {
|
|
121
|
+
await using from = inline();
|
|
122
|
+
await from.run(System.create(({ commands }) => {
|
|
123
|
+
const root = commands.spawn([label])({ [label]: "root" });
|
|
124
|
+
const child = commands.spawn([label, parent])({ [label]: "child", [parent]: root });
|
|
125
|
+
commands.spawn([label, target, link, friends])({ [label]: "other", [target]: child, [link]: { to: root, weight: 2 }, [friends]: [root, child] });
|
|
126
|
+
const a = commands.spawn([label, link])({ [label]: "self", [link]: { to: Entity.none, weight: 1 } });
|
|
127
|
+
commands.add(a, link, { to: a, weight: 1 });
|
|
128
|
+
}));
|
|
129
|
+
const before = await readAll(from);
|
|
130
|
+
const { archetypes } = await from.run(tablesOf([label, parent, target, link, friends]));
|
|
131
|
+
await using into = inline();
|
|
132
|
+
// Entities already in the world, so the new ones differ from the old.
|
|
133
|
+
await into.run(System.create(({ commands }) => { for (let i = 0; i < 5; i++) commands.spawn([pinned])(); }));
|
|
134
|
+
await spawnTables(into, archetypes);
|
|
135
|
+
const after = await readAll(into);
|
|
136
|
+
const old = new Map(before.map(entry => [entry.entity, entry.label]));
|
|
137
|
+
const now = new Map(after.map(entry => [entry.label, entry.entity]));
|
|
138
|
+
const translated = (entity: Entity | undefined): Entity | undefined => entity === undefined ? undefined : now.get(old.get(entity) ?? "");
|
|
139
|
+
expect(after.map(entry => entry.label)).toEqual(before.map(entry => entry.label));
|
|
140
|
+
for (const entry of before) {
|
|
141
|
+
const moved = after.find(other => other.label === entry.label);
|
|
142
|
+
expect(moved?.entity).not.toBe(entry.entity);
|
|
143
|
+
expect(moved?.parent).toBe(translated(entry.parent));
|
|
144
|
+
expect(moved?.target).toBe(translated(entry.target));
|
|
145
|
+
expect(moved?.link?.to).toBe(translated(entry.link?.to));
|
|
146
|
+
expect(moved?.friends).toEqual(entry.friends?.map(translated));
|
|
147
|
+
}
|
|
148
|
+
expect(after.find(entry => entry.label === "self")?.link?.to).toBe(now.get("self"));
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("leave out a weak reference to an entity outside the tables, its holder spawned without it", async () => {
|
|
152
|
+
await using world = inline();
|
|
153
|
+
await world.run(System.create(({ commands }) => {
|
|
154
|
+
const outside = commands.spawn([pinned])();
|
|
155
|
+
const inside = commands.spawn([label])({ [label]: "inside" });
|
|
156
|
+
commands.spawn([label, target])({ [label]: "away", [target]: outside });
|
|
157
|
+
commands.spawn([label, target])({ [label]: "near", [target]: inside });
|
|
158
|
+
}));
|
|
159
|
+
const { archetypes } = await world.run(tablesOf([label, target]));
|
|
160
|
+
await using other = inline();
|
|
161
|
+
expect((await spawnTables(other, archetypes)).dropped).toBe(0);
|
|
162
|
+
const read = await readAll(other);
|
|
163
|
+
expect(read.map(entry => [entry.label, entry.target === undefined ? "none" : read.find(found => found.entity === entry.target)?.label])).toEqual([["away", "none"], ["inside", "none"], ["near", "inside"]]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("spawn no row owned, directly or through others, by an entity outside the tables, saying how many", async () => {
|
|
167
|
+
await using world = inline();
|
|
168
|
+
await world.run(System.create(({ commands }) => {
|
|
169
|
+
const outside = commands.spawn([pinned])();
|
|
170
|
+
const top = commands.spawn([label, parent])({ [label]: "top", [parent]: outside });
|
|
171
|
+
const middle = commands.spawn([label, parent])({ [label]: "middle", [parent]: top });
|
|
172
|
+
commands.spawn([label, parent])({ [label]: "bottom", [parent]: middle });
|
|
173
|
+
const kept = commands.spawn([label])({ [label]: "kept" });
|
|
174
|
+
commands.spawn([label, parent, target])({ [label]: "kept child", [parent]: kept, [target]: middle });
|
|
175
|
+
}));
|
|
176
|
+
const { archetypes } = await world.run(tablesOf([label, parent, target]));
|
|
177
|
+
await using other = inline();
|
|
178
|
+
const { entities, dropped } = await spawnTables(other, archetypes);
|
|
179
|
+
expect(dropped).toBe(3);
|
|
180
|
+
expect(entities.flatMap(list => [...list]).filter(entity => entity === Entity.none).length).toBe(3);
|
|
181
|
+
const read = await readAll(other);
|
|
182
|
+
// The kept child's weak target was dropped, so it's spawned without it.
|
|
183
|
+
expect(read.map(entry => [entry.label, entry.parent === undefined ? null : read.find(found => found.entity === entry.parent)?.label, entry.target ?? null])).toEqual([["kept", null, null], ["kept child", "kept", null]]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("make any other entity value naming an entity outside the tables none: a whole component, a struct's field and an array's item", async () => {
|
|
187
|
+
await using world = inline();
|
|
188
|
+
await world.run(System.create(({ commands }) => {
|
|
189
|
+
const outside = commands.spawn([pinned])();
|
|
190
|
+
const inside = commands.spawn([label])({ [label]: "inside" });
|
|
191
|
+
commands.spawn([label, buddy, link, friends])({ [label]: "pointing", [buddy]: outside, [link]: { to: outside, weight: 1 }, [friends]: [outside, inside] });
|
|
192
|
+
}));
|
|
193
|
+
const { archetypes } = await world.run(tablesOf([label, buddy, link, friends]));
|
|
194
|
+
await using other = inline();
|
|
195
|
+
await spawnTables(other, archetypes);
|
|
196
|
+
const read = await readAll(other);
|
|
197
|
+
const inside = read.find(entry => entry.label === "inside")?.entity;
|
|
198
|
+
expect(read.find(entry => entry.label === "pointing")).toMatchObject({ buddy: Entity.none, link: { to: Entity.none, weight: 1 }, friends: [Entity.none, inside] });
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("set the resources they're given at the sync point, beside the entities", async () => {
|
|
202
|
+
await using world = createWorld({ [workers]: 0, [score]: 7 });
|
|
203
|
+
given.tables = [];
|
|
204
|
+
given.resources = { [score]: 12, [title]: "loaded" };
|
|
205
|
+
try {
|
|
206
|
+
await world.run(spawnGiven);
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
given.resources = undefined;
|
|
210
|
+
}
|
|
211
|
+
const { resources } = await world.run(tablesOf([label], [score, title]));
|
|
212
|
+
expect(resources[score]).toBe(12);
|
|
213
|
+
expect(resources[title]).toBe("loaded");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("refuse a resource only the world writes or a value its type doesn't take, spawning nothing", async () => {
|
|
217
|
+
await using world = inline();
|
|
218
|
+
const table: Table = { count: 1, entities: new Float64Array([1]), columns: { [label]: { kind: "values", values: ["kept out"] } } };
|
|
219
|
+
given.tables = [table];
|
|
220
|
+
try {
|
|
221
|
+
given.resources = { [clock]: { now: 0, delta: 0, tick: 0 } };
|
|
222
|
+
await expect(world.run(spawnGiven)).rejects.toThrow(/clock" is written only by the world/);
|
|
223
|
+
given.resources = { [score]: "twelve" };
|
|
224
|
+
await expect(world.run(spawnGiven)).rejects.toThrow(/score" is given a value its type doesn't take/);
|
|
225
|
+
given.resources = { [position]: [0, 0, 0] };
|
|
226
|
+
await expect(world.run(spawnGiven)).rejects.toThrow(/position" isn't a registered resource/);
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
given.resources = undefined;
|
|
230
|
+
}
|
|
231
|
+
expect(await readAll(world)).toEqual([]);
|
|
232
|
+
expect((await world.run(tablesOf([label], [score]))).resources[score]).toBe(0);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("fail a column that doesn't fit its component's layout or its table's count", async () => {
|
|
236
|
+
await using world = inline();
|
|
237
|
+
const table = (columns: Table["columns"], count = 1): Table => ({ count, entities: new Float64Array(count).fill(1), columns });
|
|
238
|
+
given.tables = [table({ [position]: { kind: "bytes", bytes: new Uint8Array(8), stride: 8 } })];
|
|
239
|
+
await expect(world.run(spawnGiven)).rejects.toThrow(/stride of 12 bytes in this world, not 8/);
|
|
240
|
+
given.tables = [table({ [position]: { kind: "bytes", bytes: new Uint8Array(24), stride: 12 } })];
|
|
241
|
+
await expect(world.run(spawnGiven)).rejects.toThrow(/gives 24 bytes for 1 rows of 12/);
|
|
242
|
+
given.tables = [table({ [label]: { kind: "values", values: ["a", "b"] } })];
|
|
243
|
+
await expect(world.run(spawnGiven)).rejects.toThrow(/gives 2 values for 1 rows/);
|
|
244
|
+
given.tables = [table({ [team]: { kind: "values", values: ["red"] } })];
|
|
245
|
+
await expect(world.run(spawnGiven)).rejects.toThrow(/shared component, so its column is one value/);
|
|
246
|
+
// Two rows of one entity can't both be what a reference to it names.
|
|
247
|
+
given.tables = [table({ [friends]: { kind: "values", values: [[]] } }, 1), table({ [friends]: { kind: "values", values: [[]] } }, 1)];
|
|
248
|
+
await expect(world.run(spawnGiven)).rejects.toThrow(/entity 1 is in two rows/);
|
|
249
|
+
expect(await readAll(world)).toEqual([]);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("are read only by a readonly system without components", () => {
|
|
253
|
+
// @ts-expect-error: tables are read only by a readonly system, which the declaration's type says too.
|
|
254
|
+
expect(() => System.create({ tables: { components: [label] } }, () => {})).toThrow(/reads tables, which only a readonly system reads/);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("declarations", () => {
|
|
259
|
+
it("give every component and resource registered, with its schema and traits", () => {
|
|
260
|
+
const declared = Component.declarations().get(parent);
|
|
261
|
+
expect(declared?.type).toEqual({ type: "integer", format: "entity" });
|
|
262
|
+
expect(declared?.reference).toBe("owner");
|
|
263
|
+
expect(Resource.declarations().get(score)?.default).toBe(0);
|
|
264
|
+
const components: readonly symbol[] = [...Component.declarations().keys()];
|
|
265
|
+
expect(components.includes(score)).toBe(false);
|
|
266
|
+
});
|
|
267
|
+
});
|