@adobe/ecs 0.2.1 → 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/README.md +2 -2
- 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/describe.d.ts +1 -1
- package/dist/implementation/compiler/describe.js +3 -3
- package/dist/implementation/compiler/describe.js.map +1 -1
- package/dist/implementation/compiler/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/signature.js +1 -1
- package/dist/implementation/compiler/transform.js +6 -5
- 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 +86 -2
- 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/entities.d.ts +2 -0
- package/dist/implementation/world/entities.js +6 -0
- package/dist/implementation/world/entities.js.map +1 -1
- package/dist/implementation/world/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 +22 -4
- package/dist/implementation/world/join.js.map +1 -1
- package/dist/implementation/world/register.js +2 -2
- 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.d.ts +1 -1
- package/dist/implementation/world/rows.js +23 -13
- 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 +19 -15
- package/dist/implementation/world/runner.js.map +1 -1
- package/dist/implementation/world/sharing.js +1 -1
- package/dist/implementation/world/staging.d.ts +4 -0
- package/dist/implementation/world/staging.js +15 -3
- package/dist/implementation/world/staging.js.map +1 -1
- package/dist/implementation/world/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 +9 -0
- package/dist/implementation/world/tables-read.js +72 -0
- package/dist/implementation/world/tables-read.js.map +1 -0
- package/dist/implementation/world/tables.d.ts +10 -0
- package/dist/implementation/world/tables.js +401 -0
- package/dist/implementation/world/tables.js.map +1 -0
- package/dist/implementation/world/thread-observer.js +1 -1
- package/dist/implementation/world/thread-observer.js.map +1 -1
- package/dist/implementation/world/thread-systems.js +11 -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/index.d.ts +8 -28
- package/dist/index.js +4 -9
- package/dist/index.js.map +1 -1
- package/dist/interface/component/declarations.d.ts +4 -0
- package/dist/interface/component/declarations.js +8 -0
- package/dist/interface/component/declarations.js.map +1 -0
- package/dist/interface/component/declared.d.ts +12 -0
- package/dist/interface/component/declared.js +2 -0
- package/dist/interface/component/declared.js.map +1 -0
- package/dist/interface/component/index.d.ts +2 -0
- package/dist/interface/component/index.js +1 -1
- package/dist/interface/component/index.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/names.d.ts +2 -0
- package/dist/interface/registry/names.js +9 -0
- package/dist/interface/registry/names.js.map +1 -1
- package/dist/interface/registry/schedule.d.ts +31 -4
- package/dist/interface/registry/schedule.js +185 -24
- package/dist/interface/registry/schedule.js.map +1 -1
- package/dist/interface/resource/declarations.d.ts +4 -0
- package/dist/interface/resource/declarations.js +8 -0
- package/dist/interface/resource/declarations.js.map +1 -0
- package/dist/interface/resource/index.d.ts +2 -0
- package/dist/interface/resource/index.js +1 -1
- package/dist/interface/resource/index.js.map +1 -1
- package/dist/interface/system/access.d.ts +15 -0
- package/dist/interface/system/body.d.ts +11 -6
- package/dist/interface/system/check.d.ts +8 -1
- package/dist/interface/system/check.js +42 -12
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +37 -0
- 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 +31 -15
- package/dist/interface/system/create.js +8 -7
- package/dist/interface/system/create.js.map +1 -1
- package/dist/interface/system/declaration.d.ts +9 -2
- 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 +28 -0
- package/dist/interface/system/tables.js +2 -0
- package/dist/interface/system/tables.js.map +1 -0
- package/dist/interface/world/index.d.ts +1 -1
- package/dist/interface/world/table/index.d.ts +27 -0
- package/dist/interface/world/table/index.js +3 -0
- package/dist/interface/world/table/index.js.map +1 -0
- package/dist/interface/world/table/table.d.ts +11 -0
- package/dist/interface/world/table/table.js +2 -0
- package/dist/interface/world/table/table.js.map +1 -0
- package/dist/interface/world/world.d.ts +25 -10
- package/dist/world.d.ts +27 -0
- package/dist/world.js +27 -0
- package/dist/world.js.map +1 -0
- package/package.json +11 -2
- package/src/exports.test.ts +1 -1
- package/src/guide/00-keys.ts +1 -1
- package/src/guide/01-resources.md +1 -1
- package/src/guide/01-resources.ts +1 -1
- package/src/guide/02-entities.ts +2 -2
- package/src/guide/03-systems.md +2 -2
- package/src/guide/03-systems.out +1 -1
- package/src/guide/03-systems.ts +3 -3
- package/src/guide/04-ticks.md +1 -1
- package/src/guide/04-ticks.ts +3 -3
- package/src/guide/05-scheduling.ts +1 -1
- package/src/guide/06-sync-points.md +18 -7
- package/src/guide/06-sync-points.out +1 -2
- package/src/guide/06-sync-points.ts +10 -6
- package/src/guide/07-change-filters.ts +2 -2
- package/src/guide/08-input.ts +1 -1
- package/src/guide/09-rules.ts +1 -1
- package/src/guide/11-traits.ts +3 -3
- package/src/guide/12-lookups.ts +2 -2
- package/src/guide/13-indexes.ts +4 -4
- package/src/guide/14-kernels.ts +1 -1
- package/src/guide/15-reductions.ts +1 -1
- package/src/guide/16-gpu.ts +1 -1
- package/src/guide/17-hierarchies.ts +1 -1
- package/src/guide/18-identity.out +1 -1
- package/src/guide/19-threads.md +4 -2
- package/src/guide/19-threads.ts +2 -2
- package/src/guide/20-shared.ts +3 -3
- package/src/guide/21-dom.md +2 -2
- package/src/guide/21-dom.ts +1 -1
- package/src/guide/22-canvas.md +1 -1
- package/src/guide/22-canvas.ts +1 -1
- package/src/guide/CLAUDE.md +1 -1
- package/src/guide/guide.test.ts +9 -154
- package/src/implementation/compiler/body-access.ts +102 -12
- package/src/implementation/compiler/describe.test-helper.ts +1 -1
- package/src/implementation/compiler/describe.test.ts +1 -1
- package/src/implementation/compiler/describe.ts +3 -3
- package/src/implementation/compiler/estree.ts +9 -6
- package/src/implementation/compiler/hygiene.test.ts +1 -1
- package/src/implementation/compiler/inferred.test.ts +1 -1
- package/src/implementation/compiler/inferred.ts +4 -2
- package/src/implementation/compiler/plugin.test.ts +2 -2
- package/src/implementation/compiler/signature.ts +1 -1
- package/src/implementation/compiler/transform.test.ts +43 -23
- package/src/implementation/compiler/transform.ts +6 -4
- 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/change-blocks.test.ts +1 -1
- package/src/implementation/world/commands.ts +76 -2
- package/src/implementation/world/create.test.ts +8 -8
- package/src/implementation/world/create.ts +57 -2
- package/src/implementation/world/defaults.test.ts +1 -1
- package/src/implementation/world/despawn-where.test.ts +287 -0
- package/src/implementation/world/entities.ts +6 -0
- package/src/implementation/world/execute.ts +125 -78
- package/src/implementation/world/finalizers.test.ts +1 -1
- package/src/implementation/world/finalizers.ts +27 -4
- package/src/implementation/world/ids.test.ts +1 -1
- package/src/implementation/world/index-changed.test.ts +2 -2
- package/src/implementation/world/index-joined.test.ts +5 -5
- package/src/implementation/world/index-order.test.ts +1 -1
- package/src/implementation/world/indexes.test.ts +2 -1
- package/src/implementation/world/join.test.ts +23 -4
- package/src/implementation/world/join.ts +26 -4
- package/src/implementation/world/joined-observer.test.ts +1 -1
- package/src/implementation/world/joining-worker.fixture.ts +1 -1
- package/src/implementation/world/joining.fixture.ts +1 -1
- package/src/implementation/world/joining.test-helper.ts +1 -1
- package/src/implementation/world/kernel-spawns.test.ts +1 -1
- package/src/implementation/world/names.test.ts +1 -2
- package/src/implementation/world/observe-factories.test.ts +2 -2
- package/src/implementation/world/observe-ticks-cases.test.ts +1 -1
- package/src/implementation/world/observe.test-helper.ts +1 -1
- package/src/implementation/world/observer-contained-worker.fixture.ts +1 -1
- package/src/implementation/world/observer-contained.test-helper.ts +1 -1
- package/src/implementation/world/observer-contained.test.ts +2 -2
- package/src/implementation/world/observer-joined.test-helper.ts +4 -4
- package/src/implementation/world/observer-joined.test.ts +23 -15
- package/src/implementation/world/observer.browser.test.ts +2 -2
- package/src/implementation/world/observer.test.ts +8 -8
- package/src/implementation/world/optional-workers.fixture.ts +1 -1
- package/src/implementation/world/optional.test.ts +7 -7
- package/src/implementation/world/perf.md +3 -1
- package/src/implementation/world/reclaim.test.ts +3 -3
- package/src/implementation/world/register.ts +2 -2
- package/src/implementation/world/remote-objects.test-helper.ts +1 -1
- package/src/implementation/world/resource-changes.test.ts +61 -0
- package/src/implementation/world/resource-changes.ts +67 -0
- package/src/implementation/world/rows.ts +24 -13
- 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 +10 -10
- package/src/implementation/world/runner.ts +20 -16
- package/src/implementation/world/shared-gpu.browser.test.ts +2 -2
- package/src/implementation/world/shared-kernels.test.ts +2 -2
- package/src/implementation/world/shared-workers.fixture.ts +2 -2
- package/src/implementation/world/shared.test.ts +10 -10
- package/src/implementation/world/sharing.ts +1 -1
- package/src/implementation/world/staging.ts +16 -3
- 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 +70 -0
- package/src/implementation/world/tables.test.ts +369 -0
- package/src/implementation/world/tables.ts +368 -0
- package/src/implementation/world/tails.test.ts +1 -1
- package/src/implementation/world/target.test.ts +2 -2
- package/src/implementation/world/thread-observer.ts +1 -1
- package/src/implementation/world/thread-systems.test-helper.ts +3 -3
- package/src/implementation/world/thread-systems.ts +11 -6
- package/src/implementation/world/up.test.ts +13 -1
- package/src/implementation/world/up.ts +6 -1
- package/src/implementation/world/where.test.ts +4 -4
- package/src/implementation/world/where.ts +38 -10
- package/src/implementation/world/workers.fixture.ts +2 -2
- package/src/index.ts +4 -29
- package/src/interface/component/declarations.ts +11 -0
- package/src/interface/component/declared.ts +9 -0
- package/src/interface/component/index.ts +2 -0
- 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/index.type-test.ts +2 -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/indexes/index.type-test.ts +1 -1
- package/src/interface/registry/names.ts +8 -0
- package/src/interface/registry/schedule.ts +200 -27
- package/src/interface/resource/declarations.ts +11 -0
- package/src/interface/resource/index.ts +2 -0
- package/src/interface/system/access.ts +16 -0
- package/src/interface/system/body.ts +7 -5
- package/src/interface/system/check.ts +37 -10
- package/src/interface/system/commands.ts +33 -0
- 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 +21 -13
- package/src/interface/system/create.ts +24 -15
- package/src/interface/system/declaration.ts +9 -2
- 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/lists.type-test.ts +1 -1
- package/src/interface/system/optional.type-test.ts +1 -1
- 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/shared.type-test.ts +1 -1
- package/src/interface/system/system.type-test.ts +26 -20
- package/src/interface/system/tables.ts +27 -0
- package/src/interface/system/up.type-test.ts +3 -3
- package/src/interface/world/index.ts +2 -2
- package/src/interface/world/run.type-test.ts +14 -4
- package/src/interface/world/table/index.ts +20 -0
- package/src/interface/world/table/table.ts +12 -0
- package/src/interface/world/world.ts +23 -10
- package/src/observe/entity.ts +2 -2
- package/src/observe/resource.ts +2 -2
- package/src/observe/select.ts +4 -4
- package/src/observe/world-state.ts +3 -3
- package/src/perftest/benchmarks.ts +19 -1
- package/src/perftest/indexes.ts +3 -3
- package/src/perftest/loop.ts +1 -1
- package/src/perftest/observer.ts +4 -4
- package/src/perftest/reads.ts +2 -2
- package/src/perftest/remote.ts +5 -5
- package/src/perftest/strings.ts +1 -1
- package/src/perftest/structural.ts +20 -0
- package/src/perftest/suite.ts +1 -1
- package/src/perftest/tables.ts +107 -0
- package/src/world.test.ts +2 -2
- package/src/world.ts +32 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import type { Layout, Path } from '../../interface/registry/layout.js';
|
|
3
|
+
import { declarationOf, labelOf } from '../../interface/registry/names.js';
|
|
4
|
+
import { isEntity } from '../../interface/registry/schemas.js';
|
|
5
|
+
import type { Schema } from '../../interface/schema/schema.js';
|
|
6
|
+
import type { Table } from '../../interface/world/table/table.js';
|
|
7
|
+
import { batchOf } from './batches.js';
|
|
8
|
+
import { indexOf, locate, reserveRun } from './entities.js';
|
|
9
|
+
import { internShared } from './shared.js';
|
|
10
|
+
import { claimRows, openRows, writerOf, type Column as BatchColumn } from './staging.js';
|
|
11
|
+
import { commandIdOf, type Key, type State } from './state.js';
|
|
12
|
+
|
|
13
|
+
/** Whether a value of `schema` may hold an entity anywhere in it. */
|
|
14
|
+
const holdsEntity = (schema: Schema): boolean => {
|
|
15
|
+
if (isEntity(schema)) return true;
|
|
16
|
+
if ("anyOf" in schema) return schema.anyOf.some(holdsEntity);
|
|
17
|
+
if ("allOf" in schema) return schema.allOf.some(holdsEntity);
|
|
18
|
+
if (!("type" in schema)) return false;
|
|
19
|
+
switch (schema.type) {
|
|
20
|
+
case "array": case "set": return holdsEntity(schema.items);
|
|
21
|
+
case "map": return holdsEntity(schema.keys) || holdsEntity(schema.values);
|
|
22
|
+
case "object": return Object.values(schema.properties).some(holdsEntity);
|
|
23
|
+
default: return false;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** The schema at `path` inside `schema`, through struct fields and tuple items. */
|
|
28
|
+
const schemaAt = (schema: Schema, path: Path): Schema | undefined => {
|
|
29
|
+
let at: Schema | undefined = schema;
|
|
30
|
+
for (const step of path) {
|
|
31
|
+
if (at === undefined || !("type" in at)) return undefined;
|
|
32
|
+
if (at.type === "object" && typeof step === "string") at = at.properties[step];
|
|
33
|
+
else if (at.type === "array" && typeof step === "number") at = at.items;
|
|
34
|
+
else return undefined;
|
|
35
|
+
}
|
|
36
|
+
return at;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** The f64 leaves of a linear layout holding entities: where, in f64 elements, each row's is. */
|
|
40
|
+
const entityLeavesOf = (layout: Layout, schema: Schema | undefined): readonly { readonly unitStride: number; readonly unitOffset: number }[] => {
|
|
41
|
+
if (layout.kind !== "linear" || schema === undefined) return [];
|
|
42
|
+
return layout.leaves.filter(leaf => {
|
|
43
|
+
const leafSchema = schemaAt(schema, leaf.path);
|
|
44
|
+
return leaf.view === "f64" && leafSchema !== undefined && isEntity(leafSchema);
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** The entity each entity of the tables given is spawned as, and what a value naming none of them becomes. */
|
|
49
|
+
type Remap = (entity: number) => number;
|
|
50
|
+
|
|
51
|
+
/** `value` of `schema` with each entity in it replaced by `remap`'s. */
|
|
52
|
+
const remapValue = (schema: Schema, value: unknown, remap: Remap): unknown => {
|
|
53
|
+
if (isEntity(schema)) return typeof value === "number" ? remap(value) : value;
|
|
54
|
+
if (!holdsEntity(schema)) return value;
|
|
55
|
+
if ("anyOf" in schema) {
|
|
56
|
+
for (const member of schema.anyOf) {
|
|
57
|
+
if (isEntity(member) && typeof value === "number") return remap(value);
|
|
58
|
+
if (!("type" in member) || !holdsEntity(member)) continue;
|
|
59
|
+
if ((member.type === "array" && Array.isArray(value)) || (member.type === "set" && value instanceof Set) || (member.type === "map" && value instanceof Map)
|
|
60
|
+
|| (member.type === "object" && typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Set) && !(value instanceof Map))) return remapValue(member, value, remap);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
if ("allOf" in schema) return schema.allOf.reduce<unknown>((next, member) => remapValue(member, next, remap), value);
|
|
65
|
+
if (!("type" in schema)) return value;
|
|
66
|
+
switch (schema.type) {
|
|
67
|
+
case "array": return Array.isArray(value) ? value.map(item => remapValue(schema.items, item, remap)) : value;
|
|
68
|
+
case "set": return value instanceof Set ? new Set(Array.from(value, item => remapValue(schema.items, item, remap))) : value;
|
|
69
|
+
case "map": return value instanceof Map ? new Map(Array.from(value, ([key, item]) => [remapValue(schema.keys, key, remap), remapValue(schema.values, item, remap)])) : value;
|
|
70
|
+
case "object": {
|
|
71
|
+
if (typeof value !== "object" || value === null) return value;
|
|
72
|
+
const copy: { [name: string]: unknown } = {};
|
|
73
|
+
for (const name of Object.keys(value)) {
|
|
74
|
+
const field = schema.properties[name];
|
|
75
|
+
const item: unknown = Reflect.get(value, name);
|
|
76
|
+
copy[name] = field === undefined ? item : remapValue(field, item, remap);
|
|
77
|
+
}
|
|
78
|
+
return copy;
|
|
79
|
+
}
|
|
80
|
+
default: return value;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** A table's components with their ids, the columns given for them, and their layouts. */
|
|
85
|
+
type Planned = {
|
|
86
|
+
readonly table: Table;
|
|
87
|
+
readonly keys: readonly Key[];
|
|
88
|
+
readonly ids: readonly number[];
|
|
89
|
+
readonly shared: (number | undefined)[];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** Throws, naming the component, for a column that doesn't fit its component's layout or its table's count. */
|
|
93
|
+
const checkColumn = (state: State, key: Key, id: number, column: Table.Column, count: number): void => {
|
|
94
|
+
const layout = state.layouts[id];
|
|
95
|
+
const fail = (message: string): never => { throw new Error(`commands.spawnTables: "${labelOf(key)}" ${message}.`); };
|
|
96
|
+
if (state.shared[id] !== undefined) {
|
|
97
|
+
if (column.kind !== "value") fail("is a shared component, so its column is one value");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (layout.kind === "constant" || column.kind === "value") return;
|
|
101
|
+
if (layout.kind === "linear") {
|
|
102
|
+
if (column.kind !== "bytes") fail("is stored as bytes, so its column gives bytes");
|
|
103
|
+
else if (column.stride !== layout.stride) fail(`has a stride of ${layout.stride} bytes in this world, not ${column.stride}`);
|
|
104
|
+
else if (column.bytes.length !== count * layout.stride) fail(`gives ${column.bytes.length} bytes for ${count} rows of ${layout.stride}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (column.kind !== "values") fail("is stored as values, so its column gives values");
|
|
108
|
+
else if (column.values.length !== count) fail(`gives ${column.values.length} values for ${count} rows`);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** The plan of each table: its keys, their command ids, and the numbers of its shared values. Throws for one that won't spawn. */
|
|
112
|
+
const planOf = (state: State, table: Table): Planned => {
|
|
113
|
+
const { count, entities, columns } = table;
|
|
114
|
+
if (!Number.isInteger(count) || count < 0 || entities.length !== count) throw new Error(`commands.spawnTables: a table of ${count} rows gives ${entities.length} entities.`);
|
|
115
|
+
const keys = Object.getOwnPropertySymbols(columns).filter(key => Reflect.get(columns, key) !== undefined);
|
|
116
|
+
const ids = keys.map(key => commandIdOf(state, key));
|
|
117
|
+
const shared: (number | undefined)[] = [];
|
|
118
|
+
keys.forEach((key, k) => {
|
|
119
|
+
const column: Table.Column = Reflect.get(columns, key);
|
|
120
|
+
checkColumn(state, key, ids[k], column, count);
|
|
121
|
+
const values = state.shared[ids[k]];
|
|
122
|
+
if (values !== undefined && column.kind === "value") {
|
|
123
|
+
const number = internShared(values, column.value);
|
|
124
|
+
if (number < 0) throw new Error(`commands.spawnTables: "${labelOf(key)}", a shared component, isn't given one of its type's values: ${labelOf(column.value)}.`);
|
|
125
|
+
shared[ids[k]] = number;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
return { table, keys, ids, shared };
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Where each entity of the tables is, by its index: the entity itself, to tell it from another of that index, and its
|
|
133
|
+
* position among all the tables' rows. Throws for an entity two rows name.
|
|
134
|
+
*/
|
|
135
|
+
const placesOf = (plans: readonly Planned[]): { readonly entities: Float64Array; readonly positions: Int32Array } => {
|
|
136
|
+
let largest = 0;
|
|
137
|
+
for (const { table } of plans) for (let row = 0; row < table.count; row++) largest = Math.max(largest, indexOf(table.entities[row]));
|
|
138
|
+
const entities = new Float64Array(largest + 1).fill(-1);
|
|
139
|
+
const positions = new Int32Array(largest + 1);
|
|
140
|
+
let position = 0;
|
|
141
|
+
for (const { table } of plans) {
|
|
142
|
+
for (let row = 0; row < table.count; row++) {
|
|
143
|
+
const entity = table.entities[row];
|
|
144
|
+
const index = indexOf(entity);
|
|
145
|
+
if (entities[index] === entity) throw new Error(`commands.spawnTables: entity ${entity} is in two rows.`);
|
|
146
|
+
entities[index] = entity;
|
|
147
|
+
positions[index] = position++;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { entities, positions };
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** A reference column's entity value in each row: the f64 at the row's start, a reference's whole value. */
|
|
154
|
+
const referencesOf = (column: Table.Column): ((row: number) => number) => {
|
|
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
|
+
}
|
|
162
|
+
const view = new DataView(column.bytes.buffer, column.bytes.byteOffset, column.bytes.byteLength);
|
|
163
|
+
const { stride } = column;
|
|
164
|
+
return row => view.getFloat64(row * stride, true);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/** Copies a table's `rows`, or all of them, of one column into rows `start` on of a batch's column, remapping entities. */
|
|
168
|
+
const copyColumn = (target: BatchColumn, column: Table.Column, schema: Schema | undefined, rows: readonly number[] | null, count: number, start: number, remap: Remap | null): void => {
|
|
169
|
+
const rowAt = (i: number): number => rows === null ? i : rows[i];
|
|
170
|
+
const remapped = remap !== null && schema !== undefined && holdsEntity(schema);
|
|
171
|
+
if (column.kind === "value") {
|
|
172
|
+
const write = writerOf(target, column.value);
|
|
173
|
+
const value = remapped ? remapValue(schema, column.value, remap) : column.value;
|
|
174
|
+
for (let i = 0; i < count; i++) write(start + i, value);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (column.kind === "bytes") {
|
|
178
|
+
const { stride } = target;
|
|
179
|
+
if (rows === null) target.views.u8.set(column.bytes, start * stride);
|
|
180
|
+
else for (let i = 0; i < count; i++) target.views.u8.set(column.bytes.subarray(rows[i] * stride, (rows[i] + 1) * stride), (start + i) * stride);
|
|
181
|
+
if (!remapped) return;
|
|
182
|
+
const { f64 } = target.views;
|
|
183
|
+
for (const leaf of entityLeavesOf(target.layout, schema)) {
|
|
184
|
+
for (let row = start, end = start + count; row < end; row++) {
|
|
185
|
+
const index = row * leaf.unitStride + leaf.unitOffset;
|
|
186
|
+
f64[index] = remap(f64[index]);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (target.remote !== null && column.values.some(value => value !== undefined && value !== null)) throw new Error(target.remote);
|
|
192
|
+
for (let i = 0; i < count; i++) {
|
|
193
|
+
const value = column.values[rowAt(i)];
|
|
194
|
+
target.values[start + i] = remapped ? remapValue(schema, value, remap) : value;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/** Spawns `rows` of a plan's table, or all of them, with the components it holds but `removed`, as `entities`. */
|
|
199
|
+
const spawnRows = (state: State, { table, keys, ids, shared }: Planned, rows: readonly number[] | null, removed: ReadonlySet<Key>, entities: Float64Array, remap: Remap | null): void => {
|
|
200
|
+
const kept = keys.flatMap((key, k) => removed.has(key) ? [] : [k]);
|
|
201
|
+
const batch = batchOf(state, kept.map(k => ids[k]), shared.length === 0 ? undefined : shared);
|
|
202
|
+
const start = openRows(batch, entities.length);
|
|
203
|
+
for (const k of kept) {
|
|
204
|
+
const target = batch.columns.find(column => column.id === ids[k]);
|
|
205
|
+
if (target === undefined || target.layout.kind === "constant") continue;
|
|
206
|
+
const column: Table.Column = Reflect.get(table.columns, keys[k]);
|
|
207
|
+
copyColumn(target, column, declarationOf(keys[k])?.type, rows, entities.length, start, remap);
|
|
208
|
+
}
|
|
209
|
+
claimRows(batch, entities);
|
|
210
|
+
};
|
|
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
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Spawns an entity for each row of `tables` into the batches of their components, as references would leave them had
|
|
221
|
+
* the entities outside the tables despawned: a row owned, directly or through others, by an entity outside them isn't
|
|
222
|
+
* spawned; a weak reference to one is removed; and any other entity value naming one is `Entity.none`. Every entity
|
|
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.
|
|
225
|
+
*/
|
|
226
|
+
export const spawnTables = (state: State, tables: readonly Table[], existing: ReadonlyMap<number, number> = new Map()): Table.Spawned => {
|
|
227
|
+
const plans = tables.map(table => planOf(state, table));
|
|
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 => {
|
|
231
|
+
const schema = declarationOf(key)?.type;
|
|
232
|
+
return schema !== undefined && holdsEntity(schema);
|
|
233
|
+
}));
|
|
234
|
+
const none: ReadonlySet<Key> = new Set();
|
|
235
|
+
if (!remapping) {
|
|
236
|
+
const entities = plans.map(({ table }) => {
|
|
237
|
+
const spawned = new Float64Array(table.count);
|
|
238
|
+
reserveRun(state.entities, spawned);
|
|
239
|
+
return spawned;
|
|
240
|
+
});
|
|
241
|
+
plans.forEach((plan, t) => spawnRows(state, plan, null, none, entities[t], null));
|
|
242
|
+
return { entities, dropped: 0 };
|
|
243
|
+
}
|
|
244
|
+
const places = placesOf(plans);
|
|
245
|
+
const positionOf = (entity: number): number => {
|
|
246
|
+
const index = indexOf(entity);
|
|
247
|
+
return entity !== 0 && index < places.entities.length && places.entities[index] === entity ? places.positions[index] : -1;
|
|
248
|
+
};
|
|
249
|
+
const firsts: number[] = [];
|
|
250
|
+
let total = 0;
|
|
251
|
+
for (const { table } of plans) {
|
|
252
|
+
firsts.push(total);
|
|
253
|
+
total += table.count;
|
|
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
|
+
});
|
|
260
|
+
// A row owned by an entity outside the tables isn't spawned, nor anything it owns, as despawning its owner would.
|
|
261
|
+
const dropped = new Uint8Array(total);
|
|
262
|
+
const waiting: number[] = [];
|
|
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) => {
|
|
268
|
+
for (const key of keys) {
|
|
269
|
+
if (declarationOf(key)?.reference !== "owner") continue;
|
|
270
|
+
const ownerAt = referencesOf(Reflect.get(table.columns, key));
|
|
271
|
+
for (let row = 0; row < table.count; row++) {
|
|
272
|
+
const owner = ownerAt(row);
|
|
273
|
+
const position = firsts[t] + row;
|
|
274
|
+
// A row that exists stands, whatever owns it.
|
|
275
|
+
if (owner !== 0 && existingAt[position] === 0) each(position, positionOf(owner), owner);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
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
|
+
});
|
|
284
|
+
if (waiting.length > 0) {
|
|
285
|
+
const owned = new Map<number, number[]>();
|
|
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
|
+
});
|
|
292
|
+
for (let next = waiting.pop(); next !== undefined; next = waiting.pop()) {
|
|
293
|
+
for (const child of owned.get(next) ?? []) {
|
|
294
|
+
if (dropped[child] !== 0 || existingAt[child] !== 0) continue;
|
|
295
|
+
dropped[child] = 1;
|
|
296
|
+
waiting.push(child);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// Each row's entity in this world: the one spawned for it, the one existing for it, or 0 for one dropped.
|
|
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[] = [];
|
|
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
|
+
}
|
|
315
|
+
const alive = [];
|
|
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
|
+
}
|
|
321
|
+
const reserved = new Float64Array(alive.length);
|
|
322
|
+
reserveRun(state.entities, reserved);
|
|
323
|
+
alive.forEach((row, i) => {
|
|
324
|
+
spawned[row] = reserved[i];
|
|
325
|
+
spawnedAt[first + row] = reserved[i];
|
|
326
|
+
});
|
|
327
|
+
return spawned;
|
|
328
|
+
});
|
|
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 => {
|
|
331
|
+
const at = positionOf(entity);
|
|
332
|
+
return at >= 0 ? spawnedAt[at] : existing.get(entity) ?? 0;
|
|
333
|
+
};
|
|
334
|
+
const isSpawned = (entity: number): boolean => entityFor(entity) !== 0;
|
|
335
|
+
const remap: Remap = entityFor;
|
|
336
|
+
let droppedCount = 0;
|
|
337
|
+
plans.forEach((plan, t) => {
|
|
338
|
+
const { table, keys } = plan;
|
|
339
|
+
const first = firsts[t];
|
|
340
|
+
const weak = keys.filter(key => declarationOf(key)?.reference === "weak").map(key => ({ key, targetAt: referencesOf(Reflect.get(table.columns, key)) }));
|
|
341
|
+
const loses = (row: number): boolean => weak.some(({ targetAt }) => {
|
|
342
|
+
const target = targetAt(row);
|
|
343
|
+
return target !== 0 && !isSpawned(target);
|
|
344
|
+
});
|
|
345
|
+
let whole = wholly[t];
|
|
346
|
+
if (weak.length > 0) for (let row = 0; row < table.count && whole; row++) if (loses(row)) whole = false;
|
|
347
|
+
if (whole) return spawnRows(state, plan, null, none, entities[t], remap);
|
|
348
|
+
// Rows by the weak references they lose, whose targets aren't spawned: each set of components a batch of its own.
|
|
349
|
+
const groups = new Map<string, { readonly removed: Set<Key>; readonly rows: number[] }>();
|
|
350
|
+
for (let row = 0; row < table.count; row++) {
|
|
351
|
+
if (existingAt[first + row] !== 0) continue;
|
|
352
|
+
if (dropped[first + row] !== 0) {
|
|
353
|
+
droppedCount++;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const removed = weak.filter(({ targetAt }) => {
|
|
357
|
+
const target = targetAt(row);
|
|
358
|
+
return target !== 0 && !isSpawned(target);
|
|
359
|
+
}).map(({ key }) => key);
|
|
360
|
+
const signature = removed.map(key => weak.findIndex(entry => entry.key === key)).join(",");
|
|
361
|
+
let group = groups.get(signature);
|
|
362
|
+
if (group === undefined) groups.set(signature, group = { removed: new Set(removed), rows: [] });
|
|
363
|
+
group.rows.push(row);
|
|
364
|
+
}
|
|
365
|
+
for (const { removed, rows } of groups.values()) spawnRows(state, plan, rows, removed, Float64Array.from(rows, row => entities[t][row]), remap);
|
|
366
|
+
});
|
|
367
|
+
return { entities, dropped: droppedCount };
|
|
368
|
+
};
|
|
@@ -75,7 +75,7 @@ describe("tails", () => {
|
|
|
75
75
|
const { world, remote } = await joined({ loop: driver }, { [always]: true });
|
|
76
76
|
await world.run(seed, { count: 10 });
|
|
77
77
|
const counters: number[] = [];
|
|
78
|
-
const values = System.create({
|
|
78
|
+
const values = System.create({ readonly: true }, ({ resources }) => { counters.push(resources[counter]); });
|
|
79
79
|
world.add(ticked);
|
|
80
80
|
world.add(values);
|
|
81
81
|
await until(Observe.resource(world, counter), value => value >= 5);
|
|
@@ -24,8 +24,8 @@ const remove = System.create({ target: "entity", components: { with: [tagged] }
|
|
|
24
24
|
});
|
|
25
25
|
let amounts: number[] = [];
|
|
26
26
|
let changed: number[] = [];
|
|
27
|
-
const readAmounts = System.create({
|
|
28
|
-
const watchChanged = System.create({
|
|
27
|
+
const readAmounts = System.create({ readonly: true, components: { with: [amount] } }, row => { amounts.push(row[amount]); });
|
|
28
|
+
const watchChanged = System.create({ readonly: true, components: { with: [amount], changed: [amount] } }, row => { changed.push(row[amount]); });
|
|
29
29
|
|
|
30
30
|
const inline = () => createWorld({ [workers]: 0 });
|
|
31
31
|
const amountsOf = async (world: ReturnType<typeof inline>): Promise<number[]> => {
|
|
@@ -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 };
|
|
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) => {
|
|
@@ -172,7 +172,7 @@ const reading: { entities: readonly Entity[]; objects: readonly { readonly [name
|
|
|
172
172
|
|
|
173
173
|
/** Each entity's objects of the thread, as data, a note by its text: none for an entity that's dead or has no uid. */
|
|
174
174
|
const readObjects = System.create({
|
|
175
|
-
|
|
175
|
+
readonly: true,
|
|
176
176
|
lookup: { ids: { with: [uid] }, captions: { with: [caption] }, notes: { with: [note] } },
|
|
177
177
|
}, ({ lookup }) => {
|
|
178
178
|
reading.objects = reading.entities.map(entity => {
|
|
@@ -322,9 +322,9 @@ export const paintReader = System.create(({ resources }) => () => resources[pain
|
|
|
322
322
|
/** A composite of systems on the thread returning values, which no run resolves to. */
|
|
323
323
|
export const paintsTogether = System.create([paintReader, paintLengthThere]);
|
|
324
324
|
/** An observer system on the thread, as it reads the thread's paint: what a run of it resolves to. */
|
|
325
|
-
export const paintObserved = System.create({
|
|
325
|
+
export const paintObserved = System.create({ readonly: true }, ({ resources }) => resources[paint].text);
|
|
326
326
|
/** An observer system on the thread, throwing there. */
|
|
327
|
-
export const paintObservedFails = System.create({
|
|
327
|
+
export const paintObservedFails = System.create({ readonly: true }, ({ resources }) => {
|
|
328
328
|
if (resources[paint].text !== "never") throw new Error("threadSystemsTest observer failure there");
|
|
329
329
|
return 0;
|
|
330
330
|
});
|
|
@@ -279,6 +279,8 @@ export const createThreadSystems = (host: ThreadSystemsHost) => {
|
|
|
279
279
|
return typeof preset === "object" && preset !== null ? presetSpawnerOf(components, preset) : spawnerOf(components);
|
|
280
280
|
},
|
|
281
281
|
despawn: entity => beginCommand(writer, despawnCommand, entity),
|
|
282
|
+
spawnTables: () => { throw new Error("commands.spawnTables runs on the world's thread: run the system there."); },
|
|
283
|
+
despawnWhere: () => { throw new Error("commands.despawnWhere runs on the world's thread: run the system there."); },
|
|
282
284
|
add: (entity: number, components: symbol | readonly symbol[], values?: unknown) => {
|
|
283
285
|
if (typeof components === "symbol") {
|
|
284
286
|
const key = givenKeyOf(components);
|
|
@@ -989,18 +991,19 @@ export const createThreadRunner = (host: RunnerHost, leaf: Leaf, records: Readon
|
|
|
989
991
|
const row = bindRow(archetype);
|
|
990
992
|
const subject = row.value;
|
|
991
993
|
const input = runInput;
|
|
994
|
+
const { body } = system;
|
|
992
995
|
const filters = filtersOf(archetype);
|
|
993
996
|
if (since < 0 || filters.length === 0) {
|
|
994
997
|
for (let index = 0; index < count; index++) {
|
|
995
998
|
row.index = index;
|
|
996
|
-
|
|
999
|
+
body(subject, value, input);
|
|
997
1000
|
}
|
|
998
1001
|
return;
|
|
999
1002
|
}
|
|
1000
1003
|
forEachPassingRange(filters, since, 0, count, (start, end) => {
|
|
1001
1004
|
for (let index = start; index < end; index++) {
|
|
1002
1005
|
row.index = index;
|
|
1003
|
-
|
|
1006
|
+
body(subject, value, input);
|
|
1004
1007
|
}
|
|
1005
1008
|
});
|
|
1006
1009
|
};
|
|
@@ -1102,18 +1105,19 @@ export const createThreadRunner = (host: RunnerHost, leaf: Leaf, records: Readon
|
|
|
1102
1105
|
const row = bindRow(archetype);
|
|
1103
1106
|
const subject = row.value;
|
|
1104
1107
|
const input = runInput;
|
|
1108
|
+
const { body } = system;
|
|
1105
1109
|
const filters = filtersOf(archetype);
|
|
1106
1110
|
if (since < 0 || filters.length === 0) {
|
|
1107
1111
|
for (let index = start; index < end && !halted; index++) {
|
|
1108
1112
|
row.index = index;
|
|
1109
|
-
|
|
1113
|
+
body(subject, tailValue, input);
|
|
1110
1114
|
}
|
|
1111
1115
|
return;
|
|
1112
1116
|
}
|
|
1113
1117
|
forEachPassingRange(filters, since, start, end, (first, last) => {
|
|
1114
1118
|
for (let index = first; index < last && !halted; index++) {
|
|
1115
1119
|
row.index = index;
|
|
1116
|
-
|
|
1120
|
+
body(subject, tailValue, input);
|
|
1117
1121
|
}
|
|
1118
1122
|
});
|
|
1119
1123
|
};
|
|
@@ -1232,6 +1236,7 @@ export const createThreadRunner = (host: RunnerHost, leaf: Leaf, records: Readon
|
|
|
1232
1236
|
const copied = (storages: readonly (ThreadStorage | undefined)[]): ThreadArchetype =>
|
|
1233
1237
|
({ id: -1, entities, storageById: storages, record: -1, version: -1, mirrored: null, components: [], resource: false, reclaimed: false, generation: 0 });
|
|
1234
1238
|
const ownedInput = runInput;
|
|
1239
|
+
const { body } = system;
|
|
1235
1240
|
try {
|
|
1236
1241
|
if (optionalNumbers.length === 0 && sharedReads.length === 0) {
|
|
1237
1242
|
const row = bindRow(copied(storageById));
|
|
@@ -1239,7 +1244,7 @@ export const createThreadRunner = (host: RunnerHost, leaf: Leaf, records: Readon
|
|
|
1239
1244
|
for (let index = first; index < rows && !halted; index++) {
|
|
1240
1245
|
if (skipped !== null && skipped.has(archetypeIds[index])) continue;
|
|
1241
1246
|
row.index = index;
|
|
1242
|
-
|
|
1247
|
+
body(subject, tailValue, ownedInput);
|
|
1243
1248
|
}
|
|
1244
1249
|
}
|
|
1245
1250
|
else {
|
|
@@ -1274,7 +1279,7 @@ export const createThreadRunner = (host: RunnerHost, leaf: Leaf, records: Readon
|
|
|
1274
1279
|
const subject = row.value;
|
|
1275
1280
|
for (; index < end && !halted; index++) {
|
|
1276
1281
|
row.index = index;
|
|
1277
|
-
|
|
1282
|
+
body(subject, tailValue, ownedInput);
|
|
1278
1283
|
}
|
|
1279
1284
|
}
|
|
1280
1285
|
}
|
|
@@ -190,7 +190,7 @@ describe("up", () => {
|
|
|
190
190
|
expect(await read(ecs, entities.grandchild, world)).toBe(100);
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
-
it("runs every row after
|
|
193
|
+
it("runs every row after spawning more than a patch takes, with change filters, then only the rows that changed", async () => {
|
|
194
194
|
await using ecs = createWorld({ [workers]: 0 });
|
|
195
195
|
await ecs.run(spawn);
|
|
196
196
|
await ecs.run(nest);
|
|
@@ -211,6 +211,18 @@ describe("up", () => {
|
|
|
211
211
|
expect(ran).toEqual([]);
|
|
212
212
|
});
|
|
213
213
|
|
|
214
|
+
it("runs only the rows spawned in bulk, which the journal holds once each, with change filters", async () => {
|
|
215
|
+
await using ecs = createWorld({ [workers]: 0 });
|
|
216
|
+
await ecs.run(spawn);
|
|
217
|
+
await ecs.run(nest);
|
|
218
|
+
await ecs.run(propagateChanged);
|
|
219
|
+
await ecs.run(bulkUnder, { count: 3000 });
|
|
220
|
+
ran.length = 0;
|
|
221
|
+
await ecs.run(propagateChanged);
|
|
222
|
+
expect(ran.length).toBe(3000);
|
|
223
|
+
expect(await readEach(ecs, bulk.slice(0, 3), world)).toEqual([112, 112, 112]);
|
|
224
|
+
});
|
|
225
|
+
|
|
214
226
|
it("finds a cycle a reparent closes after the tree was built, and runs its rows again once it opens", async () => {
|
|
215
227
|
await using ecs = createWorld({ [workers]: 0 });
|
|
216
228
|
await ecs.run(spawn);
|
|
@@ -22,6 +22,9 @@ const cyclic = -1;
|
|
|
22
22
|
const denseShare = 1 / 8;
|
|
23
23
|
/** Rows below this many are sorted rather than scanned. */
|
|
24
24
|
const denseMinimum = 4096;
|
|
25
|
+
/** Past this many journal entries, and past this share of the matched rows, the tree is built again rather than patched. */
|
|
26
|
+
const patchMinimum = 4096;
|
|
27
|
+
const patchShare = 1 / 8;
|
|
25
28
|
/** Sort keys pack a row with its parent's archetype: `(parent + 1) * rowLimit + row`. */
|
|
26
29
|
const rowLimit = 2 ** 27;
|
|
27
30
|
|
|
@@ -375,7 +378,9 @@ const createShared = (state: State, through: number, read: readonly number[], ma
|
|
|
375
378
|
const { journal } = state;
|
|
376
379
|
const end = journalEnd(journal);
|
|
377
380
|
if (position === end) return;
|
|
378
|
-
|
|
381
|
+
let matched = 0;
|
|
382
|
+
for (let k = 0; k < matches.length; k++) matched += matches[k].count;
|
|
383
|
+
if (position < journal.first || end - position > Math.max(patchMinimum, matched * patchShare)) {
|
|
379
384
|
build(matches);
|
|
380
385
|
position = end;
|
|
381
386
|
rebuiltAll();
|
|
@@ -62,10 +62,10 @@ export const noteInView = System.create({ components: { with: [position, note],
|
|
|
62
62
|
row[note] = { text: `${row[position]}` };
|
|
63
63
|
});
|
|
64
64
|
const heard: number[] = [];
|
|
65
|
-
export const hearInView = System.create({
|
|
65
|
+
export const hearInView = System.create({ readonly: true, components: { with: [position], changed: [position], where: { [cell]: (at, { resources }) => visible(at, resources[camera].x, resources[camera].radius) } } }, row => { heard.push(row[position]); });
|
|
66
66
|
// Filtered by a resource the predicate doesn't read: a culled archetype coming into view catches up on its change.
|
|
67
67
|
const signalled: number[] = [];
|
|
68
|
-
export const signalInView = System.create({
|
|
68
|
+
export const signalInView = System.create({ readonly: true, components: { with: [position], where: { [cell]: (at, { resources }) => visible(at, resources[camera].x, resources[camera].radius) } }, resources: { changed: [other] } }, row => { signalled.push(row[position]); });
|
|
69
69
|
// A system moving the camera its own predicate reads: its next run sees the new view.
|
|
70
70
|
const followed: number[] = [];
|
|
71
71
|
export const follow = System.create({ components: { with: [position], where: { [cell]: (at, { resources }) => visible(at, resources[camera].x, resources[camera].radius) } } }, (row, { resources }) => {
|
|
@@ -79,7 +79,7 @@ export const noteChanged = System.create({ components: { with: [position, note],
|
|
|
79
79
|
row[note] = { text: `${row[position]}` };
|
|
80
80
|
});
|
|
81
81
|
const notes: string[] = [];
|
|
82
|
-
export const readNotes = System.create({
|
|
82
|
+
export const readNotes = System.create({ readonly: true, components: { with: [note] } }, row => { if (row[note] !== null) notes.push(row[note].text); });
|
|
83
83
|
export const nudgeNoted = System.create({ components: { with: [position, note] } }, row => { row[position] += 0.5; });
|
|
84
84
|
|
|
85
85
|
/** Waits a turn at a time until `done`, for two seconds at most. */
|
|
@@ -91,7 +91,7 @@ const until = async (done: () => boolean): Promise<void> => {
|
|
|
91
91
|
/** Every row's position and how often it was seen, by position. */
|
|
92
92
|
const seenOf = async (world: ReturnType<typeof createWorld>): Promise<readonly string[]> => {
|
|
93
93
|
const read: [number, string][] = [];
|
|
94
|
-
await world.run(System.create({
|
|
94
|
+
await world.run(System.create({ readonly: true, components: { with: [position, seen] } }, row => { read.push([row[position], `${row[position]}:${row[seen]}`]); }));
|
|
95
95
|
return read.sort((a, b) => a[0] - b[0]).map(([, text]) => text).filter(text => !text.endsWith(":0"));
|
|
96
96
|
};
|
|
97
97
|
|
|
@@ -115,22 +115,50 @@ const predicateOf = (given: unknown): ((value: unknown, context: object) => unkn
|
|
|
115
115
|
typeof given === "function" ? (value, context) => Reflect.apply(given, undefined, [value, context]) : null;
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
118
|
+
* `where`'s conditions in a world, each value interned there, by number, and with `pinned`, pinned, so its number stays
|
|
119
|
+
* while no archetype holds it and one that comes to hold it again matches; or the key of a component that isn't shared.
|
|
120
|
+
* A value interned unpinned keeps its number to the end of the sync point.
|
|
120
121
|
*/
|
|
121
|
-
|
|
122
|
-
const conditions: Condition[] =
|
|
122
|
+
const conditionsOf = (state: State, where: { readonly [key: symbol]: unknown }, pinned: boolean): Condition[] | symbol => {
|
|
123
|
+
const conditions: Condition[] = [];
|
|
124
|
+
for (const key of Object.getOwnPropertySymbols(where)) {
|
|
123
125
|
const id = idOf(state, key);
|
|
124
126
|
const given: unknown = Reflect.get(where, key);
|
|
125
127
|
const predicate = predicateOf(given);
|
|
126
|
-
if (predicate !== null)
|
|
128
|
+
if (predicate !== null) {
|
|
129
|
+
conditions.push({ id, number: -1, predicate });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
127
132
|
const values = state.shared[id];
|
|
128
|
-
if (values === undefined)
|
|
133
|
+
if (values === undefined) return key;
|
|
129
134
|
const number = internShared(values, given);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
if (pinned && number >= 0) holdShared(values, number);
|
|
136
|
+
conditions.push({ id, number, predicate: null });
|
|
137
|
+
}
|
|
138
|
+
return conditions;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Whether an archetype holds each of `where`'s values, as a system's `where` of values passes it, for a `where` naming
|
|
143
|
+
* only shared components and giving no predicate; or the key of one that isn't shared. Its values are pinned with
|
|
144
|
+
* `pinned`, and otherwise only interned, for a `where` used before the sync point ends.
|
|
145
|
+
*/
|
|
146
|
+
export const holdsValuesOf = (state: State, where: { readonly [key: symbol]: unknown }, pinned: boolean): ((archetype: Valued) => boolean) | symbol => {
|
|
147
|
+
const conditions = conditionsOf(state, where, pinned);
|
|
148
|
+
if (typeof conditions === "symbol") return conditions;
|
|
149
|
+
return archetype => {
|
|
150
|
+
for (let c = 0; c < conditions.length; c++) if (archetype.shared[conditions[c].id] !== conditions[c].number) return false;
|
|
151
|
+
return true;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A system's `where` in a world: its values interned there, by number, its predicates' context reading `reads`,
|
|
157
|
+
* evaluated again once one of them changed since they last were.
|
|
158
|
+
*/
|
|
159
|
+
export const createWhere = <A extends Valued>(state: State, where: { readonly [key: symbol]: unknown }, reads: readonly symbol[], leaf: Leaf): Where<A> => {
|
|
160
|
+
const conditions = conditionsOf(state, where, true);
|
|
161
|
+
if (typeof conditions === "symbol") throw new Error(`System "${leaf.name}": where names "${labelOf(conditions)}", which isn't a shared component.`);
|
|
134
162
|
const readIds = reads.map(key => idOf(state, key));
|
|
135
163
|
// Its predicates' context: each resource read once per evaluation, since a struct's read copies it out.
|
|
136
164
|
const cached: unknown[] = reads.map(() => undefined);
|