@adobe/ecs 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/implementation/compiler/augmentations.js +8 -1
- package/dist/implementation/compiler/augmentations.js.map +1 -1
- package/dist/implementation/compiler/body-access.d.ts +18 -1
- package/dist/implementation/compiler/body-access.js +104 -15
- package/dist/implementation/compiler/body-access.js.map +1 -1
- package/dist/implementation/compiler/estree.d.ts +5 -2
- package/dist/implementation/compiler/estree.js +8 -5
- package/dist/implementation/compiler/estree.js.map +1 -1
- package/dist/implementation/compiler/inferred.js +5 -2
- package/dist/implementation/compiler/inferred.js.map +1 -1
- package/dist/implementation/compiler/transform.js +5 -4
- package/dist/implementation/compiler/transform.js.map +1 -1
- package/dist/implementation/compiler/type-schemas.d.ts +1 -0
- package/dist/implementation/compiler/type-schemas.js +35 -2
- package/dist/implementation/compiler/type-schemas.js.map +1 -1
- package/dist/implementation/world/activate.d.ts +14 -3
- package/dist/implementation/world/activate.js +28 -10
- package/dist/implementation/world/activate.js.map +1 -1
- package/dist/implementation/world/buffer.d.ts +7 -0
- package/dist/implementation/world/buffer.js +7 -0
- package/dist/implementation/world/buffer.js.map +1 -1
- package/dist/implementation/world/cascade.d.ts +10 -3
- package/dist/implementation/world/cascade.js +48 -23
- package/dist/implementation/world/cascade.js.map +1 -1
- package/dist/implementation/world/commands.js +41 -5
- package/dist/implementation/world/commands.js.map +1 -1
- package/dist/implementation/world/create.js +57 -6
- package/dist/implementation/world/create.js.map +1 -1
- package/dist/implementation/world/execute.d.ts +4 -4
- package/dist/implementation/world/execute.js +228 -93
- package/dist/implementation/world/execute.js.map +1 -1
- package/dist/implementation/world/finalizers.d.ts +10 -0
- package/dist/implementation/world/finalizers.js +60 -10
- package/dist/implementation/world/finalizers.js.map +1 -1
- package/dist/implementation/world/join.js +12 -2
- package/dist/implementation/world/join.js.map +1 -1
- package/dist/implementation/world/program.js +5 -1
- package/dist/implementation/world/program.js.map +1 -1
- package/dist/implementation/world/resource-changes.d.ts +18 -0
- package/dist/implementation/world/resource-changes.js +66 -0
- package/dist/implementation/world/resource-changes.js.map +1 -0
- package/dist/implementation/world/rows.js +21 -16
- package/dist/implementation/world/rows.js.map +1 -1
- package/dist/implementation/world/run-changes.d.ts +38 -0
- package/dist/implementation/world/run-changes.js +138 -0
- package/dist/implementation/world/run-changes.js.map +1 -0
- package/dist/implementation/world/runner.js +16 -12
- package/dist/implementation/world/runner.js.map +1 -1
- package/dist/implementation/world/state.d.ts +15 -0
- package/dist/implementation/world/state.js +22 -1
- package/dist/implementation/world/state.js.map +1 -1
- package/dist/implementation/world/synchronize.js +81 -24
- package/dist/implementation/world/synchronize.js.map +1 -1
- package/dist/implementation/world/tables-read.d.ts +5 -3
- package/dist/implementation/world/tables-read.js +16 -4
- package/dist/implementation/world/tables-read.js.map +1 -1
- package/dist/implementation/world/tables.d.ts +4 -2
- package/dist/implementation/world/tables.js +137 -38
- package/dist/implementation/world/tables.js.map +1 -1
- package/dist/implementation/world/thread-observer.js +1 -1
- package/dist/implementation/world/thread-observer.js.map +1 -1
- package/dist/implementation/world/thread-systems.js +10 -6
- package/dist/implementation/world/thread-systems.js.map +1 -1
- package/dist/implementation/world/up.js +7 -1
- package/dist/implementation/world/up.js.map +1 -1
- package/dist/implementation/world/where.d.ts +8 -0
- package/dist/implementation/world/where.js +40 -11
- package/dist/implementation/world/where.js.map +1 -1
- package/dist/interface/finalizer/body.d.ts +40 -4
- package/dist/interface/finalizer/create.d.ts +8 -6
- package/dist/interface/finalizer/create.js +45 -10
- package/dist/interface/finalizer/create.js.map +1 -1
- package/dist/interface/finalizer/declaration.d.ts +26 -4
- package/dist/interface/indexes/declaration-of.d.ts +17 -0
- package/dist/interface/indexes/declaration-of.js +20 -0
- package/dist/interface/indexes/declaration-of.js.map +1 -0
- package/dist/interface/indexes/index.d.ts +1 -0
- package/dist/interface/indexes/index.js +1 -0
- package/dist/interface/indexes/index.js.map +1 -1
- package/dist/interface/registry/index.d.ts +1 -1
- package/dist/interface/registry/index.js +1 -1
- package/dist/interface/registry/index.js.map +1 -1
- package/dist/interface/registry/names.d.ts +4 -0
- package/dist/interface/registry/names.js +20 -0
- package/dist/interface/registry/names.js.map +1 -1
- package/dist/interface/registry/schedule.d.ts +29 -1
- package/dist/interface/registry/schedule.js +261 -25
- package/dist/interface/registry/schedule.js.map +1 -1
- package/dist/interface/system/access.d.ts +15 -0
- package/dist/interface/system/check.d.ts +7 -0
- package/dist/interface/system/check.js +21 -3
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +31 -5
- package/dist/interface/system/create-resolved.js +2 -1
- package/dist/interface/system/create-resolved.js.map +1 -1
- package/dist/interface/system/create.d.ts +10 -6
- package/dist/interface/system/create.js +5 -4
- package/dist/interface/system/create.js.map +1 -1
- package/dist/interface/system/declaration.d.ts +11 -4
- package/dist/interface/system/describe.d.ts +120 -0
- package/dist/interface/system/describe.js +116 -0
- package/dist/interface/system/describe.js.map +1 -0
- package/dist/interface/system/index.d.ts +1 -0
- package/dist/interface/system/index.js +1 -0
- package/dist/interface/system/index.js.map +1 -1
- package/dist/interface/system/leaf.d.ts +7 -0
- package/dist/interface/system/render.js +2 -1
- package/dist/interface/system/render.js.map +1 -1
- package/dist/interface/system/resolve.d.ts +14 -1
- package/dist/interface/system/resolve.js +50 -1
- package/dist/interface/system/resolve.js.map +1 -1
- package/dist/interface/system/shared-only.d.ts +7 -0
- package/dist/interface/system/shared-only.js +2 -0
- package/dist/interface/system/shared-only.js.map +1 -0
- package/dist/interface/system/tables.d.ts +9 -1
- package/dist/interface/world/table/index.d.ts +3 -2
- package/dist/interface/world/world.d.ts +15 -0
- package/package.json +11 -2
- package/src/guide/01-resources.md +4 -3
- package/src/guide/03-systems.out +1 -1
- package/src/guide/05-scheduling.md +12 -6
- package/src/guide/05-scheduling.out +1 -0
- package/src/guide/05-scheduling.ts +17 -0
- package/src/guide/06-sync-points.md +20 -8
- package/src/guide/06-sync-points.out +3 -2
- package/src/guide/06-sync-points.ts +24 -5
- package/src/guide/10-finalizers.md +5 -0
- package/src/guide/18-identity.out +1 -1
- package/src/guide/guide.test.ts +6 -153
- package/src/implementation/compiler/augmentations.test.ts +28 -1
- package/src/implementation/compiler/augmentations.ts +5 -1
- package/src/implementation/compiler/body-access.ts +102 -12
- package/src/implementation/compiler/estree.ts +9 -6
- package/src/implementation/compiler/inferred.test.ts +1 -1
- package/src/implementation/compiler/inferred.ts +5 -2
- package/src/implementation/compiler/transform.test.ts +41 -21
- package/src/implementation/compiler/transform.ts +5 -3
- package/src/implementation/compiler/type-schemas.ts +28 -2
- package/src/implementation/world/activate.ts +32 -10
- package/src/implementation/world/batches.test.ts +3 -3
- package/src/implementation/world/buffer.ts +7 -0
- package/src/implementation/world/cascade.ts +46 -22
- package/src/implementation/world/commands.ts +37 -5
- package/src/implementation/world/create.test.ts +8 -8
- package/src/implementation/world/create.ts +61 -6
- package/src/implementation/world/defaults.test.ts +24 -1
- package/src/implementation/world/despawn-where.test.ts +310 -0
- package/src/implementation/world/execute.ts +222 -86
- package/src/implementation/world/finalizers.ts +63 -11
- package/src/implementation/world/ids.test.ts +1 -1
- package/src/implementation/world/indexes.test.ts +2 -1
- package/src/implementation/world/join.test.ts +19 -0
- package/src/implementation/world/join.ts +16 -2
- package/src/implementation/world/lifetime.test-helper.ts +1 -1
- package/src/implementation/world/names.test.ts +1 -2
- package/src/implementation/world/passes.test.ts +61 -0
- package/src/implementation/world/perf.md +12 -1
- package/src/implementation/world/program.ts +4 -1
- package/src/implementation/world/resource-changes.test.ts +61 -0
- package/src/implementation/world/resource-changes.ts +67 -0
- package/src/implementation/world/rows.ts +21 -14
- package/src/implementation/world/rules.test.ts +37 -0
- package/src/implementation/world/run-changes.test.ts +88 -0
- package/src/implementation/world/run-changes.ts +160 -0
- package/src/implementation/world/run-results.test.ts +17 -0
- package/src/implementation/world/runner.ts +17 -13
- package/src/implementation/world/schedule.test.ts +42 -1
- package/src/implementation/world/shared.test.ts +2 -2
- package/src/implementation/world/state.ts +27 -0
- package/src/implementation/world/sync-points.test.ts +147 -0
- package/src/implementation/world/synchronize.ts +73 -20
- package/src/implementation/world/tables-read.ts +16 -5
- package/src/implementation/world/tables.test.ts +139 -4
- package/src/implementation/world/tables.ts +118 -35
- package/src/implementation/world/thread-observer.ts +1 -1
- package/src/implementation/world/thread-systems.ts +10 -6
- package/src/implementation/world/up.test.ts +13 -1
- package/src/implementation/world/up.ts +6 -1
- package/src/implementation/world/watch-traits-late.test-helper.ts +14 -0
- package/src/implementation/world/watch-traits.test-helper.ts +10 -0
- package/src/implementation/world/watch-traits.test.ts +62 -0
- package/src/implementation/world/where.ts +38 -10
- package/src/interface/finalizer/body.ts +33 -4
- package/src/interface/finalizer/create.test.ts +6 -1
- package/src/interface/finalizer/create.ts +45 -12
- package/src/interface/finalizer/declaration.ts +31 -4
- package/src/interface/indexes/create.test.ts +10 -0
- package/src/interface/indexes/declaration-of.ts +34 -0
- package/src/interface/indexes/index.ts +1 -0
- package/src/interface/registry/index.ts +1 -1
- package/src/interface/registry/names.ts +19 -0
- package/src/interface/registry/schedule.ts +275 -29
- package/src/interface/rule/rule.type-test.ts +23 -1
- package/src/interface/system/access.ts +16 -0
- package/src/interface/system/check.ts +17 -2
- package/src/interface/system/commands.ts +28 -4
- package/src/interface/system/commands.type-test.ts +31 -0
- package/src/interface/system/create-resolved.ts +2 -1
- package/src/interface/system/create.test.ts +10 -2
- package/src/interface/system/create.ts +9 -5
- package/src/interface/system/declaration.ts +10 -4
- package/src/interface/system/describe.test.ts +98 -0
- package/src/interface/system/describe.ts +122 -0
- package/src/interface/system/index.ts +1 -0
- package/src/interface/system/leaf.ts +7 -0
- package/src/interface/system/render.ts +2 -1
- package/src/interface/system/resolve.test.ts +17 -1
- package/src/interface/system/resolve.ts +60 -2
- package/src/interface/system/shared-only.ts +5 -0
- package/src/interface/system/system.type-test.ts +6 -0
- package/src/interface/system/tables.ts +7 -1
- package/src/interface/world/run.type-test.ts +11 -1
- package/src/interface/world/table/index.ts +3 -2
- package/src/interface/world/world.ts +13 -0
- package/src/perftest/benchmarks.ts +20 -0
- package/src/perftest/chained.ts +108 -0
- package/src/perftest/structural.ts +20 -0
- package/src/perftest/tables.ts +107 -0
|
@@ -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,11 +129,67 @@ 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 alive before the sync point that a `despawnWhere` command selects given the plan so far: those
|
|
151
|
+
* planned, by where they're going, then the rest, by the archetype they're in. Never the resource entity, nor an
|
|
152
|
+
* entity the sync point spawns: `born` holds the first row each archetype gained at it.
|
|
153
|
+
*/
|
|
154
|
+
const despawnWhere = (state: State, buffer: CommandBuffer, command: number, within: boolean, born: ReadonlyMap<number, number>): void => {
|
|
155
|
+
const { plan, archetypes } = state;
|
|
156
|
+
const { ids } = buffer;
|
|
157
|
+
const start = buffer.starts[command];
|
|
158
|
+
const middle = start + buffer.entities[command];
|
|
159
|
+
const last = endOf(buffer, command);
|
|
160
|
+
const where = ids[last - 1] === despawnWhereHolds ? buffer.values[last - 1] : null;
|
|
161
|
+
const holds = typeof where === "function" ? where : null;
|
|
162
|
+
const end = holds === null ? last : last - 1;
|
|
163
|
+
const { row } = state.entities;
|
|
164
|
+
for (let entry = 0; entry < plan.count; entry++) {
|
|
165
|
+
const from = plan.from[entry];
|
|
166
|
+
if (plan.despawned[entry] === 1 || plan.entity[entry] === state.resourceEntity || from < 0) continue;
|
|
167
|
+
const first = born.get(from);
|
|
168
|
+
if (first !== undefined && row[indexOf(plan.entity[entry])] >= first) continue;
|
|
169
|
+
const to = archetypes[plan.to[entry]];
|
|
170
|
+
if (matches(within && plan.to[entry] !== plan.from[entry] ? closedOf(state, to) : to, ids, start, middle, end, holds)) plan.despawned[entry] = 1;
|
|
171
|
+
}
|
|
172
|
+
const { slots } = plan;
|
|
173
|
+
const count = archetypes.length;
|
|
174
|
+
for (let id = 0; id < count; id++) {
|
|
175
|
+
const archetype = archetypes[id];
|
|
176
|
+
if (archetype.resource || archetype.count === 0 || !matches(archetype, ids, start, middle, end, holds)) continue;
|
|
177
|
+
ensurePlanCapacity(state, plan.count + archetype.count);
|
|
178
|
+
const { entities } = archetype;
|
|
179
|
+
const alive = born.get(id) ?? archetype.count;
|
|
180
|
+
for (let at = 0; at < alive; at++) {
|
|
181
|
+
const entity = entities[at];
|
|
182
|
+
const index = indexOf(entity);
|
|
183
|
+
if (slots[index] < 0) plan.despawned[addPlanned(state, entity, index, id, id)] = 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
130
188
|
/**
|
|
131
189
|
* Plan: the net change for each entity, in issue order. Commands against dead entities do nothing. Reference values
|
|
132
190
|
* written by commands go to `written`, to check once despawns are known.
|
|
133
191
|
*/
|
|
134
|
-
const planOf = (state: State, queued: readonly Queued[], written: Written): void => {
|
|
192
|
+
const planOf = (state: State, queued: readonly Queued[], written: Written, born: ReadonlyMap<number, number>): void => {
|
|
135
193
|
const { plan, structure } = state;
|
|
136
194
|
if (plan.slots.length < state.entities.length) plan.slots = new Int32Array(Math.max(state.entities.length, plan.slots.length * 2)).fill(-1);
|
|
137
195
|
const { slots } = plan;
|
|
@@ -146,6 +204,11 @@ const planOf = (state: State, queued: readonly Queued[], written: Written): void
|
|
|
146
204
|
ensurePlanCapacity(state, plan.count + buffer.count);
|
|
147
205
|
for (let command = 0; command < buffer.count; command++) {
|
|
148
206
|
const kind = kinds[command];
|
|
207
|
+
if (kind === despawnWhereCommand) {
|
|
208
|
+
despawnWhere(state, buffer, command, within, born);
|
|
209
|
+
ensurePlanCapacity(state, plan.count + buffer.count - command);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
149
212
|
const entity = entities[command];
|
|
150
213
|
if (guarded && entity === state.resourceEntity) continue;
|
|
151
214
|
const index = indexOf(entity);
|
|
@@ -631,7 +694,9 @@ const noneQueued: readonly Queued[] = [];
|
|
|
631
694
|
export const syncPending = (state: State, queued: readonly Queued[] = state.queued): boolean =>
|
|
632
695
|
anyCommands(queued) || reserved(state.entities) || state.carried !== null || state.structure.rederive
|
|
633
696
|
// Failures for its run, as a gone thread's stops it never finalized are.
|
|
634
|
-
|| state.unplaced.length > 0
|
|
697
|
+
|| state.unplaced.length > 0
|
|
698
|
+
// A watched resource written, which its changes finalizers see at a sync point.
|
|
699
|
+
|| (state.resourcesSeen.size > 0 && resourcesWritten(state));
|
|
635
700
|
|
|
636
701
|
/**
|
|
637
702
|
* The first half of a sync point: commits batch spawns, plans every command, cascades references and closes the plan
|
|
@@ -683,27 +748,14 @@ const preparePending = (state: State, queued: readonly Queued[], isolated: boole
|
|
|
683
748
|
if (!isolated) plan.spawned += commitBatches(state, tick, born, deriving ? close : null);
|
|
684
749
|
plan.spawned += commitStaged(state, queued, tick, born, deriving ? close : null);
|
|
685
750
|
const written: Written = [];
|
|
686
|
-
planOf(state, queued, written);
|
|
751
|
+
planOf(state, queued, written, born);
|
|
687
752
|
if (structure.rederive && !isolated) rederive(state);
|
|
688
753
|
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
754
|
let index: IndexChanges | null = null;
|
|
690
755
|
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
|
-
}
|
|
756
|
+
linkBorn(state, born, written, tick);
|
|
699
757
|
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));
|
|
758
|
+
index = indexChangesOf(state, queued, entry => plan.from[entry] < 0 ? null : state.archetypes[plan.from[entry]]);
|
|
707
759
|
}
|
|
708
760
|
// References change only here: each component written to is stamped, and each holder journaled, as are spawns.
|
|
709
761
|
for (let k = 1; k < written.length; k += 3) state.referencesWritten[written[k]] = tick;
|
|
@@ -741,6 +793,7 @@ export const applySync = (state: State): readonly Thrown[] => {
|
|
|
741
793
|
let errors = nothingThrown;
|
|
742
794
|
try {
|
|
743
795
|
if (state.unplaced.length > 0 && !isolated) errors = state.unplaced.splice(0);
|
|
796
|
+
if (state.recording !== null && !isolated) recordPlan(state, state.recording, queued, born);
|
|
744
797
|
if (state.finalizers.length > 0 && !isolated) {
|
|
745
798
|
const thrown = runFinalizers(state, queued, born, tick);
|
|
746
799
|
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,33 @@ 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>; update?: boolean } = { tables: [] };
|
|
50
50
|
const spawnGiven = System.create(({ commands }) => {
|
|
51
|
-
spawnedBy.spawned =
|
|
51
|
+
spawnedBy.spawned = commands.spawnTables(given.tables, { resources: given.resources, existing: given.existing, update: given.update });
|
|
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>, update?: boolean): Promise<Table.Spawned> => {
|
|
54
54
|
given.tables = tables;
|
|
55
|
-
|
|
55
|
+
given.existing = existing;
|
|
56
|
+
given.update = update;
|
|
57
|
+
try {
|
|
58
|
+
await world.run(spawnGiven);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
given.existing = undefined;
|
|
62
|
+
given.update = undefined;
|
|
63
|
+
}
|
|
56
64
|
return spawnedBy.spawned;
|
|
57
65
|
};
|
|
58
66
|
|
|
67
|
+
/** Spawns `labels` in `world`, giving each label's entity. */
|
|
68
|
+
const spawnLabels = async (world: ReturnType<typeof inline>, labels: readonly string[]): Promise<ReadonlyMap<string, Entity>> => {
|
|
69
|
+
const made = new Map<string, Entity>();
|
|
70
|
+
await world.run(System.create(({ commands }) => {
|
|
71
|
+
for (const text of labels) made.set(text, commands.spawn([label])({ [label]: text }));
|
|
72
|
+
}));
|
|
73
|
+
return made;
|
|
74
|
+
};
|
|
75
|
+
|
|
59
76
|
const values = System.create({ readonly: true, components: { with: [label], optional: [position, parent, target, link, friends, tags, buddy] } }, row => {
|
|
60
77
|
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
78
|
});
|
|
@@ -198,6 +215,84 @@ describe("tables", () => {
|
|
|
198
215
|
expect(read.find(entry => entry.label === "pointing")).toMatchObject({ buddy: Entity.none, link: { to: Entity.none, weight: 1 }, friends: [Entity.none, inside] });
|
|
199
216
|
});
|
|
200
217
|
|
|
218
|
+
it("spawn no row existing maps, giving the entity it maps to, which every entity value naming it names", async () => {
|
|
219
|
+
await using world = inline();
|
|
220
|
+
let steel = Entity.none;
|
|
221
|
+
await world.run(System.create(({ commands }) => {
|
|
222
|
+
steel = commands.spawn([label])({ [label]: "steel" });
|
|
223
|
+
commands.spawn([label, parent, target, buddy, link, friends])({ [label]: "body", [parent]: steel, [target]: steel, [buddy]: steel, [link]: { to: steel, weight: 2 }, [friends]: [steel] });
|
|
224
|
+
}));
|
|
225
|
+
const { archetypes } = await world.run(tablesOf([label, parent, target, buddy, link, friends]));
|
|
226
|
+
await using other = inline();
|
|
227
|
+
const existingSteel = (await spawnLabels(other, ["steel in other"])).get("steel in other") ?? Entity.none;
|
|
228
|
+
const { entities, dropped } = await spawnTables(other, archetypes, new Map([[steel, existingSteel]]));
|
|
229
|
+
expect(dropped).toBe(0);
|
|
230
|
+
expect(entities.flatMap(list => [...list])).toContain(existingSteel);
|
|
231
|
+
const read = await readAll(other);
|
|
232
|
+
// Steel isn't spawned again: the body's every entity value names the one already there.
|
|
233
|
+
expect(read.map(entry => entry.label)).toEqual(["body", "steel in other"]);
|
|
234
|
+
expect(read.find(entry => entry.label === "body")).toMatchObject({ parent: existingSteel, target: existingSteel, buddy: existingSteel, link: { to: existingSteel, weight: 2 }, friends: [existingSteel] });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("let an entity existing maps stand for one outside the tables, so what it owns or points at is kept", async () => {
|
|
238
|
+
await using world = inline();
|
|
239
|
+
let steel = Entity.none;
|
|
240
|
+
await world.run(System.create(({ commands }) => {
|
|
241
|
+
steel = commands.spawn([pinned])();
|
|
242
|
+
commands.spawn([label, parent, target])({ [label]: "owned", [parent]: steel, [target]: steel });
|
|
243
|
+
}));
|
|
244
|
+
const { archetypes } = await world.run(tablesOf([label, parent, target]));
|
|
245
|
+
await using other = inline();
|
|
246
|
+
const existingSteel = (await spawnLabels(other, ["steel"])).get("steel") ?? Entity.none;
|
|
247
|
+
const { dropped } = await spawnTables(other, archetypes, new Map([[steel, existingSteel]]));
|
|
248
|
+
expect(dropped).toBe(0);
|
|
249
|
+
expect((await readAll(other)).find(entry => entry.label === "owned")).toMatchObject({ parent: existingSteel, target: existingSteel });
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("write a row existing maps onto its entity with update, as an add would, entity values rewritten as a spawned row's", async () => {
|
|
253
|
+
await using world = inline();
|
|
254
|
+
let steel = Entity.none;
|
|
255
|
+
await world.run(System.create(({ commands }) => {
|
|
256
|
+
const inside = commands.spawn([label])({ [label]: "inside" });
|
|
257
|
+
const outside = commands.spawn([pinned])();
|
|
258
|
+
steel = commands.spawn([label, position, buddy, target, link, team], { [team]: "red" })({ [label]: "steel", [position]: [1, 2, 3], [buddy]: inside, [target]: outside, [link]: { to: inside, weight: 4 } });
|
|
259
|
+
}));
|
|
260
|
+
const { archetypes } = await world.run(tablesOf([label, position, buddy, target, link, team]));
|
|
261
|
+
await using other = inline();
|
|
262
|
+
let mine = Entity.none, kept = Entity.none;
|
|
263
|
+
await other.run(System.create(({ commands }) => {
|
|
264
|
+
kept = commands.spawn([label])({ [label]: "kept" });
|
|
265
|
+
mine = commands.spawn([label, hidden, target, team], { [team]: "blue" })({ [label]: "mine", [target]: kept });
|
|
266
|
+
}));
|
|
267
|
+
const changed: string[] = [];
|
|
268
|
+
const watch = System.create({ components: { with: [label], changed: [label] } }, row => { changed.push(row[label]); });
|
|
269
|
+
await other.run(watch);
|
|
270
|
+
changed.length = 0;
|
|
271
|
+
const { entities, dropped } = await spawnTables(other, archetypes, new Map([[steel, mine]]), true);
|
|
272
|
+
expect([dropped, entities.flatMap(list => [...list]).includes(mine)]).toEqual([0, true]);
|
|
273
|
+
const read = await readAll(other);
|
|
274
|
+
const inside = read.find(entry => entry.label === "inside")?.entity;
|
|
275
|
+
// The weak reference to an entity outside the tables is left out, so the entity's own stands.
|
|
276
|
+
expect(read.find(entry => entry.entity === mine)).toMatchObject({ label: "steel", position: [1, 2, 3], buddy: inside, target: kept, link: { to: inside, weight: 4 } });
|
|
277
|
+
const teams = await other.run(System.create({ readonly: true, tables: { components: [team, hidden] } }, ({ tables }) => tables.archetypes.map(table => [table.columns[team], table.columns[hidden] !== undefined])));
|
|
278
|
+
expect(teams).toEqual([[{ kind: "value", value: "red" }, true]]);
|
|
279
|
+
await other.run(watch);
|
|
280
|
+
expect(changed.sort()).toEqual(["inside", "steel"]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("refuse existing mapping to an entity that isn't alive, spawning nothing", async () => {
|
|
284
|
+
await using world = inline();
|
|
285
|
+
const gone = (await spawnLabels(world, ["gone"])).get("gone") ?? Entity.none;
|
|
286
|
+
await world.run(System.create(({ commands }) => { commands.despawn(gone); }));
|
|
287
|
+
const { archetypes } = await world.run(tablesOf([label]));
|
|
288
|
+
await using source = inline();
|
|
289
|
+
const steel = (await spawnLabels(source, ["steel"])).get("steel") ?? Entity.none;
|
|
290
|
+
const { archetypes: tables } = await source.run(tablesOf([label]));
|
|
291
|
+
expect(archetypes).toEqual([]);
|
|
292
|
+
await expect(spawnTables(world, tables, new Map([[steel, gone]]))).rejects.toThrow(new RegExp(`as entity ${gone}, which isn't alive`));
|
|
293
|
+
expect(await readAll(world)).toEqual([]);
|
|
294
|
+
});
|
|
295
|
+
|
|
201
296
|
it("set the resources they're given at the sync point, beside the entities", async () => {
|
|
202
297
|
await using world = createWorld({ [workers]: 0, [score]: 7 });
|
|
203
298
|
given.tables = [];
|
|
@@ -249,6 +344,46 @@ describe("tables", () => {
|
|
|
249
344
|
expect(await readAll(world)).toEqual([]);
|
|
250
345
|
});
|
|
251
346
|
|
|
347
|
+
it("keep to the archetypes holding where's shared values, each required as with requires it", async () => {
|
|
348
|
+
await using world = inline();
|
|
349
|
+
await world.run(System.create(({ commands }) => {
|
|
350
|
+
commands.spawn([team, label], { [team]: "red" })({ [label]: "red" });
|
|
351
|
+
commands.spawn([team, label, position], { [team]: "red" })({ [label]: "red placed", [position]: [1, 1, 1] });
|
|
352
|
+
commands.spawn([team, label], { [team]: "blue" })({ [label]: "blue" });
|
|
353
|
+
commands.spawn([label])({ [label]: "none" });
|
|
354
|
+
}));
|
|
355
|
+
const red = await world.run(System.create({ readonly: true, tables: { components: [label], where: { [team]: "red" } } }, ({ tables }): Tables => tables));
|
|
356
|
+
expect(red.archetypes.map(table => table.columns[label]).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))))
|
|
357
|
+
.toEqual([{ kind: "values", values: ["red placed"] }, { kind: "values", values: ["red"] }]);
|
|
358
|
+
const placed = await world.run(System.create({ readonly: true, tables: { components: [label], with: [position], where: { [team]: "red" } } }, ({ tables }): Tables => tables));
|
|
359
|
+
expect(placed.archetypes.map(table => table.columns[label])).toEqual([{ kind: "values", values: ["red placed"] }]);
|
|
360
|
+
// One system's tables follow its archetypes as they come and go.
|
|
361
|
+
const blue = System.create({ readonly: true, tables: { components: [label], where: { [team]: "blue" } } }, ({ tables }): Tables => tables);
|
|
362
|
+
expect((await world.run(blue)).archetypes.map(table => table.count)).toEqual([1]);
|
|
363
|
+
await world.run(System.create(({ commands }) => {
|
|
364
|
+
commands.spawn([team, label, position], { [team]: "blue" })({ [label]: "blue placed", [position]: [2, 2, 2] });
|
|
365
|
+
}));
|
|
366
|
+
expect((await world.run(blue)).archetypes.map(table => table.count)).toEqual([1, 1]);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("give no table for a shared value no archetype holds", async () => {
|
|
370
|
+
await using world = inline();
|
|
371
|
+
await world.run(System.create(({ commands }) => { commands.spawn([team, label], { [team]: "red" })({ [label]: "red" }); }));
|
|
372
|
+
const blue = System.create({ readonly: true, tables: { components: [label], where: { [team]: "blue" } } }, ({ tables }): Tables => tables);
|
|
373
|
+
expect((await world.run(blue)).archetypes).toEqual([]);
|
|
374
|
+
await world.run(System.create(({ commands }) => { commands.spawn([team, label], { [team]: "blue" })({ [label]: "blue" }); }));
|
|
375
|
+
expect((await world.run(blue)).archetypes.map(table => table.columns[label])).toEqual([{ kind: "values", values: ["blue"] }]);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("take only values of shared components in where", () => {
|
|
379
|
+
// @ts-expect-error: where names only shared components.
|
|
380
|
+
expect(() => System.create({ readonly: true, tables: { components: [label], where: { [label]: "a" } } }, () => {})).toThrow(/tables' where names "tablesTest\/label", which isn't a shared component/);
|
|
381
|
+
// @ts-expect-error: tables' where takes values, not predicates.
|
|
382
|
+
expect(() => System.create({ readonly: true, tables: { components: [label], where: { [team]: (value: string) => value === "red" } } }, () => {})).toThrow(/tables' where gives "tablesTest\/team" a predicate/);
|
|
383
|
+
// @ts-expect-error: a value of the component's type.
|
|
384
|
+
expect(() => System.create({ readonly: true, tables: { components: [label], where: { [team]: "green" } } }, () => {})).toThrow(/isn't one of its type's values/);
|
|
385
|
+
});
|
|
386
|
+
|
|
252
387
|
it("are read only by a readonly system without components", () => {
|
|
253
388
|
// @ts-expect-error: tables are read only by a readonly system, which the declaration's type says too.
|
|
254
389
|
expect(() => System.create({ tables: { components: [label] } }, () => {})).toThrow(/reads tables, which only a readonly system reads/);
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
-
import type
|
|
2
|
+
import { viewsOf, type Layout, type Path } from '../../interface/registry/layout.js';
|
|
3
3
|
import { declarationOf, labelOf } from '../../interface/registry/names.js';
|
|
4
4
|
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
|
-
import { commandIdOf, type Key, type State } from './state.js';
|
|
11
|
+
import { commandIdOf, storedShared, type Key, type State } from './state.js';
|
|
12
|
+
import { addCommand, pushCommand, pushComponent, type CommandBuffer } from './buffer.js';
|
|
12
13
|
|
|
13
14
|
/** Whether a value of `schema` may hold an entity anywhere in it. */
|
|
14
15
|
const holdsEntity = (schema: Schema): boolean => {
|
|
@@ -153,6 +154,12 @@ const placesOf = (plans: readonly Planned[]): { readonly entities: Float64Array;
|
|
|
153
154
|
/** A reference column's entity value in each row: the f64 at the row's start, a reference's whole value. */
|
|
154
155
|
const referencesOf = (column: Table.Column): ((row: number) => number) => {
|
|
155
156
|
if (column.kind !== "bytes") return () => 0;
|
|
157
|
+
const { bytes } = column;
|
|
158
|
+
if (bytes.byteOffset % 8 === 0 && column.stride % 8 === 0) {
|
|
159
|
+
const view = new Float64Array(bytes.buffer, bytes.byteOffset, bytes.byteLength >> 3);
|
|
160
|
+
const words = column.stride >> 3;
|
|
161
|
+
return row => view[row * words];
|
|
162
|
+
}
|
|
156
163
|
const view = new DataView(column.bytes.buffer, column.bytes.byteOffset, column.bytes.byteLength);
|
|
157
164
|
const { stride } = column;
|
|
158
165
|
return row => view.getFloat64(row * stride, true);
|
|
@@ -203,16 +210,64 @@ const spawnRows = (state: State, { table, keys, ids, shared }: Planned, rows: re
|
|
|
203
210
|
claimRows(batch, entities);
|
|
204
211
|
};
|
|
205
212
|
|
|
213
|
+
/** A linear value read out of a table's bytes, copied to row 0 here first; grown for larger layouts. */
|
|
214
|
+
let scratch = viewsOf(new ArrayBuffer(256), 0, 256);
|
|
215
|
+
|
|
216
|
+
/** A table's value of a component in a row: its column's one value, its value there, or one read out of its bytes. */
|
|
217
|
+
const valueIn = (layout: Layout, column: Table.Column, row: number): unknown => {
|
|
218
|
+
if (column.kind === "value") return column.value;
|
|
219
|
+
if (column.kind === "values") return column.values[row];
|
|
220
|
+
if (layout.kind !== "linear") return undefined;
|
|
221
|
+
const { stride } = layout;
|
|
222
|
+
if (stride > scratch.u8.length) scratch = viewsOf(new ArrayBuffer(stride * 2), 0, stride * 2);
|
|
223
|
+
scratch.u8.set(column.bytes.subarray(row * stride, (row + 1) * stride));
|
|
224
|
+
return layout.read(scratch, 0);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Queues an add of each of its table's components onto the entity standing for a row `existing` maps, its entity
|
|
229
|
+
* values remapped, but a reference whose target isn't there.
|
|
230
|
+
*/
|
|
231
|
+
const updateRows = (state: State, { table, keys, ids }: Planned, rows: readonly number[], standing: Float64Array, buffer: CommandBuffer, remap: Remap): void => {
|
|
232
|
+
const columns: Table.Column[] = keys.map(key => Reflect.get(table.columns, key));
|
|
233
|
+
const schemas = keys.map(key => declarationOf(key)?.type);
|
|
234
|
+
const references = keys.map(key => declarationOf(key)?.reference !== undefined);
|
|
235
|
+
for (const row of rows) {
|
|
236
|
+
pushCommand(buffer, addCommand, standing[row]);
|
|
237
|
+
for (let k = 0; k < keys.length; k++) {
|
|
238
|
+
const id = ids[k];
|
|
239
|
+
const schema = schemas[k];
|
|
240
|
+
let value = valueIn(state.layouts[id], columns[k], row);
|
|
241
|
+
if (references[k]) {
|
|
242
|
+
const to = remap(Number(value));
|
|
243
|
+
if (to === 0 && value !== 0) continue;
|
|
244
|
+
value = to;
|
|
245
|
+
}
|
|
246
|
+
else if (schema !== undefined && holdsEntity(schema)) value = remapValue(schema, value, remap);
|
|
247
|
+
pushComponent(buffer, id, state.shared[id] === undefined ? value : storedShared(state, id, value));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/** Throws for an entity `existing` maps to that isn't alive in this world. */
|
|
253
|
+
const checkExisting = (state: State, existing: ReadonlyMap<number, number>): void => {
|
|
254
|
+
for (const [source, target] of existing) {
|
|
255
|
+
if (locate(state.entities, target) < 0) throw new Error(`commands.spawnTables: entity ${source} is given as existing as entity ${target}, which isn't alive.`);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
206
259
|
/**
|
|
207
260
|
* Spawns an entity for each row of `tables` into the batches of their components, as references would leave them had
|
|
208
261
|
* the entities outside the tables despawned: a row owned, directly or through others, by an entity outside them isn't
|
|
209
262
|
* 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.
|
|
263
|
+
* value naming an entity spawned names the one spawned for it. A row whose entity `existing` maps isn't spawned: the
|
|
264
|
+
* entity it maps to stands for it, as it does for an entity outside the tables that `existing` maps.
|
|
211
265
|
*/
|
|
212
|
-
export const spawnTables = (state: State, tables: readonly Table[]): Table.Spawned => {
|
|
266
|
+
export const spawnTables = (state: State, tables: readonly Table[], existing: ReadonlyMap<number, number> = new Map(), updates: CommandBuffer | null = null): Table.Spawned => {
|
|
213
267
|
const plans = tables.map(table => planOf(state, table));
|
|
214
|
-
|
|
215
|
-
|
|
268
|
+
checkExisting(state, existing);
|
|
269
|
+
// Entities are mapped only where a value of the tables may hold one, or a row is one that exists.
|
|
270
|
+
const remapping = existing.size > 0 || plans.some(({ keys }) => keys.some(key => {
|
|
216
271
|
const schema = declarationOf(key)?.type;
|
|
217
272
|
return schema !== undefined && holdsEntity(schema);
|
|
218
273
|
}));
|
|
@@ -237,65 +292,87 @@ export const spawnTables = (state: State, tables: readonly Table[]): Table.Spawn
|
|
|
237
292
|
firsts.push(total);
|
|
238
293
|
total += table.count;
|
|
239
294
|
}
|
|
295
|
+
// The entity standing for each row `existing` maps, 0 for a row spawned.
|
|
296
|
+
const existingAt = new Float64Array(total);
|
|
297
|
+
if (existing.size > 0) plans.forEach(({ table }, t) => {
|
|
298
|
+
for (let row = 0; row < table.count; row++) existingAt[firsts[t] + row] = existing.get(table.entities[row]) ?? 0;
|
|
299
|
+
});
|
|
240
300
|
// A row owned by an entity outside the tables isn't spawned, nor anything it owns, as despawning its owner would.
|
|
241
301
|
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
302
|
const waiting: number[] = [];
|
|
246
|
-
|
|
303
|
+
/**
|
|
304
|
+
* Calls `each` with each owner reference of a row that doesn't exist: the owned row's position, its owner's among
|
|
305
|
+
* the tables' rows or -1 for an owner outside them, and the owner.
|
|
306
|
+
*/
|
|
307
|
+
const forEachOwner = (each: (position: number, at: number, owner: number) => void): void => plans.forEach(({ table, keys }, t) => {
|
|
247
308
|
for (const key of keys) {
|
|
248
309
|
if (declarationOf(key)?.reference !== "owner") continue;
|
|
249
310
|
const ownerAt = referencesOf(Reflect.get(table.columns, key));
|
|
250
311
|
for (let row = 0; row < table.count; row++) {
|
|
251
312
|
const owner = ownerAt(row);
|
|
252
|
-
if (owner === 0) continue;
|
|
253
|
-
const at = positionOf(owner);
|
|
254
313
|
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
|
-
}
|
|
314
|
+
// A row that exists stands, whatever owns it.
|
|
315
|
+
if (owner !== 0 && existingAt[position] === 0) each(position, positionOf(owner), owner);
|
|
263
316
|
}
|
|
264
317
|
}
|
|
265
318
|
});
|
|
319
|
+
forEachOwner((position, at, owner) => {
|
|
320
|
+
if (at >= 0 || dropped[position] !== 0 || existing.has(owner)) return;
|
|
321
|
+
dropped[position] = 1;
|
|
322
|
+
waiting.push(position);
|
|
323
|
+
});
|
|
266
324
|
if (waiting.length > 0) {
|
|
267
325
|
const owned = new Map<number, number[]>();
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
326
|
+
forEachOwner((position, at) => {
|
|
327
|
+
if (at < 0) return;
|
|
328
|
+
const list = owned.get(at);
|
|
329
|
+
if (list === undefined) owned.set(at, [position]);
|
|
330
|
+
else list.push(position);
|
|
331
|
+
});
|
|
273
332
|
for (let next = waiting.pop(); next !== undefined; next = waiting.pop()) {
|
|
274
333
|
for (const child of owned.get(next) ?? []) {
|
|
275
|
-
if (dropped[child] !== 0) continue;
|
|
334
|
+
if (dropped[child] !== 0 || existingAt[child] !== 0) continue;
|
|
276
335
|
dropped[child] = 1;
|
|
277
336
|
waiting.push(child);
|
|
278
337
|
}
|
|
279
338
|
}
|
|
280
339
|
}
|
|
340
|
+
// Each row's entity in this world: the one spawned for it, the one existing for it, or 0 for one dropped.
|
|
281
341
|
const spawnedAt = new Float64Array(total);
|
|
342
|
+
/** By table, whether every row of it is spawned: none exists or is dropped. */
|
|
343
|
+
const wholly: boolean[] = [];
|
|
282
344
|
const entities = plans.map(({ table }, t) => {
|
|
345
|
+
const first = firsts[t];
|
|
346
|
+
const spawned = new Float64Array(table.count);
|
|
347
|
+
let standing = 0;
|
|
348
|
+
for (let row = 0; row < table.count; row++) if (existingAt[first + row] !== 0 || dropped[first + row] !== 0) standing++;
|
|
349
|
+
wholly.push(standing === 0);
|
|
350
|
+
if (standing === 0) {
|
|
351
|
+
reserveRun(state.entities, spawned);
|
|
352
|
+
spawnedAt.set(spawned, first);
|
|
353
|
+
return spawned;
|
|
354
|
+
}
|
|
283
355
|
const alive = [];
|
|
284
|
-
for (let row = 0; row < table.count; row++)
|
|
356
|
+
for (let row = 0; row < table.count; row++) {
|
|
357
|
+
const position = first + row;
|
|
358
|
+
if (existingAt[position] !== 0) spawnedAt[position] = spawned[row] = existingAt[position];
|
|
359
|
+
else if (dropped[position] === 0) alive.push(row);
|
|
360
|
+
}
|
|
285
361
|
const reserved = new Float64Array(alive.length);
|
|
286
362
|
reserveRun(state.entities, reserved);
|
|
287
|
-
const spawned = new Float64Array(table.count);
|
|
288
363
|
alive.forEach((row, i) => {
|
|
289
364
|
spawned[row] = reserved[i];
|
|
290
|
-
spawnedAt[
|
|
365
|
+
spawnedAt[first + row] = reserved[i];
|
|
291
366
|
});
|
|
292
367
|
return spawned;
|
|
293
368
|
});
|
|
294
|
-
|
|
369
|
+
/** The entity in this world for one of the tables' or one `existing` maps, or 0 for one that isn't there. */
|
|
370
|
+
const entityFor = (entity: number): number => {
|
|
295
371
|
const at = positionOf(entity);
|
|
296
|
-
return at >= 0
|
|
372
|
+
return at >= 0 ? spawnedAt[at] : existing.get(entity) ?? 0;
|
|
297
373
|
};
|
|
298
|
-
const
|
|
374
|
+
const isSpawned = (entity: number): boolean => entityFor(entity) !== 0;
|
|
375
|
+
const remap: Remap = entityFor;
|
|
299
376
|
let droppedCount = 0;
|
|
300
377
|
plans.forEach((plan, t) => {
|
|
301
378
|
const { table, keys } = plan;
|
|
@@ -305,12 +382,18 @@ export const spawnTables = (state: State, tables: readonly Table[]): Table.Spawn
|
|
|
305
382
|
const target = targetAt(row);
|
|
306
383
|
return target !== 0 && !isSpawned(target);
|
|
307
384
|
});
|
|
308
|
-
|
|
309
|
-
|
|
385
|
+
if (updates !== null && !wholly[t]) {
|
|
386
|
+
const mapped: number[] = [];
|
|
387
|
+
for (let row = 0; row < table.count; row++) if (existingAt[first + row] !== 0) mapped.push(row);
|
|
388
|
+
if (mapped.length > 0) updateRows(state, plan, mapped, entities[t], updates, remap);
|
|
389
|
+
}
|
|
390
|
+
let whole = wholly[t];
|
|
391
|
+
if (weak.length > 0) for (let row = 0; row < table.count && whole; row++) if (loses(row)) whole = false;
|
|
310
392
|
if (whole) return spawnRows(state, plan, null, none, entities[t], remap);
|
|
311
393
|
// Rows by the weak references they lose, whose targets aren't spawned: each set of components a batch of its own.
|
|
312
394
|
const groups = new Map<string, { readonly removed: Set<Key>; readonly rows: number[] }>();
|
|
313
395
|
for (let row = 0; row < table.count; row++) {
|
|
396
|
+
if (existingAt[first + row] !== 0) continue;
|
|
314
397
|
if (dropped[first + row] !== 0) {
|
|
315
398
|
droppedCount++;
|
|
316
399
|
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) => {
|