@adobe/ecs 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/implementation/compiler/body-access.d.ts +18 -1
- package/dist/implementation/compiler/body-access.js +104 -15
- package/dist/implementation/compiler/body-access.js.map +1 -1
- package/dist/implementation/compiler/estree.d.ts +5 -2
- package/dist/implementation/compiler/estree.js +8 -5
- package/dist/implementation/compiler/estree.js.map +1 -1
- package/dist/implementation/compiler/inferred.js +4 -2
- package/dist/implementation/compiler/inferred.js.map +1 -1
- package/dist/implementation/compiler/transform.js +5 -4
- package/dist/implementation/compiler/transform.js.map +1 -1
- package/dist/implementation/world/activate.js +14 -2
- package/dist/implementation/world/activate.js.map +1 -1
- package/dist/implementation/world/buffer.d.ts +7 -0
- package/dist/implementation/world/buffer.js +7 -0
- package/dist/implementation/world/buffer.js.map +1 -1
- package/dist/implementation/world/cascade.d.ts +10 -3
- package/dist/implementation/world/cascade.js +48 -23
- package/dist/implementation/world/cascade.js.map +1 -1
- package/dist/implementation/world/commands.js +40 -5
- package/dist/implementation/world/commands.js.map +1 -1
- package/dist/implementation/world/create.js +53 -2
- package/dist/implementation/world/create.js.map +1 -1
- package/dist/implementation/world/execute.d.ts +4 -4
- package/dist/implementation/world/execute.js +132 -83
- package/dist/implementation/world/execute.js.map +1 -1
- package/dist/implementation/world/finalizers.d.ts +2 -0
- package/dist/implementation/world/finalizers.js +24 -3
- package/dist/implementation/world/finalizers.js.map +1 -1
- package/dist/implementation/world/join.js +12 -2
- package/dist/implementation/world/join.js.map +1 -1
- package/dist/implementation/world/resource-changes.d.ts +18 -0
- package/dist/implementation/world/resource-changes.js +66 -0
- package/dist/implementation/world/resource-changes.js.map +1 -0
- package/dist/implementation/world/rows.js +21 -16
- package/dist/implementation/world/rows.js.map +1 -1
- package/dist/implementation/world/run-changes.d.ts +38 -0
- package/dist/implementation/world/run-changes.js +138 -0
- package/dist/implementation/world/run-changes.js.map +1 -0
- package/dist/implementation/world/runner.js +16 -12
- package/dist/implementation/world/runner.js.map +1 -1
- package/dist/implementation/world/state.d.ts +13 -0
- package/dist/implementation/world/state.js +13 -1
- package/dist/implementation/world/state.js.map +1 -1
- package/dist/implementation/world/synchronize.js +72 -22
- package/dist/implementation/world/synchronize.js.map +1 -1
- package/dist/implementation/world/tables-read.d.ts +5 -3
- package/dist/implementation/world/tables-read.js +16 -4
- package/dist/implementation/world/tables-read.js.map +1 -1
- package/dist/implementation/world/tables.d.ts +3 -2
- package/dist/implementation/world/tables.js +83 -37
- package/dist/implementation/world/tables.js.map +1 -1
- package/dist/implementation/world/thread-observer.js +1 -1
- package/dist/implementation/world/thread-observer.js.map +1 -1
- package/dist/implementation/world/thread-systems.js +10 -6
- package/dist/implementation/world/thread-systems.js.map +1 -1
- package/dist/implementation/world/up.js +7 -1
- package/dist/implementation/world/up.js.map +1 -1
- package/dist/implementation/world/where.d.ts +8 -0
- package/dist/implementation/world/where.js +40 -11
- package/dist/implementation/world/where.js.map +1 -1
- package/dist/interface/finalizer/body.d.ts +18 -3
- package/dist/interface/finalizer/create.d.ts +7 -5
- package/dist/interface/finalizer/create.js +8 -5
- package/dist/interface/finalizer/create.js.map +1 -1
- package/dist/interface/finalizer/declaration.d.ts +4 -2
- package/dist/interface/indexes/declaration-of.d.ts +17 -0
- package/dist/interface/indexes/declaration-of.js +20 -0
- package/dist/interface/indexes/declaration-of.js.map +1 -0
- package/dist/interface/indexes/index.d.ts +1 -0
- package/dist/interface/indexes/index.js +1 -0
- package/dist/interface/indexes/index.js.map +1 -1
- package/dist/interface/registry/schedule.d.ts +28 -1
- package/dist/interface/registry/schedule.js +182 -21
- package/dist/interface/registry/schedule.js.map +1 -1
- package/dist/interface/system/access.d.ts +15 -0
- package/dist/interface/system/check.d.ts +7 -0
- package/dist/interface/system/check.js +17 -3
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +26 -5
- package/dist/interface/system/create-resolved.js +1 -1
- package/dist/interface/system/create-resolved.js.map +1 -1
- package/dist/interface/system/create.d.ts +10 -6
- package/dist/interface/system/create.js +5 -4
- package/dist/interface/system/create.js.map +1 -1
- package/dist/interface/system/describe.d.ts +120 -0
- package/dist/interface/system/describe.js +116 -0
- package/dist/interface/system/describe.js.map +1 -0
- package/dist/interface/system/index.d.ts +1 -0
- package/dist/interface/system/index.js +1 -0
- package/dist/interface/system/index.js.map +1 -1
- package/dist/interface/system/leaf.d.ts +6 -0
- package/dist/interface/system/render.js +1 -0
- package/dist/interface/system/render.js.map +1 -1
- package/dist/interface/system/resolve.d.ts +5 -1
- package/dist/interface/system/resolve.js +19 -1
- package/dist/interface/system/resolve.js.map +1 -1
- package/dist/interface/system/shared-only.d.ts +7 -0
- package/dist/interface/system/shared-only.js +2 -0
- package/dist/interface/system/shared-only.js.map +1 -0
- package/dist/interface/system/tables.d.ts +9 -1
- package/dist/interface/world/table/index.d.ts +3 -2
- package/dist/interface/world/world.d.ts +15 -0
- package/package.json +11 -2
- package/src/guide/03-systems.out +1 -1
- package/src/guide/06-sync-points.md +12 -8
- package/src/guide/06-sync-points.out +1 -2
- package/src/guide/06-sync-points.ts +9 -5
- package/src/guide/18-identity.out +1 -1
- package/src/guide/guide.test.ts +3 -150
- package/src/implementation/compiler/body-access.ts +102 -12
- package/src/implementation/compiler/estree.ts +9 -6
- package/src/implementation/compiler/inferred.test.ts +1 -1
- package/src/implementation/compiler/inferred.ts +4 -2
- package/src/implementation/compiler/transform.test.ts +41 -21
- package/src/implementation/compiler/transform.ts +5 -3
- package/src/implementation/world/activate.ts +11 -2
- package/src/implementation/world/batches.test.ts +3 -3
- package/src/implementation/world/buffer.ts +7 -0
- package/src/implementation/world/cascade.ts +46 -22
- package/src/implementation/world/commands.ts +36 -5
- package/src/implementation/world/create.test.ts +8 -8
- package/src/implementation/world/create.ts +57 -2
- package/src/implementation/world/despawn-where.test.ts +287 -0
- package/src/implementation/world/execute.ts +125 -78
- package/src/implementation/world/finalizers.ts +27 -4
- package/src/implementation/world/ids.test.ts +1 -1
- package/src/implementation/world/indexes.test.ts +2 -1
- package/src/implementation/world/join.test.ts +19 -0
- package/src/implementation/world/join.ts +16 -2
- package/src/implementation/world/names.test.ts +1 -2
- package/src/implementation/world/perf.md +3 -1
- package/src/implementation/world/resource-changes.test.ts +61 -0
- package/src/implementation/world/resource-changes.ts +67 -0
- package/src/implementation/world/rows.ts +21 -14
- package/src/implementation/world/rules.test.ts +37 -0
- package/src/implementation/world/run-changes.test.ts +88 -0
- package/src/implementation/world/run-changes.ts +160 -0
- package/src/implementation/world/runner.ts +17 -13
- package/src/implementation/world/shared.test.ts +2 -2
- package/src/implementation/world/state.ts +19 -0
- package/src/implementation/world/sync-points.test.ts +135 -0
- package/src/implementation/world/synchronize.ts +65 -18
- package/src/implementation/world/tables-read.ts +16 -5
- package/src/implementation/world/tables.test.ts +106 -4
- package/src/implementation/world/tables.ts +71 -33
- package/src/implementation/world/thread-observer.ts +1 -1
- package/src/implementation/world/thread-systems.ts +10 -6
- package/src/implementation/world/up.test.ts +13 -1
- package/src/implementation/world/up.ts +6 -1
- package/src/implementation/world/where.ts +38 -10
- package/src/interface/finalizer/body.ts +13 -3
- package/src/interface/finalizer/create.test.ts +3 -1
- package/src/interface/finalizer/create.ts +12 -7
- package/src/interface/finalizer/declaration.ts +4 -2
- package/src/interface/indexes/create.test.ts +10 -0
- package/src/interface/indexes/declaration-of.ts +34 -0
- package/src/interface/indexes/index.ts +1 -0
- package/src/interface/registry/schedule.ts +196 -23
- package/src/interface/system/access.ts +16 -0
- package/src/interface/system/check.ts +15 -2
- package/src/interface/system/commands.ts +23 -4
- package/src/interface/system/commands.type-test.ts +31 -0
- package/src/interface/system/create-resolved.ts +1 -1
- package/src/interface/system/create.test.ts +8 -0
- package/src/interface/system/create.ts +9 -5
- package/src/interface/system/describe.test.ts +79 -0
- package/src/interface/system/describe.ts +122 -0
- package/src/interface/system/index.ts +1 -0
- package/src/interface/system/leaf.ts +6 -0
- package/src/interface/system/render.ts +1 -0
- package/src/interface/system/resolve.ts +25 -2
- package/src/interface/system/shared-only.ts +5 -0
- package/src/interface/system/tables.ts +7 -1
- package/src/interface/world/run.type-test.ts +11 -1
- package/src/interface/world/table/index.ts +3 -2
- package/src/interface/world/world.ts +13 -0
- package/src/perftest/benchmarks.ts +18 -0
- package/src/perftest/structural.ts +20 -0
- package/src/perftest/tables.ts +107 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { Entity } from "../../interface/entity/entity.js";
|
|
4
|
+
import { Index } from "../../interface/indexes/index-type.js";
|
|
5
|
+
import { System } from "../../interface/system/system.js";
|
|
6
|
+
import { workers } from "../../interface/resources/workers.js";
|
|
7
|
+
import { createWorld } from "./create.js";
|
|
8
|
+
import { count as countOf, readEach, select } from "./reads.test-helper.js";
|
|
9
|
+
|
|
10
|
+
const position = Symbol("syncPointsTest/position");
|
|
11
|
+
const cell = Symbol("syncPointsTest/cell");
|
|
12
|
+
const found = Symbol("syncPointsTest/found");
|
|
13
|
+
const team = Symbol("syncPointsTest/team");
|
|
14
|
+
const member = Symbol("syncPointsTest/member");
|
|
15
|
+
const local = Symbol("syncPointsTest/local");
|
|
16
|
+
const total = Symbol("syncPointsTest/total");
|
|
17
|
+
const parent = Symbol("syncPointsTest/parent");
|
|
18
|
+
const seed = Symbol("syncPointsTest/seed");
|
|
19
|
+
const grown = Symbol("syncPointsTest/grown");
|
|
20
|
+
const children = Symbol("syncPointsTest/children");
|
|
21
|
+
|
|
22
|
+
declare module "@adobe/ecs" {
|
|
23
|
+
export interface Components {
|
|
24
|
+
[position]: { type: number };
|
|
25
|
+
[cell]: { type: number };
|
|
26
|
+
[team]: { type: true };
|
|
27
|
+
[member]: { type: Entity; reference: "owner" };
|
|
28
|
+
[local]: { type: number };
|
|
29
|
+
[total]: { type: number };
|
|
30
|
+
[parent]: { type: Entity; reference: "weak" };
|
|
31
|
+
[seed]: { type: number };
|
|
32
|
+
[grown]: { type: number };
|
|
33
|
+
}
|
|
34
|
+
export interface Resources {
|
|
35
|
+
[found]: { type: number };
|
|
36
|
+
[children]: { type: number; default: 0 };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const byCell = Index.create({ key: [cell] });
|
|
41
|
+
|
|
42
|
+
const spawn = System.create(({ commands }) => { for (let k = 0; k < 3; k++) commands.spawn([position])({ [position]: k }); });
|
|
43
|
+
const counted: number[] = [];
|
|
44
|
+
const count = System.create({ components: { with: [position] }, after: [spawn] }, row => { counted.push(row[position]); });
|
|
45
|
+
const changes: number[] = [];
|
|
46
|
+
const watchChanges = System.create({ components: { with: [position], changed: [position] }, after: [count] }, row => { changes.push(row[position]); });
|
|
47
|
+
const deriveCells = System.create({ components: { with: [position], not: [cell] }, after: [watchChanges] }, (row, { commands }) => { commands.add(row.entity, cell, row[position] % 2); });
|
|
48
|
+
const pick = System.create({ lookup: { cells: { index: byCell } }, after: [deriveCells] }, ({ lookup, resources }) => { resources[found] = lookup.cells(0).length; });
|
|
49
|
+
const readFound = System.create({ readonly: true }, ({ resources }) => resources[found]);
|
|
50
|
+
const frame = System.create([spawn, count, watchChanges, deriveCells, pick]);
|
|
51
|
+
|
|
52
|
+
const spawnTeams = System.create(({ commands }) => {
|
|
53
|
+
for (let k = 0; k < 3; k++) {
|
|
54
|
+
const owner = commands.spawn([team])();
|
|
55
|
+
for (let m = 0; m < 2; m++) commands.spawn([member])({ [member]: owner });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
const disband = System.create({ components: { with: [team] }, after: [spawnTeams] }, (row, { commands }) => { commands.despawn(row.entity); });
|
|
59
|
+
const round = System.create([spawnTeams, disband]);
|
|
60
|
+
|
|
61
|
+
/** Spawns a root, `count` children of it and a child of each. */
|
|
62
|
+
const spawnTree = System.create({}, ({ commands }, input: { readonly count: number }) => {
|
|
63
|
+
const root = commands.spawn([local, total])({ [local]: 1, [total]: 0 });
|
|
64
|
+
for (let k = 0; k < input.count; k++) {
|
|
65
|
+
const child = commands.spawn([local, total, parent])({ [local]: 1, [total]: 0, [parent]: root });
|
|
66
|
+
commands.spawn([local, total, parent])({ [local]: 1, [total]: 0, [parent]: child });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
const propagate = System.create({ components: { with: [local, total], changed: [local] }, up: { through: parent }, after: [spawnTree] }, (row, { up }) => {
|
|
70
|
+
row[total] = (up === null ? 0 : up[total]) + row[local];
|
|
71
|
+
});
|
|
72
|
+
const grow = System.create([spawnTree, propagate]);
|
|
73
|
+
|
|
74
|
+
const plant = System.create(({ commands }) => { for (let k = 0; k < 64; k++) commands.spawn([seed])({ [seed]: k }); });
|
|
75
|
+
const sprout = System.create({ kernel: true, components: { with: [seed] }, after: [plant] }, (row, { commands }) => {
|
|
76
|
+
commands.spawn([grown])({ [grown]: row[seed] * 2 });
|
|
77
|
+
});
|
|
78
|
+
const tally = System.create({ components: { with: [grown] }, after: [sprout] }, (row, { resources }) => { resources[children] += row[grown]; });
|
|
79
|
+
const garden = System.create([plant, sprout, tally]);
|
|
80
|
+
const readChildren = System.create({ readonly: true }, ({ resources }) => resources[children]);
|
|
81
|
+
|
|
82
|
+
describe("sync points within a run", () => {
|
|
83
|
+
it("apply commands before a later system that depends on what they change, which then sees them in the same run", async () => {
|
|
84
|
+
const world = createWorld({ [workers]: 0 });
|
|
85
|
+
await world.run(frame);
|
|
86
|
+
expect(counted).toEqual([0, 1, 2]);
|
|
87
|
+
expect(await world.run(readFound)).toBe(2);
|
|
88
|
+
await world.dispose();
|
|
89
|
+
});
|
|
90
|
+
it("keep each change seen once by change filters across runs", async () => {
|
|
91
|
+
const world = createWorld({ [workers]: 0 });
|
|
92
|
+
changes.length = 0;
|
|
93
|
+
await world.run(frame);
|
|
94
|
+
expect(changes).toEqual([0, 1, 2]);
|
|
95
|
+
changes.length = 0;
|
|
96
|
+
counted.length = 0;
|
|
97
|
+
await world.run(frame);
|
|
98
|
+
// The second run's spawns are new; the first run's three are seen again by count alone.
|
|
99
|
+
expect([counted.length, changes]).toEqual([6, [0, 1, 2]]);
|
|
100
|
+
expect(await world.run(readFound)).toBe(4);
|
|
101
|
+
await world.dispose();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("despawn what rows spawned at an earlier sync point of the run own, once a later tier despawns their owners", async () => {
|
|
105
|
+
expect(System.describe(round).tiers.map(tier => tier.sync === null)).toEqual([false, true]);
|
|
106
|
+
await using world = createWorld({ [workers]: 0 });
|
|
107
|
+
await world.run(round);
|
|
108
|
+
expect([await countOf(world, { required: [team] }), await countOf(world, { required: [member] })]).toEqual([0, 0]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("bring an `up` tree up to date at each, built again or patched, for a tier after it in the same run", async () => {
|
|
112
|
+
expect(System.describe(grow).tiers.map(tier => tier.sync === null)).toEqual([false, true]);
|
|
113
|
+
await using world = createWorld({ [workers]: 0 });
|
|
114
|
+
let spawned = 0;
|
|
115
|
+
// Past the journal's worth a tree patches, then within it.
|
|
116
|
+
for (const count of [5_000, 3_000, 10]) {
|
|
117
|
+
await world.run(grow, { count });
|
|
118
|
+
spawned += count;
|
|
119
|
+
const totals = await readEach(world, await select(world, { required: [total] }), total);
|
|
120
|
+
const byDepth = [1, 2, 3].map(depth => totals.filter(value => value === depth).length);
|
|
121
|
+
expect([byDepth, totals.length]).toEqual([[totals.length - 2 * spawned, spawned, spawned], totals.length]);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("commit a kernel's staged spawns at a sync point within the run, for a later tier", async () => {
|
|
126
|
+
expect(System.describe(garden).tiers.map(tier => tier.sync === null)).toEqual([false, false, true]);
|
|
127
|
+
await using world = createWorld({ [workers]: 0 });
|
|
128
|
+
const sum = 63 * 64;
|
|
129
|
+
await world.run(garden);
|
|
130
|
+
expect(await world.run(readChildren)).toBe(sum);
|
|
131
|
+
// The second run's seeds are both runs' plantings, and its tally reads the first run's grown rows too.
|
|
132
|
+
await world.run(garden);
|
|
133
|
+
expect(await world.run(readChildren)).toBe(sum + sum + 2 * sum);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import type { Views } from '../../interface/registry/layout.js';
|
|
3
3
|
import { copyValue, ensureCapacity, swapRemove, writeF32, writeF32Lanes, writeF64, writeI32, writeU32, type Archetype, type Storage } from './archetype.js';
|
|
4
4
|
import { blockShift, chunkShift } from './kernel.js';
|
|
5
|
-
import { addCommand, clearBuffer, despawnCommand, endOf, referenceAt, removeCommand, spawnCommand, type CommandBuffer } from './buffer.js';
|
|
5
|
+
import { addCommand, clearBuffer, despawnCommand, despawnWhereCommand, despawnWhereHolds, endOf, referenceAt, removeCommand, spawnCommand, type CommandBuffer } from './buffer.js';
|
|
6
6
|
import { readCell, writeCell } from './cells.js';
|
|
7
7
|
import { commitBatches, commitStaged } from './batches.js';
|
|
8
|
-
import { applyIndexChanges, cascadeReferences, indexChangesOf, type IndexChanges, type Written } from './cascade.js';
|
|
8
|
+
import { applyIndexChanges, cascadeReferences, indexChangesOf, linkBorn, type IndexChanges, type Written } from './cascade.js';
|
|
9
9
|
import { refreshDirectories } from './directories.js';
|
|
10
10
|
import { createArrivals, logArrival } from './arrivals.js';
|
|
11
11
|
import { noteEdge, noteEmptied, reclaimEmptied } from './reclaim.js';
|
|
@@ -21,6 +21,8 @@ import type { Plan } from './plan.js';
|
|
|
21
21
|
import type { MoveLog } from './move-log.js';
|
|
22
22
|
import { archetypeOf, movesOf, sharedNumberOf, withComponent, withoutComponent, withShared, type State } from './state.js';
|
|
23
23
|
import { releaseValue } from './transfers.js';
|
|
24
|
+
import { resourcesWritten } from './resource-changes.js';
|
|
25
|
+
import { recordPlan } from './run-changes.js';
|
|
24
26
|
|
|
25
27
|
/** Buffers queued for the next sync point, in merge order. Commands in guarded buffers can't touch the resource entity. */
|
|
26
28
|
export type Queued = { readonly buffer: CommandBuffer; readonly guarded: boolean };
|
|
@@ -127,6 +129,56 @@ const releaseDropped = (state: State, buffer: CommandBuffer, command: number): v
|
|
|
127
129
|
}
|
|
128
130
|
};
|
|
129
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Whether `archetype` has each component of `ids` from `start` to `middle` and none from there to `end`, and holds the
|
|
134
|
+
* values `holds` tells it holds, when it isn't null.
|
|
135
|
+
*/
|
|
136
|
+
const matches = (archetype: Archetype, ids: Int32Array, start: number, middle: number, end: number, holds: Function | null): boolean => {
|
|
137
|
+
const { storageById } = archetype;
|
|
138
|
+
for (let k = start; k < middle; k++) if (storageById[ids[k]] === undefined) return false;
|
|
139
|
+
for (let k = middle; k < end; k++) if (storageById[ids[k]] !== undefined) return false;
|
|
140
|
+
return holds === null || Reflect.apply(holds, undefined, [archetype]) === true;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** The archetype an entity planned into `archetype` ends up in once derived. */
|
|
144
|
+
const closedOf = (state: State, archetype: Archetype): Archetype => {
|
|
145
|
+
const cached = state.structure.closures[archetype.id] ?? -1;
|
|
146
|
+
return cached >= 0 ? state.archetypes[cached] : closureOf(state.structure, state.derived, archetype, (components, shared) => archetypeOf(state, components, shared), state.archetypes);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Despawns each entity a `despawnWhere` command selects given the plan so far: those planned, by where they're going,
|
|
151
|
+
* then the rest, by the archetype they're in. Never the resource entity.
|
|
152
|
+
*/
|
|
153
|
+
const despawnWhere = (state: State, buffer: CommandBuffer, command: number, within: boolean): void => {
|
|
154
|
+
const { plan, archetypes } = state;
|
|
155
|
+
const { ids } = buffer;
|
|
156
|
+
const start = buffer.starts[command];
|
|
157
|
+
const middle = start + buffer.entities[command];
|
|
158
|
+
const last = endOf(buffer, command);
|
|
159
|
+
const where = ids[last - 1] === despawnWhereHolds ? buffer.values[last - 1] : null;
|
|
160
|
+
const holds = typeof where === "function" ? where : null;
|
|
161
|
+
const end = holds === null ? last : last - 1;
|
|
162
|
+
for (let entry = 0; entry < plan.count; entry++) {
|
|
163
|
+
if (plan.despawned[entry] === 1 || plan.entity[entry] === state.resourceEntity) continue;
|
|
164
|
+
const to = archetypes[plan.to[entry]];
|
|
165
|
+
if (matches(within && plan.to[entry] !== plan.from[entry] ? closedOf(state, to) : to, ids, start, middle, end, holds)) plan.despawned[entry] = 1;
|
|
166
|
+
}
|
|
167
|
+
const { slots } = plan;
|
|
168
|
+
const count = archetypes.length;
|
|
169
|
+
for (let id = 0; id < count; id++) {
|
|
170
|
+
const archetype = archetypes[id];
|
|
171
|
+
if (archetype.resource || archetype.count === 0 || !matches(archetype, ids, start, middle, end, holds)) continue;
|
|
172
|
+
ensurePlanCapacity(state, plan.count + archetype.count);
|
|
173
|
+
const { entities } = archetype;
|
|
174
|
+
for (let row = 0; row < archetype.count; row++) {
|
|
175
|
+
const entity = entities[row];
|
|
176
|
+
const index = indexOf(entity);
|
|
177
|
+
if (slots[index] < 0) plan.despawned[addPlanned(state, entity, index, id, id)] = 1;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
130
182
|
/**
|
|
131
183
|
* Plan: the net change for each entity, in issue order. Commands against dead entities do nothing. Reference values
|
|
132
184
|
* written by commands go to `written`, to check once despawns are known.
|
|
@@ -146,6 +198,11 @@ const planOf = (state: State, queued: readonly Queued[], written: Written): void
|
|
|
146
198
|
ensurePlanCapacity(state, plan.count + buffer.count);
|
|
147
199
|
for (let command = 0; command < buffer.count; command++) {
|
|
148
200
|
const kind = kinds[command];
|
|
201
|
+
if (kind === despawnWhereCommand) {
|
|
202
|
+
despawnWhere(state, buffer, command, within);
|
|
203
|
+
ensurePlanCapacity(state, plan.count + buffer.count - command);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
149
206
|
const entity = entities[command];
|
|
150
207
|
if (guarded && entity === state.resourceEntity) continue;
|
|
151
208
|
const index = indexOf(entity);
|
|
@@ -631,7 +688,9 @@ const noneQueued: readonly Queued[] = [];
|
|
|
631
688
|
export const syncPending = (state: State, queued: readonly Queued[] = state.queued): boolean =>
|
|
632
689
|
anyCommands(queued) || reserved(state.entities) || state.carried !== null || state.structure.rederive
|
|
633
690
|
// Failures for its run, as a gone thread's stops it never finalized are.
|
|
634
|
-
|| state.unplaced.length > 0
|
|
691
|
+
|| state.unplaced.length > 0
|
|
692
|
+
// A watched resource written, which its changes finalizers see at a sync point.
|
|
693
|
+
|| (state.resourcesSeen.size > 0 && resourcesWritten(state));
|
|
635
694
|
|
|
636
695
|
/**
|
|
637
696
|
* The first half of a sync point: commits batch spawns, plans every command, cascades references and closes the plan
|
|
@@ -688,22 +747,9 @@ const preparePending = (state: State, queued: readonly Queued[], isolated: boole
|
|
|
688
747
|
if (deriving) for (let entry = 0; entry < plan.count; entry++) if (plan.despawned[entry] === 0) plan.to[entry] = close(state.archetypes[plan.to[entry]]).id;
|
|
689
748
|
let index: IndexChanges | null = null;
|
|
690
749
|
if (structure.referenceIds.length > 0) {
|
|
691
|
-
|
|
692
|
-
const archetype = state.archetypes[id];
|
|
693
|
-
for (const component of structure.referenceIds) {
|
|
694
|
-
const storage = archetype.storageById[component];
|
|
695
|
-
if (storage === undefined) continue;
|
|
696
|
-
for (let row = first; row < archetype.count; row++) written.push(archetype.entities[row], component, Number(readCell(storage, row)));
|
|
697
|
-
}
|
|
698
|
-
}
|
|
750
|
+
linkBorn(state, born, written, tick);
|
|
699
751
|
cascadeReferences(state, queued, written, entity => entryOf(state, entity));
|
|
700
|
-
|
|
701
|
-
const from = plan.from[entry];
|
|
702
|
-
if (from < 0) return null;
|
|
703
|
-
const first = born.get(from);
|
|
704
|
-
return first !== undefined && state.entities.row[indexOf(plan.entity[entry])] >= first ? null : state.archetypes[from];
|
|
705
|
-
};
|
|
706
|
-
index = indexChangesOf(state, queued, fromOf, [...born].map(([id, first]) => [state.archetypes[id], first] as const));
|
|
752
|
+
index = indexChangesOf(state, queued, entry => plan.from[entry] < 0 ? null : state.archetypes[plan.from[entry]]);
|
|
707
753
|
}
|
|
708
754
|
// References change only here: each component written to is stamped, and each holder journaled, as are spawns.
|
|
709
755
|
for (let k = 1; k < written.length; k += 3) state.referencesWritten[written[k]] = tick;
|
|
@@ -741,6 +787,7 @@ export const applySync = (state: State): readonly Thrown[] => {
|
|
|
741
787
|
let errors = nothingThrown;
|
|
742
788
|
try {
|
|
743
789
|
if (state.unplaced.length > 0 && !isolated) errors = state.unplaced.splice(0);
|
|
790
|
+
if (state.recording !== null && !isolated) recordPlan(state, state.recording, queued, born);
|
|
744
791
|
if (state.finalizers.length > 0 && !isolated) {
|
|
745
792
|
const thrown = runFinalizers(state, queued, born, tick);
|
|
746
793
|
errors = errors.length === 0 ? thrown : [...errors, ...thrown];
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
import { labelOf } from '../../interface/registry/names.js';
|
|
3
3
|
import type { Tables, TablesDeclaration } from '../../interface/system/tables.js';
|
|
4
4
|
import type { Table } from '../../interface/world/table/table.js';
|
|
5
|
-
import type { Storage } from './archetype.js';
|
|
5
|
+
import type { Archetype, Storage } from './archetype.js';
|
|
6
6
|
import { readCell } from './cells.js';
|
|
7
7
|
import { resourceCell } from './resources.js';
|
|
8
8
|
import { idOf, type Key, type State } from './state.js';
|
|
9
|
+
import { holdsValuesOf } from './where.js';
|
|
9
10
|
|
|
10
11
|
// Reading tables, apart from spawning them: rows' contexts import it, and what spawning imports would make a cycle.
|
|
11
12
|
|
|
@@ -27,12 +28,21 @@ const columnOf = (key: Key, storage: Storage, count: number): Table.Column => {
|
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
|
-
* A readonly system's `tables
|
|
31
|
-
* entities and a copy of the column of each of them it holds;
|
|
31
|
+
* A readonly system's `tables`, read by the function this returns, once per read: of each archetype matching `with`,
|
|
32
|
+
* `not` and `where` and holding any of `components`, its entities and a copy of the column of each of them it holds;
|
|
33
|
+
* and the value of each resource of `resources`. `where` holds its values as a system's does, and an archetype
|
|
34
|
+
* matches it as one matches a system's `where`, required as `with` requires them.
|
|
32
35
|
*/
|
|
33
|
-
export const
|
|
36
|
+
export const tablesReader = (state: State, declaration: TablesDeclaration): (() => Tables) => {
|
|
37
|
+
const holds = declaration.where === undefined ? null : holdsValuesOf(state, declaration.where, true);
|
|
38
|
+
if (typeof holds === "symbol") throw new Error(`Tables' where names "${labelOf(holds)}", which isn't a shared component.`);
|
|
39
|
+
const where = declaration.where === undefined ? [] : Object.getOwnPropertySymbols(declaration.where);
|
|
40
|
+
return () => readTables(state, declaration, [...where, ...declaration.with ?? []], holds);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const readTables = (state: State, declaration: TablesDeclaration, requiring: readonly Key[], holds: ((archetype: Archetype) => boolean) | null): Tables => {
|
|
34
44
|
const asked = declaration.components.map(key => ({ key, id: idOf(state, key) }));
|
|
35
|
-
const required =
|
|
45
|
+
const required = requiring.map(key => idOf(state, key));
|
|
36
46
|
const excluded = (declaration.not ?? []).map(key => idOf(state, key));
|
|
37
47
|
const archetypes: Table[] = [];
|
|
38
48
|
for (const archetype of state.archetypes) {
|
|
@@ -40,6 +50,7 @@ export const readTables = (state: State, declaration: TablesDeclaration): Tables
|
|
|
40
50
|
if (count === 0 || archetype.reclaimed) continue;
|
|
41
51
|
const { storageById } = archetype;
|
|
42
52
|
if (required.some(id => storageById[id] === undefined) || excluded.some(id => storageById[id] !== undefined)) continue;
|
|
53
|
+
if (holds !== null && !holds(archetype)) continue;
|
|
43
54
|
const columns: { [key: symbol]: Table.Column } = {};
|
|
44
55
|
let held = false;
|
|
45
56
|
for (const { key, id } of asked) {
|
|
@@ -46,16 +46,31 @@ const tablesOf = (components: readonly Component.Key[], resources: readonly Reso
|
|
|
46
46
|
System.create({ readonly: true, tables: { components, resources, not } }, ({ tables }): Tables => tables);
|
|
47
47
|
|
|
48
48
|
const spawnedBy: { spawned: Table.Spawned } = { spawned: { entities: [], dropped: 0 } };
|
|
49
|
-
const given: { tables: readonly Table[]; resources?: { readonly [key: symbol]: unknown } } = { tables: [] };
|
|
49
|
+
const given: { tables: readonly Table[]; resources?: { readonly [key: symbol]: unknown }; existing?: ReadonlyMap<Entity, Entity> } = { tables: [] };
|
|
50
50
|
const spawnGiven = System.create(({ commands }) => {
|
|
51
|
-
spawnedBy.spawned =
|
|
51
|
+
spawnedBy.spawned = commands.spawnTables(given.tables, { resources: given.resources, existing: given.existing });
|
|
52
52
|
});
|
|
53
|
-
const spawnTables = async (world: ReturnType<typeof inline>, tables: readonly Table[]): Promise<Table.Spawned> => {
|
|
53
|
+
const spawnTables = async (world: ReturnType<typeof inline>, tables: readonly Table[], existing?: ReadonlyMap<Entity, Entity>): Promise<Table.Spawned> => {
|
|
54
54
|
given.tables = tables;
|
|
55
|
-
|
|
55
|
+
given.existing = existing;
|
|
56
|
+
try {
|
|
57
|
+
await world.run(spawnGiven);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
given.existing = undefined;
|
|
61
|
+
}
|
|
56
62
|
return spawnedBy.spawned;
|
|
57
63
|
};
|
|
58
64
|
|
|
65
|
+
/** Spawns `labels` in `world`, giving each label's entity. */
|
|
66
|
+
const spawnLabels = async (world: ReturnType<typeof inline>, labels: readonly string[]): Promise<ReadonlyMap<string, Entity>> => {
|
|
67
|
+
const made = new Map<string, Entity>();
|
|
68
|
+
await world.run(System.create(({ commands }) => {
|
|
69
|
+
for (const text of labels) made.set(text, commands.spawn([label])({ [label]: text }));
|
|
70
|
+
}));
|
|
71
|
+
return made;
|
|
72
|
+
};
|
|
73
|
+
|
|
59
74
|
const values = System.create({ readonly: true, components: { with: [label], optional: [position, parent, target, link, friends, tags, buddy] } }, row => {
|
|
60
75
|
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
76
|
});
|
|
@@ -198,6 +213,53 @@ describe("tables", () => {
|
|
|
198
213
|
expect(read.find(entry => entry.label === "pointing")).toMatchObject({ buddy: Entity.none, link: { to: Entity.none, weight: 1 }, friends: [Entity.none, inside] });
|
|
199
214
|
});
|
|
200
215
|
|
|
216
|
+
it("spawn no row existing maps, giving the entity it maps to, which every entity value naming it names", async () => {
|
|
217
|
+
await using world = inline();
|
|
218
|
+
let steel = Entity.none;
|
|
219
|
+
await world.run(System.create(({ commands }) => {
|
|
220
|
+
steel = commands.spawn([label])({ [label]: "steel" });
|
|
221
|
+
commands.spawn([label, parent, target, buddy, link, friends])({ [label]: "body", [parent]: steel, [target]: steel, [buddy]: steel, [link]: { to: steel, weight: 2 }, [friends]: [steel] });
|
|
222
|
+
}));
|
|
223
|
+
const { archetypes } = await world.run(tablesOf([label, parent, target, buddy, link, friends]));
|
|
224
|
+
await using other = inline();
|
|
225
|
+
const existingSteel = (await spawnLabels(other, ["steel in other"])).get("steel in other") ?? Entity.none;
|
|
226
|
+
const { entities, dropped } = await spawnTables(other, archetypes, new Map([[steel, existingSteel]]));
|
|
227
|
+
expect(dropped).toBe(0);
|
|
228
|
+
expect(entities.flatMap(list => [...list])).toContain(existingSteel);
|
|
229
|
+
const read = await readAll(other);
|
|
230
|
+
// Steel isn't spawned again: the body's every entity value names the one already there.
|
|
231
|
+
expect(read.map(entry => entry.label)).toEqual(["body", "steel in other"]);
|
|
232
|
+
expect(read.find(entry => entry.label === "body")).toMatchObject({ parent: existingSteel, target: existingSteel, buddy: existingSteel, link: { to: existingSteel, weight: 2 }, friends: [existingSteel] });
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("let an entity existing maps stand for one outside the tables, so what it owns or points at is kept", async () => {
|
|
236
|
+
await using world = inline();
|
|
237
|
+
let steel = Entity.none;
|
|
238
|
+
await world.run(System.create(({ commands }) => {
|
|
239
|
+
steel = commands.spawn([pinned])();
|
|
240
|
+
commands.spawn([label, parent, target])({ [label]: "owned", [parent]: steel, [target]: steel });
|
|
241
|
+
}));
|
|
242
|
+
const { archetypes } = await world.run(tablesOf([label, parent, target]));
|
|
243
|
+
await using other = inline();
|
|
244
|
+
const existingSteel = (await spawnLabels(other, ["steel"])).get("steel") ?? Entity.none;
|
|
245
|
+
const { dropped } = await spawnTables(other, archetypes, new Map([[steel, existingSteel]]));
|
|
246
|
+
expect(dropped).toBe(0);
|
|
247
|
+
expect((await readAll(other)).find(entry => entry.label === "owned")).toMatchObject({ parent: existingSteel, target: existingSteel });
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("refuse existing mapping to an entity that isn't alive, spawning nothing", async () => {
|
|
251
|
+
await using world = inline();
|
|
252
|
+
const gone = (await spawnLabels(world, ["gone"])).get("gone") ?? Entity.none;
|
|
253
|
+
await world.run(System.create(({ commands }) => { commands.despawn(gone); }));
|
|
254
|
+
const { archetypes } = await world.run(tablesOf([label]));
|
|
255
|
+
await using source = inline();
|
|
256
|
+
const steel = (await spawnLabels(source, ["steel"])).get("steel") ?? Entity.none;
|
|
257
|
+
const { archetypes: tables } = await source.run(tablesOf([label]));
|
|
258
|
+
expect(archetypes).toEqual([]);
|
|
259
|
+
await expect(spawnTables(world, tables, new Map([[steel, gone]]))).rejects.toThrow(new RegExp(`as entity ${gone}, which isn't alive`));
|
|
260
|
+
expect(await readAll(world)).toEqual([]);
|
|
261
|
+
});
|
|
262
|
+
|
|
201
263
|
it("set the resources they're given at the sync point, beside the entities", async () => {
|
|
202
264
|
await using world = createWorld({ [workers]: 0, [score]: 7 });
|
|
203
265
|
given.tables = [];
|
|
@@ -249,6 +311,46 @@ describe("tables", () => {
|
|
|
249
311
|
expect(await readAll(world)).toEqual([]);
|
|
250
312
|
});
|
|
251
313
|
|
|
314
|
+
it("keep to the archetypes holding where's shared values, each required as with requires it", async () => {
|
|
315
|
+
await using world = inline();
|
|
316
|
+
await world.run(System.create(({ commands }) => {
|
|
317
|
+
commands.spawn([team, label], { [team]: "red" })({ [label]: "red" });
|
|
318
|
+
commands.spawn([team, label, position], { [team]: "red" })({ [label]: "red placed", [position]: [1, 1, 1] });
|
|
319
|
+
commands.spawn([team, label], { [team]: "blue" })({ [label]: "blue" });
|
|
320
|
+
commands.spawn([label])({ [label]: "none" });
|
|
321
|
+
}));
|
|
322
|
+
const red = await world.run(System.create({ readonly: true, tables: { components: [label], where: { [team]: "red" } } }, ({ tables }): Tables => tables));
|
|
323
|
+
expect(red.archetypes.map(table => table.columns[label]).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))))
|
|
324
|
+
.toEqual([{ kind: "values", values: ["red placed"] }, { kind: "values", values: ["red"] }]);
|
|
325
|
+
const placed = await world.run(System.create({ readonly: true, tables: { components: [label], with: [position], where: { [team]: "red" } } }, ({ tables }): Tables => tables));
|
|
326
|
+
expect(placed.archetypes.map(table => table.columns[label])).toEqual([{ kind: "values", values: ["red placed"] }]);
|
|
327
|
+
// One system's tables follow its archetypes as they come and go.
|
|
328
|
+
const blue = System.create({ readonly: true, tables: { components: [label], where: { [team]: "blue" } } }, ({ tables }): Tables => tables);
|
|
329
|
+
expect((await world.run(blue)).archetypes.map(table => table.count)).toEqual([1]);
|
|
330
|
+
await world.run(System.create(({ commands }) => {
|
|
331
|
+
commands.spawn([team, label, position], { [team]: "blue" })({ [label]: "blue placed", [position]: [2, 2, 2] });
|
|
332
|
+
}));
|
|
333
|
+
expect((await world.run(blue)).archetypes.map(table => table.count)).toEqual([1, 1]);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("give no table for a shared value no archetype holds", async () => {
|
|
337
|
+
await using world = inline();
|
|
338
|
+
await world.run(System.create(({ commands }) => { commands.spawn([team, label], { [team]: "red" })({ [label]: "red" }); }));
|
|
339
|
+
const blue = System.create({ readonly: true, tables: { components: [label], where: { [team]: "blue" } } }, ({ tables }): Tables => tables);
|
|
340
|
+
expect((await world.run(blue)).archetypes).toEqual([]);
|
|
341
|
+
await world.run(System.create(({ commands }) => { commands.spawn([team, label], { [team]: "blue" })({ [label]: "blue" }); }));
|
|
342
|
+
expect((await world.run(blue)).archetypes.map(table => table.columns[label])).toEqual([{ kind: "values", values: ["blue"] }]);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("take only values of shared components in where", () => {
|
|
346
|
+
// @ts-expect-error: where names only shared components.
|
|
347
|
+
expect(() => System.create({ readonly: true, tables: { components: [label], where: { [label]: "a" } } }, () => {})).toThrow(/tables' where names "tablesTest\/label", which isn't a shared component/);
|
|
348
|
+
// @ts-expect-error: tables' where takes values, not predicates.
|
|
349
|
+
expect(() => System.create({ readonly: true, tables: { components: [label], where: { [team]: (value: string) => value === "red" } } }, () => {})).toThrow(/tables' where gives "tablesTest\/team" a predicate/);
|
|
350
|
+
// @ts-expect-error: a value of the component's type.
|
|
351
|
+
expect(() => System.create({ readonly: true, tables: { components: [label], where: { [team]: "green" } } }, () => {})).toThrow(/isn't one of its type's values/);
|
|
352
|
+
});
|
|
353
|
+
|
|
252
354
|
it("are read only by a readonly system without components", () => {
|
|
253
355
|
// @ts-expect-error: tables are read only by a readonly system, which the declaration's type says too.
|
|
254
356
|
expect(() => System.create({ tables: { components: [label] } }, () => {})).toThrow(/reads tables, which only a readonly system reads/);
|
|
@@ -5,7 +5,7 @@ import { isEntity } from '../../interface/registry/schemas.js';
|
|
|
5
5
|
import type { Schema } from '../../interface/schema/schema.js';
|
|
6
6
|
import type { Table } from '../../interface/world/table/table.js';
|
|
7
7
|
import { batchOf } from './batches.js';
|
|
8
|
-
import { indexOf, reserveRun } from './entities.js';
|
|
8
|
+
import { indexOf, locate, reserveRun } from './entities.js';
|
|
9
9
|
import { internShared } from './shared.js';
|
|
10
10
|
import { claimRows, openRows, writerOf, type Column as BatchColumn } from './staging.js';
|
|
11
11
|
import { commandIdOf, type Key, type State } from './state.js';
|
|
@@ -153,6 +153,12 @@ const placesOf = (plans: readonly Planned[]): { readonly entities: Float64Array;
|
|
|
153
153
|
/** A reference column's entity value in each row: the f64 at the row's start, a reference's whole value. */
|
|
154
154
|
const referencesOf = (column: Table.Column): ((row: number) => number) => {
|
|
155
155
|
if (column.kind !== "bytes") return () => 0;
|
|
156
|
+
const { bytes } = column;
|
|
157
|
+
if (bytes.byteOffset % 8 === 0 && column.stride % 8 === 0) {
|
|
158
|
+
const view = new Float64Array(bytes.buffer, bytes.byteOffset, bytes.byteLength >> 3);
|
|
159
|
+
const words = column.stride >> 3;
|
|
160
|
+
return row => view[row * words];
|
|
161
|
+
}
|
|
156
162
|
const view = new DataView(column.bytes.buffer, column.bytes.byteOffset, column.bytes.byteLength);
|
|
157
163
|
const { stride } = column;
|
|
158
164
|
return row => view.getFloat64(row * stride, true);
|
|
@@ -203,16 +209,25 @@ const spawnRows = (state: State, { table, keys, ids, shared }: Planned, rows: re
|
|
|
203
209
|
claimRows(batch, entities);
|
|
204
210
|
};
|
|
205
211
|
|
|
212
|
+
/** Throws for an entity `existing` maps to that isn't alive in this world. */
|
|
213
|
+
const checkExisting = (state: State, existing: ReadonlyMap<number, number>): void => {
|
|
214
|
+
for (const [source, target] of existing) {
|
|
215
|
+
if (locate(state.entities, target) < 0) throw new Error(`commands.spawnTables: entity ${source} is given as existing as entity ${target}, which isn't alive.`);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
206
219
|
/**
|
|
207
220
|
* Spawns an entity for each row of `tables` into the batches of their components, as references would leave them had
|
|
208
221
|
* the entities outside the tables despawned: a row owned, directly or through others, by an entity outside them isn't
|
|
209
222
|
* spawned; a weak reference to one is removed; and any other entity value naming one is `Entity.none`. Every entity
|
|
210
|
-
* value naming an entity spawned names the one spawned for it.
|
|
223
|
+
* value naming an entity spawned names the one spawned for it. A row whose entity `existing` maps isn't spawned: the
|
|
224
|
+
* entity it maps to stands for it, as it does for an entity outside the tables that `existing` maps.
|
|
211
225
|
*/
|
|
212
|
-
export const spawnTables = (state: State, tables: readonly Table[]): Table.Spawned => {
|
|
226
|
+
export const spawnTables = (state: State, tables: readonly Table[], existing: ReadonlyMap<number, number> = new Map()): Table.Spawned => {
|
|
213
227
|
const plans = tables.map(table => planOf(state, table));
|
|
214
|
-
|
|
215
|
-
|
|
228
|
+
checkExisting(state, existing);
|
|
229
|
+
// Entities are mapped only where a value of the tables may hold one, or a row is one that exists.
|
|
230
|
+
const remapping = existing.size > 0 || plans.some(({ keys }) => keys.some(key => {
|
|
216
231
|
const schema = declarationOf(key)?.type;
|
|
217
232
|
return schema !== undefined && holdsEntity(schema);
|
|
218
233
|
}));
|
|
@@ -237,65 +252,87 @@ export const spawnTables = (state: State, tables: readonly Table[]): Table.Spawn
|
|
|
237
252
|
firsts.push(total);
|
|
238
253
|
total += table.count;
|
|
239
254
|
}
|
|
255
|
+
// The entity standing for each row `existing` maps, 0 for a row spawned.
|
|
256
|
+
const existingAt = new Float64Array(total);
|
|
257
|
+
if (existing.size > 0) plans.forEach(({ table }, t) => {
|
|
258
|
+
for (let row = 0; row < table.count; row++) existingAt[firsts[t] + row] = existing.get(table.entities[row]) ?? 0;
|
|
259
|
+
});
|
|
240
260
|
// A row owned by an entity outside the tables isn't spawned, nor anything it owns, as despawning its owner would.
|
|
241
261
|
const dropped = new Uint8Array(total);
|
|
242
|
-
/** Each owner reference between rows of the tables: the owned row's position, and its owner's. */
|
|
243
|
-
const ownedRows: number[] = [];
|
|
244
|
-
const ownerRows: number[] = [];
|
|
245
262
|
const waiting: number[] = [];
|
|
246
|
-
|
|
263
|
+
/**
|
|
264
|
+
* Calls `each` with each owner reference of a row that doesn't exist: the owned row's position, its owner's among
|
|
265
|
+
* the tables' rows or -1 for an owner outside them, and the owner.
|
|
266
|
+
*/
|
|
267
|
+
const forEachOwner = (each: (position: number, at: number, owner: number) => void): void => plans.forEach(({ table, keys }, t) => {
|
|
247
268
|
for (const key of keys) {
|
|
248
269
|
if (declarationOf(key)?.reference !== "owner") continue;
|
|
249
270
|
const ownerAt = referencesOf(Reflect.get(table.columns, key));
|
|
250
271
|
for (let row = 0; row < table.count; row++) {
|
|
251
272
|
const owner = ownerAt(row);
|
|
252
|
-
if (owner === 0) continue;
|
|
253
|
-
const at = positionOf(owner);
|
|
254
273
|
const position = firsts[t] + row;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
ownerRows.push(at);
|
|
258
|
-
}
|
|
259
|
-
else if (dropped[position] === 0) {
|
|
260
|
-
dropped[position] = 1;
|
|
261
|
-
waiting.push(position);
|
|
262
|
-
}
|
|
274
|
+
// A row that exists stands, whatever owns it.
|
|
275
|
+
if (owner !== 0 && existingAt[position] === 0) each(position, positionOf(owner), owner);
|
|
263
276
|
}
|
|
264
277
|
}
|
|
265
278
|
});
|
|
279
|
+
forEachOwner((position, at, owner) => {
|
|
280
|
+
if (at >= 0 || dropped[position] !== 0 || existing.has(owner)) return;
|
|
281
|
+
dropped[position] = 1;
|
|
282
|
+
waiting.push(position);
|
|
283
|
+
});
|
|
266
284
|
if (waiting.length > 0) {
|
|
267
285
|
const owned = new Map<number, number[]>();
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
286
|
+
forEachOwner((position, at) => {
|
|
287
|
+
if (at < 0) return;
|
|
288
|
+
const list = owned.get(at);
|
|
289
|
+
if (list === undefined) owned.set(at, [position]);
|
|
290
|
+
else list.push(position);
|
|
291
|
+
});
|
|
273
292
|
for (let next = waiting.pop(); next !== undefined; next = waiting.pop()) {
|
|
274
293
|
for (const child of owned.get(next) ?? []) {
|
|
275
|
-
if (dropped[child] !== 0) continue;
|
|
294
|
+
if (dropped[child] !== 0 || existingAt[child] !== 0) continue;
|
|
276
295
|
dropped[child] = 1;
|
|
277
296
|
waiting.push(child);
|
|
278
297
|
}
|
|
279
298
|
}
|
|
280
299
|
}
|
|
300
|
+
// Each row's entity in this world: the one spawned for it, the one existing for it, or 0 for one dropped.
|
|
281
301
|
const spawnedAt = new Float64Array(total);
|
|
302
|
+
/** By table, whether every row of it is spawned: none exists or is dropped. */
|
|
303
|
+
const wholly: boolean[] = [];
|
|
282
304
|
const entities = plans.map(({ table }, t) => {
|
|
305
|
+
const first = firsts[t];
|
|
306
|
+
const spawned = new Float64Array(table.count);
|
|
307
|
+
let standing = 0;
|
|
308
|
+
for (let row = 0; row < table.count; row++) if (existingAt[first + row] !== 0 || dropped[first + row] !== 0) standing++;
|
|
309
|
+
wholly.push(standing === 0);
|
|
310
|
+
if (standing === 0) {
|
|
311
|
+
reserveRun(state.entities, spawned);
|
|
312
|
+
spawnedAt.set(spawned, first);
|
|
313
|
+
return spawned;
|
|
314
|
+
}
|
|
283
315
|
const alive = [];
|
|
284
|
-
for (let row = 0; row < table.count; row++)
|
|
316
|
+
for (let row = 0; row < table.count; row++) {
|
|
317
|
+
const position = first + row;
|
|
318
|
+
if (existingAt[position] !== 0) spawnedAt[position] = spawned[row] = existingAt[position];
|
|
319
|
+
else if (dropped[position] === 0) alive.push(row);
|
|
320
|
+
}
|
|
285
321
|
const reserved = new Float64Array(alive.length);
|
|
286
322
|
reserveRun(state.entities, reserved);
|
|
287
|
-
const spawned = new Float64Array(table.count);
|
|
288
323
|
alive.forEach((row, i) => {
|
|
289
324
|
spawned[row] = reserved[i];
|
|
290
|
-
spawnedAt[
|
|
325
|
+
spawnedAt[first + row] = reserved[i];
|
|
291
326
|
});
|
|
292
327
|
return spawned;
|
|
293
328
|
});
|
|
294
|
-
|
|
329
|
+
/** The entity in this world for one of the tables' or one `existing` maps, or 0 for one that isn't there. */
|
|
330
|
+
const entityFor = (entity: number): number => {
|
|
295
331
|
const at = positionOf(entity);
|
|
296
|
-
return at >= 0
|
|
332
|
+
return at >= 0 ? spawnedAt[at] : existing.get(entity) ?? 0;
|
|
297
333
|
};
|
|
298
|
-
const
|
|
334
|
+
const isSpawned = (entity: number): boolean => entityFor(entity) !== 0;
|
|
335
|
+
const remap: Remap = entityFor;
|
|
299
336
|
let droppedCount = 0;
|
|
300
337
|
plans.forEach((plan, t) => {
|
|
301
338
|
const { table, keys } = plan;
|
|
@@ -305,12 +342,13 @@ export const spawnTables = (state: State, tables: readonly Table[]): Table.Spawn
|
|
|
305
342
|
const target = targetAt(row);
|
|
306
343
|
return target !== 0 && !isSpawned(target);
|
|
307
344
|
});
|
|
308
|
-
let whole =
|
|
309
|
-
for (let row = 0; row < table.count && whole; row++) if (
|
|
345
|
+
let whole = wholly[t];
|
|
346
|
+
if (weak.length > 0) for (let row = 0; row < table.count && whole; row++) if (loses(row)) whole = false;
|
|
310
347
|
if (whole) return spawnRows(state, plan, null, none, entities[t], remap);
|
|
311
348
|
// Rows by the weak references they lose, whose targets aren't spawned: each set of components a batch of its own.
|
|
312
349
|
const groups = new Map<string, { readonly removed: Set<Key>; readonly rows: number[] }>();
|
|
313
350
|
for (let row = 0; row < table.count; row++) {
|
|
351
|
+
if (existingAt[first + row] !== 0) continue;
|
|
314
352
|
if (dropped[first + row] !== 0) {
|
|
315
353
|
droppedCount++;
|
|
316
354
|
continue;
|
|
@@ -69,7 +69,7 @@ const errorOf = (error: unknown): WorkerError =>
|
|
|
69
69
|
|
|
70
70
|
const refuse = (): never => { throw new Error("An observer system issues no commands."); };
|
|
71
71
|
/** An observer system's context has no commands, so these are never called. */
|
|
72
|
-
const noCommands: Commands = { spawn: refuse, despawn: refuse, add: refuse, remove: refuse, spawnTables: refuse };
|
|
72
|
+
const noCommands: Commands = { spawn: refuse, despawn: refuse, despawnWhere: refuse, add: refuse, remove: refuse, spawnTables: refuse };
|
|
73
73
|
const noStamps: number[] = [];
|
|
74
74
|
|
|
75
75
|
export const createThreadObserver = (host: ThreadObserverHost) => {
|