@forgeax/engine-ecs 0.1.21 → 0.1.24
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 +83 -17
- package/dist/__tests__/changed-block-summary.candidate.test.d.ts +2 -0
- package/dist/__tests__/changed-block-summary.candidate.test.d.ts.map +1 -0
- package/dist/__tests__/component-requirements.unit.test.d.ts +2 -0
- package/dist/__tests__/component-requirements.unit.test.d.ts.map +1 -0
- package/dist/__tests__/component-version-surface.test.d.ts +2 -0
- package/dist/__tests__/component-version-surface.test.d.ts.map +1 -0
- package/dist/__tests__/derived-range-writer.contract.test-d.d.ts +2 -0
- package/dist/__tests__/derived-range-writer.contract.test-d.d.ts.map +1 -0
- package/dist/__tests__/derived-range-writer.contract.test.d.ts +2 -0
- package/dist/__tests__/derived-range-writer.contract.test.d.ts.map +1 -0
- package/dist/__tests__/structural-evidence.contract.test-d.d.ts +2 -0
- package/dist/__tests__/structural-evidence.contract.test-d.d.ts.map +1 -0
- package/dist/__tests__/structural-evidence.contract.test.d.ts +2 -0
- package/dist/__tests__/structural-evidence.contract.test.d.ts.map +1 -0
- package/dist/component-schema.d.ts +17 -0
- package/dist/component-schema.d.ts.map +1 -1
- package/dist/component.d.ts +16 -7
- package/dist/component.d.ts.map +1 -1
- package/dist/errors/query-and-component-errors.d.ts +1 -1
- package/dist/errors/query-and-component-errors.d.ts.map +1 -1
- package/dist/errors/validation-errors.d.ts +24 -0
- package/dist/errors/validation-errors.d.ts.map +1 -1
- package/dist/errors.d.ts +14 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/externalization/index.mjs.map +1 -1
- package/dist/index.mjs +977 -255
- package/dist/index.mjs.map +1 -1
- package/dist/internal.d.ts +18 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.mjs +20 -2
- package/dist/internal.mjs.map +1 -1
- package/dist/projection/index.d.ts +27 -52
- package/dist/projection/index.d.ts.map +1 -1
- package/dist/projection/index.mjs +66 -76
- package/dist/projection/index.mjs.map +1 -1
- package/dist/query/derived-range-writer.d.ts +62 -0
- package/dist/query/derived-range-writer.d.ts.map +1 -0
- package/dist/query/query.d.ts +1 -1
- package/dist/query/query.d.ts.map +1 -1
- package/dist/relationship-index.d.ts +8 -0
- package/dist/relationship-index.d.ts.map +1 -1
- package/dist/shared-ref-store.d.ts +5 -9
- package/dist/shared-ref-store.d.ts.map +1 -1
- package/dist/shared.mjs.map +1 -1
- package/dist/storage/change-detection.d.ts +1 -3
- package/dist/storage/change-detection.d.ts.map +1 -1
- package/dist/storage/structural-evidence.d.ts +30 -0
- package/dist/storage/structural-evidence.d.ts.map +1 -0
- package/dist/world-component-access.d.ts +65 -19
- package/dist/world-component-access.d.ts.map +1 -1
- package/dist/world-component-storage.d.ts +26 -10
- package/dist/world-component-storage.d.ts.map +1 -1
- package/dist/world-core.d.ts +2 -2
- package/dist/world-core.d.ts.map +1 -1
- package/dist/world-entity-lifecycle.d.ts.map +1 -1
- package/dist/world-internal.d.ts +1 -1
- package/dist/world-internal.d.ts.map +1 -1
- package/dist/world.d.ts +15 -14
- package/dist/world.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/changed-block-summary.candidate.test.ts +57 -0
- package/src/__tests__/component-requirements.unit.test.ts +76 -0
- package/src/__tests__/component-version-surface.test.ts +45 -0
- package/src/__tests__/derived-range-writer.contract.test-d.ts +35 -0
- package/src/__tests__/derived-range-writer.contract.test.ts +339 -0
- package/src/__tests__/errors.unit.test.ts +21 -5
- package/src/__tests__/externalization-render-read-lease.test-d.ts +1 -1
- package/src/__tests__/externalization-render-read-lease.unit.test.ts +15 -15
- package/src/__tests__/query-storage-trends.perf.test.ts +19 -0
- package/src/__tests__/query.unit.test.ts +10 -6
- package/src/__tests__/relationship-index.test.ts +147 -1
- package/src/__tests__/shared-ref-store.unit.test.ts +3 -8
- package/src/__tests__/structural-evidence.contract.test-d.ts +6 -0
- package/src/__tests__/structural-evidence.contract.test.ts +49 -0
- package/src/component-schema.ts +51 -0
- package/src/component.ts +21 -8
- package/src/errors/query-and-component-errors.ts +4 -1
- package/src/errors/validation-errors.ts +30 -0
- package/src/errors.ts +29 -5
- package/src/internal.ts +59 -0
- package/src/projection/index.ts +68 -149
- package/src/query/derived-range-writer.ts +443 -0
- package/src/query/query.ts +115 -16
- package/src/relationship-index.ts +13 -1
- package/src/shared-ref-store.ts +16 -24
- package/src/storage/change-detection.ts +0 -15
- package/src/storage/structural-evidence.ts +64 -0
- package/src/world-component-access.ts +479 -130
- package/src/world-component-storage.ts +104 -18
- package/src/world-core.ts +2 -2
- package/src/world-entity-lifecycle.ts +59 -10
- package/src/world-internal.ts +10 -5
- package/src/world.ts +97 -53
- package/dist/.tsbuildinfo +0 -1
- package/dist/__tests__/world-change-journal.unit.test.d.ts +0 -2
- package/dist/__tests__/world-change-journal.unit.test.d.ts.map +0 -1
- package/dist/__tests__/world-internal-identity.unit.test.d.ts +0 -2
- package/dist/__tests__/world-internal-identity.unit.test.d.ts.map +0 -1
- package/dist/world-change-journal.d.ts +0 -34
- package/dist/world-change-journal.d.ts.map +0 -1
- package/src/__tests__/world-change-journal.unit.test.ts +0 -97
- package/src/__tests__/world-internal-identity.unit.test.ts +0 -17
- package/src/world-change-journal.ts +0 -84
package/dist/index.mjs
CHANGED
|
@@ -21,6 +21,31 @@ function componentDefinition(component) {
|
|
|
21
21
|
}
|
|
22
22
|
return definition;
|
|
23
23
|
}
|
|
24
|
+
function componentRequirements(component) {
|
|
25
|
+
return definitions.definitions.get(component)?.policy.requires ?? [];
|
|
26
|
+
}
|
|
27
|
+
function expandComponentRequirements(componentDatas) {
|
|
28
|
+
let hasRequirements = false;
|
|
29
|
+
for (const entry of componentDatas) {
|
|
30
|
+
if (componentRequirements(entry.component).length !== 0) {
|
|
31
|
+
hasRequirements = true;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!hasRequirements) return componentDatas;
|
|
36
|
+
const expanded = [...componentDatas];
|
|
37
|
+
const seen = new Set(expanded.map((entry) => entry.component));
|
|
38
|
+
for (let index = 0; index < expanded.length; index++) {
|
|
39
|
+
const component = expanded[index]?.component;
|
|
40
|
+
if (component === void 0) continue;
|
|
41
|
+
for (const required of componentRequirements(component)) {
|
|
42
|
+
if (seen.has(required)) continue;
|
|
43
|
+
seen.add(required);
|
|
44
|
+
expanded.push({ component: required, data: {} });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return expanded;
|
|
48
|
+
}
|
|
24
49
|
function deepFreeze(value) {
|
|
25
50
|
if (value === null || typeof value !== "object" && typeof value !== "function") {
|
|
26
51
|
return value;
|
|
@@ -654,6 +679,17 @@ var ChangeEpochExhaustedError = class extends Error {
|
|
|
654
679
|
this.detail = { epoch };
|
|
655
680
|
}
|
|
656
681
|
};
|
|
682
|
+
var DerivedRangeOutOfBoundsError = class extends Error {
|
|
683
|
+
name = "DerivedRangeOutOfBoundsError";
|
|
684
|
+
code = "derived-range-out-of-bounds";
|
|
685
|
+
expected = "a non-negative span-relative range with start + count <= span.length";
|
|
686
|
+
hint = "check start and count against the QuerySpan length, then retry without changing World state";
|
|
687
|
+
detail;
|
|
688
|
+
constructor(start, count, spanLength) {
|
|
689
|
+
super(`Derived range [${start}, ${start + count}) exceeds QuerySpan length ${spanLength}.`);
|
|
690
|
+
this.detail = { start, count, spanLength };
|
|
691
|
+
}
|
|
692
|
+
};
|
|
657
693
|
var ProtectedResourceError = class extends Error {
|
|
658
694
|
name = "ProtectedResourceError";
|
|
659
695
|
code = "resource-protected";
|
|
@@ -1254,7 +1290,11 @@ function defineComponent(name, fields, options) {
|
|
|
1254
1290
|
registerComponentDefinition(token, {
|
|
1255
1291
|
fields: frozenFields,
|
|
1256
1292
|
defaults: frozenDefaults,
|
|
1257
|
-
policy: {
|
|
1293
|
+
policy: {
|
|
1294
|
+
transient: options?.transient ?? false,
|
|
1295
|
+
meta,
|
|
1296
|
+
requires: Object.freeze([...options?.requires ?? []])
|
|
1297
|
+
}
|
|
1258
1298
|
});
|
|
1259
1299
|
return Object.freeze(token);
|
|
1260
1300
|
}
|
|
@@ -1385,7 +1425,11 @@ function defineRelationship(options) {
|
|
|
1385
1425
|
};
|
|
1386
1426
|
const target = defineComponent(options.targetName, targetFields);
|
|
1387
1427
|
const sourceFields = { [options.sourceField]: { type: "entity" } };
|
|
1388
|
-
const source = defineComponent(
|
|
1428
|
+
const source = defineComponent(
|
|
1429
|
+
options.sourceName,
|
|
1430
|
+
sourceFields,
|
|
1431
|
+
options.sourceRequires === void 0 ? void 0 : { requires: options.sourceRequires }
|
|
1432
|
+
);
|
|
1389
1433
|
roles.set(source, {
|
|
1390
1434
|
kind: "source",
|
|
1391
1435
|
target,
|
|
@@ -1563,51 +1607,10 @@ function entityGeneration(entity) {
|
|
|
1563
1607
|
return unpackGen(entity);
|
|
1564
1608
|
}
|
|
1565
1609
|
|
|
1566
|
-
// src/
|
|
1567
|
-
var
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
this.capacity = capacity;
|
|
1571
|
-
if (!Number.isSafeInteger(capacity) || capacity <= 0) {
|
|
1572
|
-
throw new RangeError("WorldChangeJournal capacity must be a positive safe integer");
|
|
1573
|
-
}
|
|
1574
|
-
this.records = new Array(capacity);
|
|
1575
|
-
}
|
|
1576
|
-
capacity;
|
|
1577
|
-
records;
|
|
1578
|
-
nextSequence = 1;
|
|
1579
|
-
cursor() {
|
|
1580
|
-
return this.nextSequence - 1;
|
|
1581
|
-
}
|
|
1582
|
-
append(change) {
|
|
1583
|
-
const sequence = this.nextSequence;
|
|
1584
|
-
this.nextSequence += 1;
|
|
1585
|
-
this.records[(sequence - 1) % this.capacity] = { ...change, sequence };
|
|
1586
|
-
return sequence;
|
|
1587
|
-
}
|
|
1588
|
-
readAfter(cursor) {
|
|
1589
|
-
const latest = this.cursor();
|
|
1590
|
-
if (!Number.isSafeInteger(cursor) || cursor < 0 || cursor > latest) {
|
|
1591
|
-
throw new RangeError(`WorldChangeJournal cursor ${cursor} is outside 0..${latest}`);
|
|
1592
|
-
}
|
|
1593
|
-
const oldestAvailable = Math.max(1, latest - this.capacity + 1);
|
|
1594
|
-
if (cursor < oldestAvailable - 1) {
|
|
1595
|
-
return { status: "overflow", cursor: latest, oldestAvailable };
|
|
1596
|
-
}
|
|
1597
|
-
if (cursor === latest) {
|
|
1598
|
-
return { status: "ok", cursor: latest, records: EMPTY_CHANGES };
|
|
1599
|
-
}
|
|
1600
|
-
const records = [];
|
|
1601
|
-
for (let sequence = cursor + 1; sequence <= latest; sequence += 1) {
|
|
1602
|
-
const record = this.records[(sequence - 1) % this.capacity];
|
|
1603
|
-
if (record === void 0 || record.sequence !== sequence) {
|
|
1604
|
-
return { status: "overflow", cursor: latest, oldestAvailable };
|
|
1605
|
-
}
|
|
1606
|
-
records.push(record);
|
|
1607
|
-
}
|
|
1608
|
-
return { status: "ok", cursor: latest, records };
|
|
1609
|
-
}
|
|
1610
|
-
};
|
|
1610
|
+
// src/internal.ts
|
|
1611
|
+
var DERIVED_WRITER = /* @__PURE__ */ Symbol.for(
|
|
1612
|
+
"forgeax.ecs.query.derivedWriter"
|
|
1613
|
+
);
|
|
1611
1614
|
|
|
1612
1615
|
// src/storage/archetype.ts
|
|
1613
1616
|
var INITIAL_CAPACITY = 64;
|
|
@@ -2039,10 +2042,246 @@ function markComponentChanged(graph, location, entity, componentId2, epoch) {
|
|
|
2039
2042
|
const tableRow2 = archetype.rows[location.archetypeRow] ?? -1;
|
|
2040
2043
|
epochs.changed[tableRow2] = epoch();
|
|
2041
2044
|
}
|
|
2042
|
-
function
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2045
|
+
function createDerivedRangeWriter(world, component, source) {
|
|
2046
|
+
let boundEpoch = -1;
|
|
2047
|
+
let bindingTables = [];
|
|
2048
|
+
let bindings = [];
|
|
2049
|
+
let bindingByTable = /* @__PURE__ */ new Map();
|
|
2050
|
+
let runStartBuffers = [];
|
|
2051
|
+
let runCountBuffers = [];
|
|
2052
|
+
const rebind = () => {
|
|
2053
|
+
const tables = source.tables();
|
|
2054
|
+
const nextBindings = [];
|
|
2055
|
+
const nextRunStartBuffers = [];
|
|
2056
|
+
const nextRunCountBuffers = [];
|
|
2057
|
+
for (const table of tables) {
|
|
2058
|
+
nextBindings.push({
|
|
2059
|
+
tableId: table.id,
|
|
2060
|
+
entities: table.storage.get(componentId(Entity))?.fields.get("self")?.view ?? new Uint32Array(0),
|
|
2061
|
+
read: buildWholeColumnShape(table, source.readComponents),
|
|
2062
|
+
write: buildWholeColumnShape(table, source.writeComponents),
|
|
2063
|
+
rowCapacity: table.size
|
|
2064
|
+
});
|
|
2065
|
+
const runCapacity = Math.max(1, table.size);
|
|
2066
|
+
nextRunStartBuffers.push(new Int32Array(runCapacity));
|
|
2067
|
+
nextRunCountBuffers.push(new Int32Array(runCapacity));
|
|
2068
|
+
}
|
|
2069
|
+
bindingTables = tables;
|
|
2070
|
+
bindings = nextBindings;
|
|
2071
|
+
bindingByTable = /* @__PURE__ */ new Map();
|
|
2072
|
+
for (let index = 0; index < nextBindings.length; index += 1) {
|
|
2073
|
+
const binding = nextBindings[index];
|
|
2074
|
+
if (binding !== void 0) bindingByTable.set(binding.tableId, index);
|
|
2075
|
+
}
|
|
2076
|
+
runStartBuffers = nextRunStartBuffers;
|
|
2077
|
+
runCountBuffers = nextRunCountBuffers;
|
|
2078
|
+
boundEpoch = source.structureEpoch();
|
|
2079
|
+
};
|
|
2080
|
+
rebind();
|
|
2081
|
+
const writer = {
|
|
2082
|
+
get bindings() {
|
|
2083
|
+
if (boundEpoch !== source.structureEpoch()) rebind();
|
|
2084
|
+
return bindings;
|
|
2085
|
+
},
|
|
2086
|
+
locateEntity(entity, cursor) {
|
|
2087
|
+
if (world.execution.health === "poisoned") return false;
|
|
2088
|
+
if (boundEpoch !== source.structureEpoch()) rebind();
|
|
2089
|
+
const archetype = world[worldInternal].getEntityArchetype(entity);
|
|
2090
|
+
if (archetype === void 0) return false;
|
|
2091
|
+
const bindingIndex = bindingByTable.get(archetype.tableId);
|
|
2092
|
+
if (bindingIndex === void 0) return false;
|
|
2093
|
+
const record = world[worldInternal].getRecords()[entity & 16777215];
|
|
2094
|
+
if (record === void 0 || record.archetypeId !== archetype.id) return false;
|
|
2095
|
+
const row = archetype.rows[record.archetypeRow];
|
|
2096
|
+
const binding = bindings[bindingIndex];
|
|
2097
|
+
if (binding === void 0 || row === void 0 || row < 0 || row >= binding.rowCapacity) {
|
|
2098
|
+
return false;
|
|
2099
|
+
}
|
|
2100
|
+
cursor.bindingIndex = bindingIndex;
|
|
2101
|
+
cursor.row = row;
|
|
2102
|
+
return true;
|
|
2103
|
+
},
|
|
2104
|
+
publishChangedRows(bindingIndex, changed) {
|
|
2105
|
+
if (world.execution.health === "poisoned") {
|
|
2106
|
+
return err(new WorldPoisonedError(world.identity, world.execution.fault));
|
|
2107
|
+
}
|
|
2108
|
+
if (boundEpoch !== source.structureEpoch()) rebind();
|
|
2109
|
+
const binding = bindings[bindingIndex];
|
|
2110
|
+
const table = bindingTables[bindingIndex];
|
|
2111
|
+
const runStarts = runStartBuffers[bindingIndex];
|
|
2112
|
+
const runCounts = runCountBuffers[bindingIndex];
|
|
2113
|
+
if (binding === void 0 || table === void 0 || runStarts === void 0 || runCounts === void 0 || !Number.isSafeInteger(bindingIndex) || bindingIndex < 0 || changed.length < binding.rowCapacity || table.storage.get(componentId(component)) === void 0) {
|
|
2114
|
+
return err(new DerivedRangeOutOfBoundsError(0, changed.length, binding?.rowCapacity ?? 0));
|
|
2115
|
+
}
|
|
2116
|
+
let runCount = 0;
|
|
2117
|
+
let runStart = -1;
|
|
2118
|
+
for (let row = 0; row < binding.rowCapacity; row += 1) {
|
|
2119
|
+
if ((changed[row] ?? 0) !== 0) {
|
|
2120
|
+
if (runStart < 0) runStart = row;
|
|
2121
|
+
} else if (runStart >= 0) {
|
|
2122
|
+
runStarts[runCount] = runStart;
|
|
2123
|
+
runCounts[runCount] = row - runStart;
|
|
2124
|
+
runCount += 1;
|
|
2125
|
+
runStart = -1;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
if (runStart >= 0) {
|
|
2129
|
+
runStarts[runCount] = runStart;
|
|
2130
|
+
runCounts[runCount] = binding.rowCapacity - runStart;
|
|
2131
|
+
runCount += 1;
|
|
2132
|
+
}
|
|
2133
|
+
if (runCount === 0) return ok(void 0);
|
|
2134
|
+
const previousEpoch = world[worldInternal].getMutationEpoch();
|
|
2135
|
+
let epoch;
|
|
2136
|
+
try {
|
|
2137
|
+
epoch = world[worldInternal].nextMutationEpoch();
|
|
2138
|
+
const componentIdentifier = componentId(component);
|
|
2139
|
+
for (let index = 0; index < runCount; index += 1) {
|
|
2140
|
+
world[worldInternal].publishDerivedRange(
|
|
2141
|
+
table,
|
|
2142
|
+
componentIdentifier,
|
|
2143
|
+
runStarts[index] ?? 0,
|
|
2144
|
+
runCounts[index] ?? 0,
|
|
2145
|
+
epoch
|
|
2146
|
+
);
|
|
2147
|
+
}
|
|
2148
|
+
changed.fill(0, 0, binding.rowCapacity);
|
|
2149
|
+
return ok(void 0);
|
|
2150
|
+
} catch (cause) {
|
|
2151
|
+
world[worldInternal].restoreMutationEpoch(previousEpoch);
|
|
2152
|
+
world[worldInternal].poisonExecution({
|
|
2153
|
+
code: "shared-kernel-failed",
|
|
2154
|
+
kernelName: `derived-range:${component.name}:publish`,
|
|
2155
|
+
cause,
|
|
2156
|
+
partialWrite: true,
|
|
2157
|
+
retryable: false
|
|
2158
|
+
});
|
|
2159
|
+
return err(new SharedKernelFailureError(component.name, world.identity, cause, true));
|
|
2160
|
+
}
|
|
2161
|
+
},
|
|
2162
|
+
writeRange(bindingIndex, base, start, count, kernel, context) {
|
|
2163
|
+
if (world.execution.health === "poisoned") {
|
|
2164
|
+
return err(new WorldPoisonedError(world.identity, world.execution.fault));
|
|
2165
|
+
}
|
|
2166
|
+
if (boundEpoch !== source.structureEpoch()) rebind();
|
|
2167
|
+
const binding = bindings[bindingIndex];
|
|
2168
|
+
const table = bindingTables[bindingIndex];
|
|
2169
|
+
if (binding === void 0 || table === void 0 || !Number.isSafeInteger(bindingIndex) || !Number.isSafeInteger(base) || !Number.isSafeInteger(start) || !Number.isSafeInteger(count) || bindingIndex < 0 || base < 0 || start < 0 || count < 0 || base + start + count > binding.rowCapacity || table.storage.get(componentId(component)) === void 0) {
|
|
2170
|
+
return err(new DerivedRangeOutOfBoundsError(start, count, binding?.rowCapacity ?? 0));
|
|
2171
|
+
}
|
|
2172
|
+
if (count === 0) return ok(void 0);
|
|
2173
|
+
const previousEpoch = world[worldInternal].getMutationEpoch();
|
|
2174
|
+
let epoch;
|
|
2175
|
+
try {
|
|
2176
|
+
epoch = world[worldInternal].nextMutationEpoch();
|
|
2177
|
+
} catch (cause) {
|
|
2178
|
+
return err(cause);
|
|
2179
|
+
}
|
|
2180
|
+
try {
|
|
2181
|
+
kernel(binding, base, start, count, context);
|
|
2182
|
+
world[worldInternal].publishDerivedRange(
|
|
2183
|
+
table,
|
|
2184
|
+
componentId(component),
|
|
2185
|
+
base + start,
|
|
2186
|
+
count,
|
|
2187
|
+
epoch
|
|
2188
|
+
);
|
|
2189
|
+
return ok(void 0);
|
|
2190
|
+
} catch (cause) {
|
|
2191
|
+
world[worldInternal].restoreMutationEpoch(previousEpoch);
|
|
2192
|
+
world[worldInternal].poisonExecution({
|
|
2193
|
+
code: "shared-kernel-failed",
|
|
2194
|
+
kernelName: `derived-range:${component.name}`,
|
|
2195
|
+
cause,
|
|
2196
|
+
partialWrite: true,
|
|
2197
|
+
retryable: false
|
|
2198
|
+
});
|
|
2199
|
+
return err(new SharedKernelFailureError(component.name, world.identity, cause, true));
|
|
2200
|
+
}
|
|
2201
|
+
},
|
|
2202
|
+
probeAndCommitRange(bindingIndex, base, start, count, probe, commit, context) {
|
|
2203
|
+
if (world.execution.health === "poisoned") {
|
|
2204
|
+
return err(new WorldPoisonedError(world.identity, world.execution.fault));
|
|
2205
|
+
}
|
|
2206
|
+
if (boundEpoch !== source.structureEpoch()) rebind();
|
|
2207
|
+
const binding = bindings[bindingIndex];
|
|
2208
|
+
const table = bindingTables[bindingIndex];
|
|
2209
|
+
const runStarts = runStartBuffers[bindingIndex];
|
|
2210
|
+
const runCounts = runCountBuffers[bindingIndex];
|
|
2211
|
+
if (binding === void 0 || table === void 0 || runStarts === void 0 || runCounts === void 0 || !Number.isSafeInteger(bindingIndex) || !Number.isSafeInteger(base) || !Number.isSafeInteger(start) || !Number.isSafeInteger(count) || bindingIndex < 0 || base < 0 || start < 0 || count < 0 || base + start + count > binding.rowCapacity || table.storage.get(componentId(component)) === void 0) {
|
|
2212
|
+
return err(new DerivedRangeOutOfBoundsError(start, count, binding?.rowCapacity ?? 0));
|
|
2213
|
+
}
|
|
2214
|
+
if (count === 0) return ok(void 0);
|
|
2215
|
+
const previousEpoch = world[worldInternal].getMutationEpoch();
|
|
2216
|
+
let runCount = 0;
|
|
2217
|
+
let epoch = 0;
|
|
2218
|
+
let epochReserved = false;
|
|
2219
|
+
let runStart = -1;
|
|
2220
|
+
try {
|
|
2221
|
+
for (let offset = 0; offset < count; offset += 1) {
|
|
2222
|
+
const row = base + start + offset;
|
|
2223
|
+
const changed = probe(binding, row, context);
|
|
2224
|
+
if (changed) {
|
|
2225
|
+
if (!epochReserved) {
|
|
2226
|
+
try {
|
|
2227
|
+
epoch = world[worldInternal].nextMutationEpoch();
|
|
2228
|
+
epochReserved = true;
|
|
2229
|
+
} catch (cause) {
|
|
2230
|
+
return err(cause);
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
if (runStart < 0) runStart = row;
|
|
2234
|
+
commit(binding, row, context);
|
|
2235
|
+
} else if (runStart >= 0) {
|
|
2236
|
+
runStarts[runCount] = runStart;
|
|
2237
|
+
runCounts[runCount] = row - runStart;
|
|
2238
|
+
runCount += 1;
|
|
2239
|
+
runStart = -1;
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
if (runStart >= 0) {
|
|
2243
|
+
runStarts[runCount] = runStart;
|
|
2244
|
+
runCounts[runCount] = base + start + count - runStart;
|
|
2245
|
+
runCount += 1;
|
|
2246
|
+
}
|
|
2247
|
+
if (runCount > 0) {
|
|
2248
|
+
const componentIdentifier = componentId(component);
|
|
2249
|
+
for (let index = 0; index < runCount; index += 1) {
|
|
2250
|
+
const runStartValue = runStarts[index] ?? 0;
|
|
2251
|
+
const runLength = runCounts[index] ?? 0;
|
|
2252
|
+
world[worldInternal].publishDerivedRange(
|
|
2253
|
+
table,
|
|
2254
|
+
componentIdentifier,
|
|
2255
|
+
runStartValue,
|
|
2256
|
+
runLength,
|
|
2257
|
+
epoch
|
|
2258
|
+
);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
return ok(void 0);
|
|
2262
|
+
} catch (cause) {
|
|
2263
|
+
if (epochReserved) world[worldInternal].restoreMutationEpoch(previousEpoch);
|
|
2264
|
+
world[worldInternal].poisonExecution({
|
|
2265
|
+
code: "shared-kernel-failed",
|
|
2266
|
+
kernelName: `derived-range:${component.name}:probe`,
|
|
2267
|
+
cause,
|
|
2268
|
+
partialWrite: true,
|
|
2269
|
+
retryable: false
|
|
2270
|
+
});
|
|
2271
|
+
return err(new SharedKernelFailureError(component.name, world.identity, cause, true));
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
};
|
|
2275
|
+
return writer;
|
|
2276
|
+
}
|
|
2277
|
+
function buildWholeColumnShape(table, components) {
|
|
2278
|
+
const shape = {};
|
|
2279
|
+
for (const component of components) {
|
|
2280
|
+
const fields = table.storage.get(componentId(component))?.fields;
|
|
2281
|
+
if (fields === void 0) continue;
|
|
2282
|
+
for (const [fieldName, column] of fields) shape[fieldName] = column.view;
|
|
2283
|
+
}
|
|
2284
|
+
return shape;
|
|
2046
2285
|
}
|
|
2047
2286
|
|
|
2048
2287
|
// src/query/query.ts
|
|
@@ -2133,7 +2372,13 @@ var QueryRowFacade = class _QueryRowFacade {
|
|
|
2133
2372
|
mut(component) {
|
|
2134
2373
|
const current = this.get(component);
|
|
2135
2374
|
if (current === void 0) throw new Error(`Query row lacks ${component.name}.`);
|
|
2136
|
-
|
|
2375
|
+
if (isRelationshipTarget(component)) {
|
|
2376
|
+
throw new RelationshipTargetReadonlyError(component.name, "query row");
|
|
2377
|
+
}
|
|
2378
|
+
const relationshipSource2 = relationshipRole(component)?.kind === "source";
|
|
2379
|
+
if (!relationshipSource2) {
|
|
2380
|
+
this.world[worldInternal].markComponentChanged(this.entity, componentId(component));
|
|
2381
|
+
}
|
|
2137
2382
|
return new Proxy(current, {
|
|
2138
2383
|
set: (target, property, value) => {
|
|
2139
2384
|
if (typeof property !== "string") return false;
|
|
@@ -2154,7 +2399,7 @@ var QuerySpanFacade = class {
|
|
|
2154
2399
|
this.rowStart = rowStart;
|
|
2155
2400
|
this.length = length;
|
|
2156
2401
|
const entityColumn = table.storage.get(componentId(Entity))?.fields.get("self");
|
|
2157
|
-
this.entities = entityColumn
|
|
2402
|
+
this.entities = entityColumn === void 0 ? new Uint32Array(0) : rowStart === 0 && length === entityColumn.view.length ? entityColumn.view : entityColumn.view.subarray(rowStart, rowStart + length);
|
|
2158
2403
|
}
|
|
2159
2404
|
world;
|
|
2160
2405
|
table;
|
|
@@ -2170,6 +2415,9 @@ var QuerySpanFacade = class {
|
|
|
2170
2415
|
);
|
|
2171
2416
|
}
|
|
2172
2417
|
mut(component) {
|
|
2418
|
+
if (relationshipRole(component) !== void 0) {
|
|
2419
|
+
throw new RelationshipTargetReadonlyError(component.name, "query span");
|
|
2420
|
+
}
|
|
2173
2421
|
this.world[worldInternal].markComponentRangeChanged(
|
|
2174
2422
|
this.table,
|
|
2175
2423
|
componentId(component),
|
|
@@ -2185,7 +2433,7 @@ var QuerySpanFacade = class {
|
|
|
2185
2433
|
}
|
|
2186
2434
|
};
|
|
2187
2435
|
function makeManagedColumnReader(view, length, fieldType) {
|
|
2188
|
-
const slots = view.subarray(0, length);
|
|
2436
|
+
const slots = view.length === length ? view : view.subarray(0, length);
|
|
2189
2437
|
return Object.freeze({
|
|
2190
2438
|
length,
|
|
2191
2439
|
get(i) {
|
|
@@ -2201,7 +2449,7 @@ function buildColumnShape(table, component, rowStart, rowCount) {
|
|
|
2201
2449
|
for (const [fieldName, column] of fields) {
|
|
2202
2450
|
const start = rowStart * column.arity;
|
|
2203
2451
|
const end = start + rowCount * column.arity;
|
|
2204
|
-
const view = column.view.subarray(start, end);
|
|
2452
|
+
const view = start === 0 && end === column.view.length ? column.view : column.view.subarray(start, end);
|
|
2205
2453
|
const fieldType = componentSchema(component)[fieldName];
|
|
2206
2454
|
shape[fieldName] = fieldType !== void 0 && isManagedField(fieldType) ? makeManagedColumnReader(view, view.length, fieldType) : view;
|
|
2207
2455
|
}
|
|
@@ -2216,6 +2464,7 @@ var ExecutableQuery = class {
|
|
|
2216
2464
|
compiled;
|
|
2217
2465
|
matchedArchetypes = [];
|
|
2218
2466
|
matchedTables = [];
|
|
2467
|
+
matchedTableObjects = [];
|
|
2219
2468
|
lastGraphGeneration = -1;
|
|
2220
2469
|
lastObservedEpoch = 0;
|
|
2221
2470
|
active = false;
|
|
@@ -2306,7 +2555,8 @@ var ExecutableQuery = class {
|
|
|
2306
2555
|
query.refreshMatches();
|
|
2307
2556
|
const structureEpoch = query.world[worldInternal].getStructureEpoch();
|
|
2308
2557
|
const upperBound = query.world[worldInternal].getMutationEpoch();
|
|
2309
|
-
let
|
|
2558
|
+
let tableIndex = 0;
|
|
2559
|
+
let rowIndex = 0;
|
|
2310
2560
|
let finished = false;
|
|
2311
2561
|
const close = (commit) => {
|
|
2312
2562
|
if (finished) return;
|
|
@@ -2324,12 +2574,37 @@ var ExecutableQuery = class {
|
|
|
2324
2574
|
query.world[worldInternal].getStructureEpoch()
|
|
2325
2575
|
);
|
|
2326
2576
|
}
|
|
2327
|
-
while (
|
|
2328
|
-
const table = query.world[worldInternal].getGraph().tables[query.matchedTables[
|
|
2329
|
-
if (table === void 0 || table.size === 0)
|
|
2577
|
+
while (tableIndex < query.matchedTables.length) {
|
|
2578
|
+
const table = query.world[worldInternal].getGraph().tables[query.matchedTables[tableIndex] ?? -1];
|
|
2579
|
+
if (table === void 0 || table.size === 0) {
|
|
2580
|
+
tableIndex += 1;
|
|
2581
|
+
rowIndex = 0;
|
|
2582
|
+
continue;
|
|
2583
|
+
}
|
|
2584
|
+
if (query.compiled.changedIds.length === 0 && query.compiled.addedIds.length === 0) {
|
|
2585
|
+
tableIndex += 1;
|
|
2586
|
+
rowIndex = 0;
|
|
2587
|
+
return {
|
|
2588
|
+
done: false,
|
|
2589
|
+
value: new QuerySpanFacade(query.world, table, 0, table.size)
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
while (rowIndex < table.size && !query.denseChangeMatches(table, rowIndex, upperBound)) {
|
|
2593
|
+
rowIndex += 1;
|
|
2594
|
+
}
|
|
2595
|
+
if (rowIndex >= table.size) {
|
|
2596
|
+
tableIndex += 1;
|
|
2597
|
+
rowIndex = 0;
|
|
2598
|
+
continue;
|
|
2599
|
+
}
|
|
2600
|
+
const start = rowIndex;
|
|
2601
|
+
rowIndex += 1;
|
|
2602
|
+
while (rowIndex < table.size && query.denseChangeMatches(table, rowIndex, upperBound)) {
|
|
2603
|
+
rowIndex += 1;
|
|
2604
|
+
}
|
|
2330
2605
|
return {
|
|
2331
2606
|
done: false,
|
|
2332
|
-
value: new QuerySpanFacade(query.world, table,
|
|
2607
|
+
value: new QuerySpanFacade(query.world, table, start, rowIndex - start)
|
|
2333
2608
|
};
|
|
2334
2609
|
}
|
|
2335
2610
|
close(true);
|
|
@@ -2343,6 +2618,25 @@ var ExecutableQuery = class {
|
|
|
2343
2618
|
}
|
|
2344
2619
|
});
|
|
2345
2620
|
}
|
|
2621
|
+
[DERIVED_WRITER](component) {
|
|
2622
|
+
const reason = this.spanUnavailableReason();
|
|
2623
|
+
if (reason !== void 0) return err(new QuerySpanUnavailableError(reason));
|
|
2624
|
+
return ok(
|
|
2625
|
+
createDerivedRangeWriter(
|
|
2626
|
+
this.world,
|
|
2627
|
+
component,
|
|
2628
|
+
{
|
|
2629
|
+
structureEpoch: () => this.world[worldInternal].getStructureEpoch(),
|
|
2630
|
+
tables: () => {
|
|
2631
|
+
this.refreshMatches();
|
|
2632
|
+
return this.matchedTableObjects;
|
|
2633
|
+
},
|
|
2634
|
+
readComponents: this.compiled.descriptor.read ?? [],
|
|
2635
|
+
writeComponents: this.compiled.descriptor.write ?? []
|
|
2636
|
+
}
|
|
2637
|
+
)
|
|
2638
|
+
);
|
|
2639
|
+
}
|
|
2346
2640
|
combinations(k = 2) {
|
|
2347
2641
|
const query = this;
|
|
2348
2642
|
return {
|
|
@@ -2393,6 +2687,10 @@ var ExecutableQuery = class {
|
|
|
2393
2687
|
if (this.lastGraphGeneration === graph.generation) return;
|
|
2394
2688
|
this.matchedArchetypes = graph.archetypes.filter((archetype) => this.archetypeMatches(archetype)).map((archetype) => archetype.id);
|
|
2395
2689
|
this.matchedTables = graph.tables.filter((table) => this.tableMatches(table)).map((table) => table.id);
|
|
2690
|
+
this.matchedTableObjects = this.matchedTables.flatMap((id) => {
|
|
2691
|
+
const table = graph.tables[id];
|
|
2692
|
+
return table === void 0 ? [] : [table];
|
|
2693
|
+
});
|
|
2396
2694
|
this.lastGraphGeneration = graph.generation;
|
|
2397
2695
|
}
|
|
2398
2696
|
archetypeMatches(archetype) {
|
|
@@ -2431,11 +2729,23 @@ var ExecutableQuery = class {
|
|
|
2431
2729
|
}
|
|
2432
2730
|
return true;
|
|
2433
2731
|
}
|
|
2732
|
+
denseChangeMatches(table, row, upperBound) {
|
|
2733
|
+
for (const componentId2 of this.compiled.changedIds) {
|
|
2734
|
+
const epoch = table.storage.get(componentId2)?.epochs.changed[row] ?? 0;
|
|
2735
|
+
if (epoch <= this.lastObservedEpoch || epoch > upperBound) return false;
|
|
2736
|
+
}
|
|
2737
|
+
for (const componentId2 of this.compiled.addedIds) {
|
|
2738
|
+
const epoch = table.storage.get(componentId2)?.epochs.added[row] ?? 0;
|
|
2739
|
+
if (epoch <= this.lastObservedEpoch || epoch > upperBound) return false;
|
|
2740
|
+
}
|
|
2741
|
+
return true;
|
|
2742
|
+
}
|
|
2434
2743
|
spanUnavailableReason() {
|
|
2435
2744
|
if (this.compiled.sparseRoute) return "sparse-component";
|
|
2436
2745
|
if ((this.compiled.descriptor.optional?.length ?? 0) > 0) return "optional-data";
|
|
2437
|
-
if ((this.compiled.descriptor.
|
|
2438
|
-
return "
|
|
2746
|
+
if ((this.compiled.descriptor.write ?? []).some((component) => relationshipRole(component))) {
|
|
2747
|
+
return "relationship-component";
|
|
2748
|
+
}
|
|
2439
2749
|
return void 0;
|
|
2440
2750
|
}
|
|
2441
2751
|
};
|
|
@@ -3394,8 +3704,8 @@ var ComponentStorage = class {
|
|
|
3394
3704
|
get sharedRefs() {
|
|
3395
3705
|
return this.state.sharedRefs;
|
|
3396
3706
|
}
|
|
3397
|
-
routeError(
|
|
3398
|
-
this.state.routeError(
|
|
3707
|
+
routeError(err12, ctx) {
|
|
3708
|
+
this.state.routeError(err12, ctx);
|
|
3399
3709
|
}
|
|
3400
3710
|
readArrayView(arch, component, row, fieldName) {
|
|
3401
3711
|
const fieldCols = this.table(arch).storage.get(componentId(component))?.fields;
|
|
@@ -3423,6 +3733,52 @@ var ComponentStorage = class {
|
|
|
3423
3733
|
const elementCount = countCol?.view[row] ?? 0;
|
|
3424
3734
|
return reinterpretSlotBytes(liveBytes, arrayMeta.elementType, elementCount);
|
|
3425
3735
|
}
|
|
3736
|
+
/**
|
|
3737
|
+
* Read the live length of an array field without materialising a typed view.
|
|
3738
|
+
* Relationship consumers use this narrow storage seam for hot reverse-list
|
|
3739
|
+
* walks; the target array remains owned by the ECS column and no snapshot is
|
|
3740
|
+
* created per entity.
|
|
3741
|
+
*/
|
|
3742
|
+
readArrayLength(arch, component, row, fieldName) {
|
|
3743
|
+
const fieldCols = this.table(arch).storage.get(componentId(component))?.fields;
|
|
3744
|
+
if (fieldCols === void 0) return void 0;
|
|
3745
|
+
const arrayMeta = componentDefinition(component).fields[fieldName]?.arrayMeta;
|
|
3746
|
+
if (arrayMeta === void 0) return void 0;
|
|
3747
|
+
if (arrayMeta.length !== void 0) return arrayMeta.length;
|
|
3748
|
+
const count = fieldCols.get(arrayCountColumnName(fieldName))?.view[row];
|
|
3749
|
+
return typeof count === "number" ? count : 0;
|
|
3750
|
+
}
|
|
3751
|
+
/**
|
|
3752
|
+
* Read one array element directly from its backing column/BufferPool slot.
|
|
3753
|
+
* The relationship target vocabulary is `array<entity>`, so its hot path
|
|
3754
|
+
* decodes the packed u32 in-place and does not allocate a TypedArray view.
|
|
3755
|
+
*/
|
|
3756
|
+
readArrayElement(arch, component, row, fieldName, index) {
|
|
3757
|
+
if (!Number.isSafeInteger(index) || index < 0) return void 0;
|
|
3758
|
+
const fieldCols = this.table(arch).storage.get(componentId(component))?.fields;
|
|
3759
|
+
if (fieldCols === void 0) return void 0;
|
|
3760
|
+
const arrayMeta = componentDefinition(component).fields[fieldName]?.arrayMeta;
|
|
3761
|
+
if (arrayMeta === void 0) return void 0;
|
|
3762
|
+
const length = this.readArrayLength(arch, component, row, fieldName);
|
|
3763
|
+
if (length === void 0 || index >= length) return void 0;
|
|
3764
|
+
const col = fieldCols.get(fieldName);
|
|
3765
|
+
if (col === void 0) return void 0;
|
|
3766
|
+
if (arrayMeta.length !== void 0) {
|
|
3767
|
+
return col.view[row * col.arity + index];
|
|
3768
|
+
}
|
|
3769
|
+
const slotId = col.view[row];
|
|
3770
|
+
const bytes = this.bufferPool.view(slotId);
|
|
3771
|
+
if (arrayMeta.elementType === "entity") {
|
|
3772
|
+
const byteOffset = index * 4;
|
|
3773
|
+
if (byteOffset + 4 > bytes.byteLength) return void 0;
|
|
3774
|
+
return ((bytes[byteOffset] ?? 0) | (bytes[byteOffset + 1] ?? 0) << 8 | (bytes[byteOffset + 2] ?? 0) << 16 | (bytes[byteOffset + 3] ?? 0) << 24) >>> 0;
|
|
3775
|
+
}
|
|
3776
|
+
return this.readArrayElementAt(bytes, index, arrayMeta.elementType);
|
|
3777
|
+
}
|
|
3778
|
+
/** Read one scalar field without constructing a component shape. */
|
|
3779
|
+
readFieldValue(arch, component, row, fieldName) {
|
|
3780
|
+
return this.table(arch).storage.get(componentId(component))?.fields.get(fieldName)?.view[row];
|
|
3781
|
+
}
|
|
3426
3782
|
// ──────────────────────────────────────────────────────────────────────────
|
|
3427
3783
|
// Internal — archetype data read/write
|
|
3428
3784
|
// ──────────────────────────────────────────────────────────────────────────
|
|
@@ -3487,7 +3843,7 @@ var ComponentStorage = class {
|
|
|
3487
3843
|
if (!col) return;
|
|
3488
3844
|
col.view[row] = handle;
|
|
3489
3845
|
}
|
|
3490
|
-
writeRow(arch, component, row, value) {
|
|
3846
|
+
writeRow(arch, component, row, value, options) {
|
|
3491
3847
|
const localId = componentId(component);
|
|
3492
3848
|
const fieldCols = this.table(arch).storage.get(localId)?.fields;
|
|
3493
3849
|
if (!fieldCols) {
|
|
@@ -3536,6 +3892,12 @@ var ComponentStorage = class {
|
|
|
3536
3892
|
} else {
|
|
3537
3893
|
const arrayMeta = componentDefinition(component).fields[fieldName]?.arrayMeta;
|
|
3538
3894
|
if (arrayMeta !== void 0) {
|
|
3895
|
+
if (options?.skipVariableArrayInitialization === true && arrayMeta.length === void 0) {
|
|
3896
|
+
col.view[row] = 0;
|
|
3897
|
+
const countCol = fieldCols.get(arrayCountColumnName(fieldName));
|
|
3898
|
+
if (countCol !== void 0) countCol.view[row] = 0;
|
|
3899
|
+
continue;
|
|
3900
|
+
}
|
|
3539
3901
|
this.writeArrayField(arch, component, row, fieldName, fieldType, arrayMeta, raw);
|
|
3540
3902
|
} else {
|
|
3541
3903
|
col.view[row] = raw;
|
|
@@ -3762,11 +4124,11 @@ var ComponentStorage = class {
|
|
|
3762
4124
|
* the EcsError contract; this helper only attaches the systemName context
|
|
3763
4125
|
* so AI users can correlate the error with the holder component / field.
|
|
3764
4126
|
*/
|
|
3765
|
-
routeArrayError(
|
|
4127
|
+
routeArrayError(err12, componentName, fieldName) {
|
|
3766
4128
|
const ctx = {
|
|
3767
4129
|
systemName: `World.write (${componentName}.${fieldName})`
|
|
3768
4130
|
};
|
|
3769
|
-
this.routeError(
|
|
4131
|
+
this.routeError(err12, ctx);
|
|
3770
4132
|
}
|
|
3771
4133
|
/**
|
|
3772
4134
|
* Write the value of an `array<T>` / `array<T,N>` field at `row` (D-3 +
|
|
@@ -3911,20 +4273,19 @@ var ComponentStorage = class {
|
|
|
3911
4273
|
return readArrayElementAt(bytes, idx, elementType);
|
|
3912
4274
|
}
|
|
3913
4275
|
/**
|
|
3914
|
-
* Materialise a fresh `TypedArray`
|
|
3915
|
-
*
|
|
3916
|
-
*
|
|
3917
|
-
*
|
|
4276
|
+
* Materialise a fresh `TypedArray` view for an `array<T,N>` / `array<T>`
|
|
4277
|
+
* field at `row`. This is the internal row-storage path: the returned view
|
|
4278
|
+
* aliases the live column or BufferPool slot. `WorldComponentAccess.get`
|
|
4279
|
+
* detaches relationship target arrays at the public boundary; internal ECS
|
|
4280
|
+
* relationship maintenance and Scene traversal keep this zero-copy path.
|
|
3918
4281
|
*
|
|
3919
4282
|
* **Transient view contract (feat-20260602):** for fixed `array<T,N>`
|
|
3920
4283
|
* columns the returned `TypedArray` aliases the inline column buffer
|
|
3921
4284
|
* directly (`col.view.subarray(row * arity, ...)`); for variable
|
|
3922
4285
|
* `array<T>` columns it aliases the live `BufferPool` slot bytes
|
|
3923
|
-
* (zero-copy; `pool.view(slotId)` is the SSOT byte region). In both
|
|
3924
|
-
*
|
|
3925
|
-
*
|
|
3926
|
-
* and the implementation may switch to a copy in the future without
|
|
3927
|
-
* breaking AI users who consume only `length` / index reads.
|
|
4286
|
+
* (zero-copy; `pool.view(slotId)` is the SSOT byte region). In both cases
|
|
4287
|
+
* the view is valid only until the next structural change. Internal callers
|
|
4288
|
+
* must not write through it except via the owner mutation helpers.
|
|
3928
4289
|
*
|
|
3929
4290
|
* For variable arrays the typed-array length matches the live count from
|
|
3930
4291
|
* the sidecar `<fieldName>:count` column; for fixed arrays it matches the
|
|
@@ -4183,6 +4544,9 @@ function relationshipPayloadWrites(data) {
|
|
|
4183
4544
|
return true;
|
|
4184
4545
|
});
|
|
4185
4546
|
}
|
|
4547
|
+
function detachRelationshipTargetArray(value) {
|
|
4548
|
+
return value instanceof Uint32Array ? value.slice() : value;
|
|
4549
|
+
}
|
|
4186
4550
|
var WorldComponentAccess = class {
|
|
4187
4551
|
constructor(state) {
|
|
4188
4552
|
this.state = state;
|
|
@@ -4211,8 +4575,8 @@ var WorldComponentAccess = class {
|
|
|
4211
4575
|
get uniqueRefs() {
|
|
4212
4576
|
return this.state.uniqueRefs;
|
|
4213
4577
|
}
|
|
4214
|
-
routeError(
|
|
4215
|
-
this.state.routeError(
|
|
4578
|
+
routeError(err12, ctx) {
|
|
4579
|
+
this.state.routeError(err12, ctx);
|
|
4216
4580
|
}
|
|
4217
4581
|
relationshipIndex(component) {
|
|
4218
4582
|
if (relationshipRole(component)?.kind !== "source") return void 0;
|
|
@@ -4223,16 +4587,19 @@ var WorldComponentAccess = class {
|
|
|
4223
4587
|
}
|
|
4224
4588
|
return index;
|
|
4225
4589
|
}
|
|
4226
|
-
/**
|
|
4590
|
+
/**
|
|
4591
|
+
* Read the World-owned materialized target array without a public snapshot.
|
|
4592
|
+
* This is an internal relationship-owner path: it borrows the live
|
|
4593
|
+
* `Uint32Array` so attach/detach stays zero-copy. Public `World.get` detaches
|
|
4594
|
+
* target arrays before returning them to callers.
|
|
4595
|
+
*/
|
|
4227
4596
|
relationshipTargetEntries(source, target) {
|
|
4228
4597
|
const role = relationshipRole(source);
|
|
4229
4598
|
if (role?.kind !== "source") return [];
|
|
4230
4599
|
const mirror = relationshipMirror(source);
|
|
4231
4600
|
if (mirror === void 0) return [];
|
|
4232
|
-
const
|
|
4233
|
-
|
|
4234
|
-
const entries = result.value[role.targetField];
|
|
4235
|
-
return entries !== void 0 && typeof entries === "object" ? entries : [];
|
|
4601
|
+
const entries = this._getArrayView(target, mirror, role.targetField);
|
|
4602
|
+
return entries === void 0 ? [] : entries;
|
|
4236
4603
|
}
|
|
4237
4604
|
markComponentAdded(entity, component) {
|
|
4238
4605
|
this.state.markComponentAdded(entity, componentId(component));
|
|
@@ -4312,8 +4679,7 @@ var WorldComponentAccess = class {
|
|
|
4312
4679
|
const targetRecord = this.records[entityIndex(target)];
|
|
4313
4680
|
const actualGeneration = targetRecord?.generation ?? -1;
|
|
4314
4681
|
const targetLive = this.recordIsLive(targetRecord, entityGeneration(target));
|
|
4315
|
-
|
|
4316
|
-
if (!targetIsPending && !targetLive && !holderIsPending) {
|
|
4682
|
+
if (!targetIsPending && !targetLive) {
|
|
4317
4683
|
return err(
|
|
4318
4684
|
new StaleEntityError(target, entityIndex(target), entityGeneration(target), {
|
|
4319
4685
|
operation: "relationship-insert",
|
|
@@ -4370,7 +4736,14 @@ var WorldComponentAccess = class {
|
|
|
4370
4736
|
current = next;
|
|
4371
4737
|
}
|
|
4372
4738
|
}
|
|
4373
|
-
/**
|
|
4739
|
+
/**
|
|
4740
|
+
* Reserve target-side relationship capacity without touching World columns.
|
|
4741
|
+
*
|
|
4742
|
+
* A missing mirror reserves its first slot before the mirror archetype is
|
|
4743
|
+
* created. An existing mirror either reserves an empty slot or grows its
|
|
4744
|
+
* existing BufferPool slot; both failure paths return before any component,
|
|
4745
|
+
* relationship index, or ECS epoch changes.
|
|
4746
|
+
*/
|
|
4374
4747
|
prepareRelationshipInsert(component, value) {
|
|
4375
4748
|
const role = relationshipRole(component);
|
|
4376
4749
|
if (role?.kind !== "source") return ok(void 0);
|
|
@@ -4391,34 +4764,114 @@ var WorldComponentAccess = class {
|
|
|
4391
4764
|
);
|
|
4392
4765
|
}
|
|
4393
4766
|
const targetArch = this.graph.archetypes[targetRec.archetypeId];
|
|
4394
|
-
const
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4767
|
+
const mirrorLocalId = componentId(mirror);
|
|
4768
|
+
const mirrorPresent = targetArch?.components.some((candidate) => componentId(candidate) === mirrorLocalId) ?? false;
|
|
4769
|
+
const fieldName = role.targetField;
|
|
4770
|
+
const arrayMeta = componentDefinition(mirror).fields[fieldName]?.arrayMeta;
|
|
4771
|
+
if (arrayMeta === void 0) {
|
|
4772
|
+
return err(new ComponentNotPresentError(target, mirror.name));
|
|
4773
|
+
}
|
|
4774
|
+
const meta = TYPE_METADATA[arrayMeta.elementType];
|
|
4775
|
+
if (meta?.byteSize === void 0) {
|
|
4776
|
+
return err(new ComponentNotPresentError(target, mirror.name));
|
|
4777
|
+
}
|
|
4778
|
+
let currentLength = 0;
|
|
4779
|
+
let slotId = 0;
|
|
4780
|
+
if (mirrorPresent) {
|
|
4781
|
+
currentLength = this._getArrayLength(target, mirror, fieldName) ?? 0;
|
|
4782
|
+
const fieldCols = this.table(targetArch).storage.get(mirrorLocalId)?.fields;
|
|
4783
|
+
const column = fieldCols?.get(fieldName);
|
|
4784
|
+
if (column === void 0) {
|
|
4785
|
+
return err(new ComponentNotPresentError(target, mirror.name));
|
|
4786
|
+
}
|
|
4787
|
+
const row = this.tableRow(targetRec);
|
|
4788
|
+
slotId = column.view[row];
|
|
4789
|
+
}
|
|
4790
|
+
const requiredBytes = (currentLength + 1) * meta.byteSize;
|
|
4791
|
+
const maximum = Math.floor(262144 / meta.byteSize);
|
|
4792
|
+
if (!Number.isSafeInteger(currentLength + 1) || currentLength + 1 > maximum) {
|
|
4793
|
+
return err(new ManagedBufferOutOfBoundsError(requiredBytes, 262144));
|
|
4794
|
+
}
|
|
4795
|
+
const preparation = {
|
|
4796
|
+
target,
|
|
4797
|
+
mirror,
|
|
4798
|
+
fieldName,
|
|
4799
|
+
mirrorPresent,
|
|
4800
|
+
reservedSlotId: void 0
|
|
4801
|
+
};
|
|
4802
|
+
if (!mirrorPresent || slotId === 0) {
|
|
4803
|
+
const allocated = this.bufferPool.alloc(requiredBytes);
|
|
4804
|
+
if (!allocated.ok) return allocated;
|
|
4805
|
+
preparation.reservedSlotId = allocated.value.id;
|
|
4806
|
+
} else if (this.bufferPool.view(slotId).byteLength < requiredBytes) {
|
|
4807
|
+
const grown = this.bufferPool.grow(slotId, requiredBytes);
|
|
4808
|
+
if (!grown.ok) return grown;
|
|
4809
|
+
}
|
|
4810
|
+
return ok(preparation);
|
|
4811
|
+
}
|
|
4812
|
+
/** Release a target-capacity reservation that did not reach commit. */
|
|
4813
|
+
releaseRelationshipPreparation(preparation) {
|
|
4814
|
+
if (preparation === void 0 || preparation.reservedSlotId === void 0) return;
|
|
4815
|
+
const slotId = preparation.reservedSlotId;
|
|
4816
|
+
this.bufferPool.release(slotId);
|
|
4817
|
+
preparation.reservedSlotId = void 0;
|
|
4818
|
+
}
|
|
4819
|
+
/** Install a reserved slot after the target mirror archetype exists. */
|
|
4820
|
+
installRelationshipPreparation(preparation) {
|
|
4821
|
+
const record = this.lookupAlive(
|
|
4822
|
+
preparation.target,
|
|
4823
|
+
"relationship-capacity",
|
|
4824
|
+
preparation.mirror.name
|
|
4825
|
+
);
|
|
4826
|
+
if (!record.ok) return record;
|
|
4827
|
+
const arch = this.graph.archetypes[record.value.archetypeId];
|
|
4828
|
+
if (arch === void 0) {
|
|
4829
|
+
return err(
|
|
4830
|
+
new ComponentNotPresentError(preparation.target, preparation.mirror.name)
|
|
4831
|
+
);
|
|
4832
|
+
}
|
|
4833
|
+
const fieldCols = this.table(arch).storage.get(componentId(preparation.mirror))?.fields;
|
|
4834
|
+
const column = fieldCols?.get(preparation.fieldName);
|
|
4835
|
+
if (column === void 0) {
|
|
4836
|
+
return err(
|
|
4837
|
+
new ComponentNotPresentError(preparation.target, preparation.mirror.name)
|
|
4400
4838
|
);
|
|
4401
|
-
if (!added.ok) return added;
|
|
4402
4839
|
}
|
|
4403
|
-
const
|
|
4404
|
-
|
|
4840
|
+
const slotId = preparation.reservedSlotId;
|
|
4841
|
+
if (slotId !== void 0) {
|
|
4842
|
+
column.view[this.tableRow(record.value)] = slotId;
|
|
4843
|
+
preparation.reservedSlotId = void 0;
|
|
4844
|
+
}
|
|
4845
|
+
return ok(void 0);
|
|
4405
4846
|
}
|
|
4406
4847
|
/** Append `holder` to the materialized target list. */
|
|
4407
|
-
relationshipOnInsert(holder, component, value) {
|
|
4848
|
+
relationshipOnInsert(holder, component, value, preparation) {
|
|
4408
4849
|
const role = relationshipRole(component);
|
|
4409
4850
|
if (role?.kind !== "source") return ok(void 0);
|
|
4410
4851
|
const target = this.relationshipTargetEntity(component, value);
|
|
4411
4852
|
if (target === null) return ok(void 0);
|
|
4412
4853
|
const mirror = relationshipMirror(component);
|
|
4413
4854
|
if (mirror === void 0) return ok(void 0);
|
|
4414
|
-
|
|
4415
|
-
if (
|
|
4416
|
-
|
|
4417
|
-
return
|
|
4855
|
+
let prepared = preparation;
|
|
4856
|
+
if (prepared === void 0) {
|
|
4857
|
+
const preparedResult = this.prepareRelationshipInsert(component, value);
|
|
4858
|
+
if (!preparedResult.ok) return preparedResult;
|
|
4859
|
+
prepared = preparedResult.value;
|
|
4418
4860
|
}
|
|
4861
|
+
if (prepared === void 0) return ok(void 0);
|
|
4419
4862
|
const targetSlot = entityIndex(target);
|
|
4420
4863
|
const targetRec = this.records[targetSlot];
|
|
4421
|
-
|
|
4864
|
+
const actualGeneration = targetRec?.generation ?? -1;
|
|
4865
|
+
if (!this.recordIsLive(targetRec, entityGeneration(target))) {
|
|
4866
|
+
return err(
|
|
4867
|
+
new StaleEntityError(target, targetSlot, entityGeneration(target), {
|
|
4868
|
+
operation: "relationship-insert",
|
|
4869
|
+
component: component.name,
|
|
4870
|
+
expectedGeneration: entityGeneration(target),
|
|
4871
|
+
actualGeneration
|
|
4872
|
+
})
|
|
4873
|
+
);
|
|
4874
|
+
}
|
|
4422
4875
|
const targetArch = this.graph.archetypes[targetRec.archetypeId];
|
|
4423
4876
|
const mirrorLocalId = componentId(mirror);
|
|
4424
4877
|
const hasMirror = targetArch?.components.some((component2) => componentId(component2) === mirrorLocalId) ?? false;
|
|
@@ -4429,9 +4882,19 @@ var WorldComponentAccess = class {
|
|
|
4429
4882
|
component: mirror,
|
|
4430
4883
|
data: {}
|
|
4431
4884
|
},
|
|
4885
|
+
true,
|
|
4886
|
+
false,
|
|
4432
4887
|
true
|
|
4433
4888
|
);
|
|
4434
|
-
if (!added.ok)
|
|
4889
|
+
if (!added.ok) {
|
|
4890
|
+
this.releaseRelationshipPreparation(prepared);
|
|
4891
|
+
return added;
|
|
4892
|
+
}
|
|
4893
|
+
}
|
|
4894
|
+
const installed = this.installRelationshipPreparation(prepared);
|
|
4895
|
+
if (!installed.ok) {
|
|
4896
|
+
this.releaseRelationshipPreparation(prepared);
|
|
4897
|
+
return installed;
|
|
4435
4898
|
}
|
|
4436
4899
|
const targetEntries = this.relationshipTargetEntries(component, target);
|
|
4437
4900
|
const slot = targetEntries.length;
|
|
@@ -4441,7 +4904,10 @@ var WorldComponentAccess = class {
|
|
|
4441
4904
|
role.targetField,
|
|
4442
4905
|
holder
|
|
4443
4906
|
);
|
|
4444
|
-
if (!mirrored.ok)
|
|
4907
|
+
if (!mirrored.ok) {
|
|
4908
|
+
this.releaseRelationshipPreparation(prepared);
|
|
4909
|
+
return mirrored;
|
|
4910
|
+
}
|
|
4445
4911
|
this.relationshipIndex(component)?.attach(holder, target, slot);
|
|
4446
4912
|
return ok(void 0);
|
|
4447
4913
|
}
|
|
@@ -4474,15 +4940,16 @@ var WorldComponentAccess = class {
|
|
|
4474
4940
|
/**
|
|
4475
4941
|
* Read component data from an entity.
|
|
4476
4942
|
*
|
|
4477
|
-
* **
|
|
4478
|
-
* `
|
|
4479
|
-
*
|
|
4480
|
-
*
|
|
4481
|
-
* `
|
|
4482
|
-
*
|
|
4483
|
-
*
|
|
4484
|
-
*
|
|
4485
|
-
* `
|
|
4943
|
+
* **Public array contract:** relationship target `array<entity>` fields are
|
|
4944
|
+
* detached `Uint32Array` copies. Mutating that returned array cannot alter
|
|
4945
|
+
* the materialized target, relationship index, or source. Other array
|
|
4946
|
+
* fields retain the existing transient view contract: fixed-capacity
|
|
4947
|
+
* `array<T,N>` and `buffer<N>` fields alias the archetype column buffer
|
|
4948
|
+
* directly, while variable managed arrays alias their BufferPool slot. Those
|
|
4949
|
+
* views are valid only until the next structural change (`spawn` /
|
|
4950
|
+
* `despawn` / `addComponent` / `removeComponent`); callers must re-fetch
|
|
4951
|
+
* `world.get(e, C)` on every access. Internal owners use `readRow` and
|
|
4952
|
+
* `_getArrayView` directly and retain zero-copy access.
|
|
4486
4953
|
*
|
|
4487
4954
|
* @returns `Result<ShapeOf<S>, EcsError>` —
|
|
4488
4955
|
* `ok(ShapeOf<S>)` on success;
|
|
@@ -4522,7 +4989,13 @@ var WorldComponentAccess = class {
|
|
|
4522
4989
|
if (!arch.components.some((candidate) => componentId(candidate) === localId)) {
|
|
4523
4990
|
return err(new ComponentNotPresentError(entity, component.name));
|
|
4524
4991
|
}
|
|
4525
|
-
|
|
4992
|
+
const value = this.storage.readRow(arch, component, this.tableRow(rec));
|
|
4993
|
+
const role = relationshipRole(component);
|
|
4994
|
+
if (role?.kind === "target") {
|
|
4995
|
+
const targetValue = value;
|
|
4996
|
+
targetValue[role.targetField] = detachRelationshipTargetArray(targetValue[role.targetField]);
|
|
4997
|
+
}
|
|
4998
|
+
return ok(value);
|
|
4526
4999
|
}
|
|
4527
5000
|
/**
|
|
4528
5001
|
* Column-level zero-copy view of an `array<T, N>` / `array<T>` field.
|
|
@@ -4556,9 +5029,10 @@ var WorldComponentAccess = class {
|
|
|
4556
5029
|
* Returns `undefined` when the entity is dead, the component is absent, the
|
|
4557
5030
|
* field does not exist, or the field is not an `array<...>` column.
|
|
4558
5031
|
*
|
|
4559
|
-
* @internal Engine-internal fast path; AI users read
|
|
4560
|
-
* `world.get
|
|
4561
|
-
*
|
|
5032
|
+
* @internal Engine-internal fast path; AI users read public component values
|
|
5033
|
+
* through `world.get`. This accessor is the zero-materialization route the
|
|
5034
|
+
* propagate kernel, relationship owner, and render walk use; it bypasses
|
|
5035
|
+
* the detached public relationship-target snapshot.
|
|
4562
5036
|
*/
|
|
4563
5037
|
_getArrayView(entity, component, fieldName) {
|
|
4564
5038
|
const record = this.lookupAlive(entity, "_getArrayView", component.name);
|
|
@@ -4568,6 +5042,109 @@ var WorldComponentAccess = class {
|
|
|
4568
5042
|
if (!arch) return void 0;
|
|
4569
5043
|
return this.storage.readArrayView(arch, component, this.tableRow(rec), fieldName);
|
|
4570
5044
|
}
|
|
5045
|
+
/** Internal zero-materialisation read for ECS-owned relationship lists. */
|
|
5046
|
+
_getArrayLength(entity, component, fieldName) {
|
|
5047
|
+
const record = this.lookupAlive(entity, "relationship-read", component.name);
|
|
5048
|
+
if (!record.ok) return void 0;
|
|
5049
|
+
const arch = this.graph.archetypes[record.value.archetypeId];
|
|
5050
|
+
if (arch === void 0) return void 0;
|
|
5051
|
+
return this.storage.readArrayLength(arch, component, this.tableRow(record.value), fieldName);
|
|
5052
|
+
}
|
|
5053
|
+
/** Internal zero-materialisation read for one ECS-owned array element. */
|
|
5054
|
+
_getArrayElement(entity, component, fieldName, index) {
|
|
5055
|
+
const record = this.lookupAlive(entity, "relationship-read", component.name);
|
|
5056
|
+
if (!record.ok) return void 0;
|
|
5057
|
+
const arch = this.graph.archetypes[record.value.archetypeId];
|
|
5058
|
+
if (arch === void 0) return void 0;
|
|
5059
|
+
return this.storage.readArrayElement(
|
|
5060
|
+
arch,
|
|
5061
|
+
component,
|
|
5062
|
+
this.tableRow(record.value),
|
|
5063
|
+
fieldName,
|
|
5064
|
+
index
|
|
5065
|
+
);
|
|
5066
|
+
}
|
|
5067
|
+
/** Internal scalar-column read used by parent-first hierarchy traversal. */
|
|
5068
|
+
_getFieldValue(entity, component, fieldName) {
|
|
5069
|
+
const record = this.lookupAlive(entity, "relationship-read", component.name);
|
|
5070
|
+
if (!record.ok) return void 0;
|
|
5071
|
+
const arch = this.graph.archetypes[record.value.archetypeId];
|
|
5072
|
+
if (arch === void 0) return void 0;
|
|
5073
|
+
return this.storage.readFieldValue(arch, component, this.tableRow(record.value), fieldName);
|
|
5074
|
+
}
|
|
5075
|
+
/**
|
|
5076
|
+
* Converge one writable relationship source mutation through the source
|
|
5077
|
+
* owner. The source scalar and its materialized target list are committed as
|
|
5078
|
+
* one operation; no caller receives a raw source column view that could
|
|
5079
|
+
* bypass the mirror/index maintenance.
|
|
5080
|
+
*/
|
|
5081
|
+
setRelationshipSource(entity, component, value, record, arch, markChanged) {
|
|
5082
|
+
const role = relationshipRole(component);
|
|
5083
|
+
if (role?.kind !== "source") return ok(void 0);
|
|
5084
|
+
const row = this.tableRow(record);
|
|
5085
|
+
const current = this.storage.readRow(arch, component, row);
|
|
5086
|
+
const valuePreflight = this.preflightComponentFieldValues(entity, {
|
|
5087
|
+
component,
|
|
5088
|
+
data: value
|
|
5089
|
+
});
|
|
5090
|
+
if (!valuePreflight.ok) return valuePreflight;
|
|
5091
|
+
const merged = { ...current, ...value };
|
|
5092
|
+
const enumError = validateEnumFieldValues(component, merged, entity);
|
|
5093
|
+
if (enumError !== null) return err(enumError);
|
|
5094
|
+
const oldTarget = this.relationshipTargetEntity(component, current);
|
|
5095
|
+
const target = this.relationshipTargetEntity(component, merged);
|
|
5096
|
+
if (target !== null) {
|
|
5097
|
+
const targetRecord = this.records[entityIndex(target)];
|
|
5098
|
+
const actualGeneration = targetRecord?.generation ?? -1;
|
|
5099
|
+
if (!this.recordIsLive(targetRecord, entityGeneration(target))) {
|
|
5100
|
+
return err(
|
|
5101
|
+
new StaleEntityError(target, entityIndex(target), entityGeneration(target), {
|
|
5102
|
+
operation: "relationship-insert",
|
|
5103
|
+
component: component.name,
|
|
5104
|
+
expectedGeneration: entityGeneration(target),
|
|
5105
|
+
actualGeneration
|
|
5106
|
+
})
|
|
5107
|
+
);
|
|
5108
|
+
}
|
|
5109
|
+
const roleAllowsSelf = role.allowSelf;
|
|
5110
|
+
if (entity === target && !roleAllowsSelf) {
|
|
5111
|
+
return err(
|
|
5112
|
+
new RelationshipSelfCycleError(component.name, entity, target)
|
|
5113
|
+
);
|
|
5114
|
+
}
|
|
5115
|
+
const cycleHit = entity === target && roleAllowsSelf ? null : this.relationshipCycleHit(component, target, entity);
|
|
5116
|
+
if (cycleHit !== null) {
|
|
5117
|
+
return err(
|
|
5118
|
+
new RelationshipSelfCycleError(component.name, entity, cycleHit)
|
|
5119
|
+
);
|
|
5120
|
+
}
|
|
5121
|
+
}
|
|
5122
|
+
if (oldTarget !== target) {
|
|
5123
|
+
const prepared = this.prepareRelationshipInsert(component, merged);
|
|
5124
|
+
if (!prepared.ok) return prepared;
|
|
5125
|
+
const preparation = prepared.value;
|
|
5126
|
+
if (oldTarget !== null) {
|
|
5127
|
+
const detached = this.relationshipOnRemove(entity, component, current);
|
|
5128
|
+
if (!detached.ok) {
|
|
5129
|
+
this.releaseRelationshipPreparation(preparation);
|
|
5130
|
+
return detached;
|
|
5131
|
+
}
|
|
5132
|
+
}
|
|
5133
|
+
const attached = this.relationshipOnInsert(entity, component, merged, preparation);
|
|
5134
|
+
if (!attached.ok) {
|
|
5135
|
+
this.releaseRelationshipPreparation(preparation);
|
|
5136
|
+
return attached;
|
|
5137
|
+
}
|
|
5138
|
+
}
|
|
5139
|
+
const fieldCols = this.table(arch).storage.get(componentId(component))?.fields;
|
|
5140
|
+
const sourceColumn = fieldCols?.get(role.sourceField);
|
|
5141
|
+
if (sourceColumn === void 0) {
|
|
5142
|
+
return err(new ComponentNotPresentError(entity, component.name));
|
|
5143
|
+
}
|
|
5144
|
+
sourceColumn.view[row] = target === null ? ENTITY_NULL_RAW : target;
|
|
5145
|
+
if (markChanged) this.markComponentChanged(entity, component);
|
|
5146
|
+
return ok(void 0);
|
|
5147
|
+
}
|
|
4571
5148
|
/**
|
|
4572
5149
|
* Write (partial) component data to an entity.
|
|
4573
5150
|
*
|
|
@@ -4606,6 +5183,20 @@ var WorldComponentAccess = class {
|
|
|
4606
5183
|
if (!arch.components.some((candidate) => componentId(candidate) === localId)) {
|
|
4607
5184
|
return err(new ComponentNotPresentError(entity, component.name));
|
|
4608
5185
|
}
|
|
5186
|
+
const role = relationshipRole(component);
|
|
5187
|
+
if (role?.kind === "target") {
|
|
5188
|
+
return err(new RelationshipTargetReadonlyError(component.name, "set"));
|
|
5189
|
+
}
|
|
5190
|
+
if (role?.kind === "source") {
|
|
5191
|
+
return this.setRelationshipSource(
|
|
5192
|
+
entity,
|
|
5193
|
+
component,
|
|
5194
|
+
value,
|
|
5195
|
+
rec,
|
|
5196
|
+
arch,
|
|
5197
|
+
markChanged
|
|
5198
|
+
);
|
|
5199
|
+
}
|
|
4609
5200
|
const valuePreflight = this.preflightComponentFieldValues(entity, {
|
|
4610
5201
|
component,
|
|
4611
5202
|
data: value
|
|
@@ -4772,51 +5363,6 @@ var WorldComponentAccess = class {
|
|
|
4772
5363
|
this.markComponentChanged(entity, component);
|
|
4773
5364
|
return ok(void 0);
|
|
4774
5365
|
}
|
|
4775
|
-
ensureArrayCapacity(entity, component, fieldName, minimum) {
|
|
4776
|
-
const record = this.lookupAlive(entity, "relationship-capacity", component.name);
|
|
4777
|
-
if (!record.ok) return record;
|
|
4778
|
-
const rec = record.value;
|
|
4779
|
-
const arch = this.graph.archetypes[rec.archetypeId];
|
|
4780
|
-
if (!arch) {
|
|
4781
|
-
return err(
|
|
4782
|
-
new StaleEntityError(entity, entityIndex(entity), entityGeneration(entity), {
|
|
4783
|
-
operation: "relationship-capacity",
|
|
4784
|
-
component: component.name,
|
|
4785
|
-
expectedGeneration: entityGeneration(entity),
|
|
4786
|
-
actualGeneration: rec.generation
|
|
4787
|
-
})
|
|
4788
|
-
);
|
|
4789
|
-
}
|
|
4790
|
-
const fieldCols = this.table(arch).storage.get(componentId(component))?.fields;
|
|
4791
|
-
if (!fieldCols) return err(new ComponentNotPresentError(entity, component.name));
|
|
4792
|
-
const fieldNameStr = fieldName;
|
|
4793
|
-
const col = fieldCols.get(fieldNameStr);
|
|
4794
|
-
if (!col) return err(new ComponentNotPresentError(entity, component.name));
|
|
4795
|
-
const arrayMeta = componentDefinition(component).fields[fieldNameStr]?.arrayMeta;
|
|
4796
|
-
if (arrayMeta === void 0) {
|
|
4797
|
-
return err(new ComponentNotPresentError(entity, component.name));
|
|
4798
|
-
}
|
|
4799
|
-
const meta = TYPE_METADATA[arrayMeta.elementType];
|
|
4800
|
-
if (!meta?.byteSize) {
|
|
4801
|
-
return err(new ComponentNotPresentError(entity, component.name));
|
|
4802
|
-
}
|
|
4803
|
-
const maximum = Math.floor(262144 / meta.byteSize);
|
|
4804
|
-
if (!Number.isSafeInteger(minimum) || minimum < 0 || minimum > maximum) {
|
|
4805
|
-
return err(new ManagedBufferOutOfBoundsError(minimum, maximum));
|
|
4806
|
-
}
|
|
4807
|
-
const byteLength = minimum * meta.byteSize;
|
|
4808
|
-
const slotId = col.view[this.tableRow(rec)];
|
|
4809
|
-
if (slotId === 0) {
|
|
4810
|
-
if (minimum === 0) return ok(void 0);
|
|
4811
|
-
const allocated = this.bufferPool.alloc(byteLength);
|
|
4812
|
-
if (!allocated.ok) return allocated;
|
|
4813
|
-
col.view[this.tableRow(rec)] = allocated.value.id;
|
|
4814
|
-
return ok(void 0);
|
|
4815
|
-
}
|
|
4816
|
-
if (this.bufferPool.view(slotId).byteLength >= byteLength) return ok(void 0);
|
|
4817
|
-
const grown = this.bufferPool.grow(slotId, byteLength);
|
|
4818
|
-
return grown.ok ? ok(void 0) : grown;
|
|
4819
|
-
}
|
|
4820
5366
|
/**
|
|
4821
5367
|
* Remove one variable-array element at a known slot. Relationship holders
|
|
4822
5368
|
* supply the slot from their backpointer, so this is O(1) and never scans
|
|
@@ -4884,11 +5430,11 @@ var WorldComponentAccess = class {
|
|
|
4884
5430
|
* (lazy mirror create or exclusive reparent).
|
|
4885
5431
|
* @internal
|
|
4886
5432
|
*/
|
|
4887
|
-
_addComponentCore(entity, componentData, internal) {
|
|
5433
|
+
_addComponentCore(entity, componentData, internal, resolveRequirements = true, skipVariableArrayInitialization = false) {
|
|
4888
5434
|
const record = this.lookupAlive(entity, "addComponent", componentData.component.name);
|
|
4889
5435
|
if (!record.ok) return record;
|
|
4890
5436
|
const rec = record.value;
|
|
4891
|
-
|
|
5437
|
+
let srcArch = this.graph.archetypes[rec.archetypeId];
|
|
4892
5438
|
if (!srcArch) {
|
|
4893
5439
|
return err(
|
|
4894
5440
|
new StaleEntityError(entity, entityIndex(entity), entityGeneration(entity), {
|
|
@@ -4931,31 +5477,60 @@ var WorldComponentAccess = class {
|
|
|
4931
5477
|
return err(enumErr);
|
|
4932
5478
|
}
|
|
4933
5479
|
const localId = componentId(componentData.component);
|
|
5480
|
+
let relationshipPreparation;
|
|
5481
|
+
const componentAlreadyPresent = srcArch.components.some(
|
|
5482
|
+
(candidate) => componentId(candidate) === localId
|
|
5483
|
+
);
|
|
5484
|
+
if (!internal && !componentAlreadyPresent && relationshipRole(componentData.component)?.kind === "source") {
|
|
5485
|
+
const prepared = this.prepareRelationshipInsert(
|
|
5486
|
+
componentData.component,
|
|
5487
|
+
filled
|
|
5488
|
+
);
|
|
5489
|
+
if (!prepared.ok) return prepared;
|
|
5490
|
+
relationshipPreparation = prepared.value;
|
|
5491
|
+
}
|
|
5492
|
+
if (resolveRequirements) {
|
|
5493
|
+
const required = expandComponentRequirements([componentData]).slice(1);
|
|
5494
|
+
for (const requirement of required) {
|
|
5495
|
+
if (srcArch.components.some(
|
|
5496
|
+
(candidate) => componentId(candidate) === componentId(requirement.component)
|
|
5497
|
+
)) {
|
|
5498
|
+
continue;
|
|
5499
|
+
}
|
|
5500
|
+
const added = this._addComponentCore(entity, requirement, internal, false);
|
|
5501
|
+
if (!added.ok) {
|
|
5502
|
+
this.releaseRelationshipPreparation(relationshipPreparation);
|
|
5503
|
+
return added;
|
|
5504
|
+
}
|
|
5505
|
+
srcArch = this.graph.archetypes[rec.archetypeId];
|
|
5506
|
+
if (!srcArch) {
|
|
5507
|
+
this.releaseRelationshipPreparation(relationshipPreparation);
|
|
5508
|
+
return err(
|
|
5509
|
+
new StaleEntityError(entity, entityIndex(entity), entityGeneration(entity), {
|
|
5510
|
+
operation: "addComponent",
|
|
5511
|
+
component: componentData.component.name,
|
|
5512
|
+
expectedGeneration: rec.generation,
|
|
5513
|
+
actualGeneration: rec.generation
|
|
5514
|
+
})
|
|
5515
|
+
);
|
|
5516
|
+
}
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
4934
5519
|
if (srcArch.components.some((candidate) => componentId(candidate) === localId)) {
|
|
5520
|
+
this.releaseRelationshipPreparation(relationshipPreparation);
|
|
4935
5521
|
const role = relationshipRole(componentData.component);
|
|
4936
5522
|
if (role?.kind === "source" && role.exclusive && !internal) {
|
|
4937
|
-
|
|
4938
|
-
componentData.component,
|
|
4939
|
-
filled
|
|
4940
|
-
);
|
|
4941
|
-
if (!prepared.ok) return prepared;
|
|
4942
|
-
const removeR = this._removeComponentCore(
|
|
5523
|
+
return this.setRelationshipSource(
|
|
4943
5524
|
entity,
|
|
4944
5525
|
componentData.component,
|
|
4945
|
-
|
|
5526
|
+
filled,
|
|
5527
|
+
rec,
|
|
5528
|
+
srcArch,
|
|
5529
|
+
true
|
|
4946
5530
|
);
|
|
4947
|
-
if (!removeR.ok) return removeR;
|
|
4948
|
-
return this._addComponentCore(entity, componentData, false);
|
|
4949
5531
|
}
|
|
4950
5532
|
return err(new ComponentAlreadyPresentError(entity, componentData.component.name));
|
|
4951
5533
|
}
|
|
4952
|
-
if (!internal && relationshipRole(componentData.component)?.kind === "source") {
|
|
4953
|
-
const prepared = this.prepareRelationshipInsert(
|
|
4954
|
-
componentData.component,
|
|
4955
|
-
filled
|
|
4956
|
-
);
|
|
4957
|
-
if (!prepared.ok) return prepared;
|
|
4958
|
-
}
|
|
4959
5534
|
const targetArch = getAddEdge(
|
|
4960
5535
|
this.graph,
|
|
4961
5536
|
srcArch,
|
|
@@ -4972,7 +5547,8 @@ var WorldComponentAccess = class {
|
|
|
4972
5547
|
targetArch,
|
|
4973
5548
|
componentData.component,
|
|
4974
5549
|
this.tableRow(rec),
|
|
4975
|
-
filled
|
|
5550
|
+
filled,
|
|
5551
|
+
skipVariableArrayInitialization ? { skipVariableArrayInitialization: true } : void 0
|
|
4976
5552
|
);
|
|
4977
5553
|
}
|
|
4978
5554
|
this.markComponentAdded(entity, componentData.component);
|
|
@@ -4980,11 +5556,20 @@ var WorldComponentAccess = class {
|
|
|
4980
5556
|
const relationshipResult = this.relationshipOnInsert(
|
|
4981
5557
|
entity,
|
|
4982
5558
|
componentData.component,
|
|
4983
|
-
filled
|
|
5559
|
+
filled,
|
|
5560
|
+
relationshipPreparation
|
|
4984
5561
|
);
|
|
4985
|
-
if (!relationshipResult.ok)
|
|
5562
|
+
if (!relationshipResult.ok) {
|
|
5563
|
+
this.releaseRelationshipPreparation(relationshipPreparation);
|
|
5564
|
+
return relationshipResult;
|
|
5565
|
+
}
|
|
4986
5566
|
}
|
|
4987
5567
|
this.markStructureChanged();
|
|
5568
|
+
this.state.recordStructuralEvidence({
|
|
5569
|
+
kind: "component-added",
|
|
5570
|
+
entity,
|
|
5571
|
+
componentId: localId
|
|
5572
|
+
});
|
|
4988
5573
|
return ok(void 0);
|
|
4989
5574
|
}
|
|
4990
5575
|
/**
|
|
@@ -5062,8 +5647,12 @@ var WorldComponentAccess = class {
|
|
|
5062
5647
|
} else {
|
|
5063
5648
|
this.storage.migrateEntity(rec, srcArch, targetArch);
|
|
5064
5649
|
}
|
|
5065
|
-
this.state.removeComponentChange(entity, componentId(component));
|
|
5066
5650
|
this.markStructureChanged();
|
|
5651
|
+
this.state.recordStructuralEvidence({
|
|
5652
|
+
kind: "component-removed",
|
|
5653
|
+
entity,
|
|
5654
|
+
componentId: localId
|
|
5655
|
+
});
|
|
5067
5656
|
return ok(void 0);
|
|
5068
5657
|
}
|
|
5069
5658
|
// ──────────────────────────────────────────────────────────────────────────
|
|
@@ -5097,9 +5686,39 @@ var WorldComponentAccess = class {
|
|
|
5097
5686
|
* Idempotent: a record with archetypeId !== -1 is already materialized.
|
|
5098
5687
|
*/
|
|
5099
5688
|
_materializePendingEntity(entity, componentDatas) {
|
|
5689
|
+
componentDatas = expandComponentRequirements(componentDatas);
|
|
5100
5690
|
const slot = entityIndex(entity);
|
|
5101
5691
|
const record = this.records[slot];
|
|
5102
5692
|
if (!record || record.archetypeId !== -1) return ok(void 0);
|
|
5693
|
+
const relationshipPreparations = [];
|
|
5694
|
+
for (let index = 0; index < componentDatas.length; index += 1) {
|
|
5695
|
+
const componentData = componentDatas[index];
|
|
5696
|
+
if (componentData === void 0 || relationshipRole(componentData.component)?.kind !== "source") {
|
|
5697
|
+
continue;
|
|
5698
|
+
}
|
|
5699
|
+
const filled = fillComponentDefaults(
|
|
5700
|
+
componentData.component,
|
|
5701
|
+
componentData.data
|
|
5702
|
+
);
|
|
5703
|
+
const target = this.relationshipTargetEntity(
|
|
5704
|
+
componentData.component,
|
|
5705
|
+
filled
|
|
5706
|
+
);
|
|
5707
|
+
const targetRecord = target === null ? void 0 : this.records[entityIndex(target)];
|
|
5708
|
+
if (target !== null && targetRecord !== void 0 && this.recordIsLive(targetRecord, entityGeneration(target))) {
|
|
5709
|
+
const prepared = this.prepareRelationshipInsert(
|
|
5710
|
+
componentData.component,
|
|
5711
|
+
filled
|
|
5712
|
+
);
|
|
5713
|
+
if (!prepared.ok) {
|
|
5714
|
+
for (const reservation of relationshipPreparations) {
|
|
5715
|
+
this.releaseRelationshipPreparation(reservation);
|
|
5716
|
+
}
|
|
5717
|
+
return prepared;
|
|
5718
|
+
}
|
|
5719
|
+
relationshipPreparations[index] = prepared.value;
|
|
5720
|
+
}
|
|
5721
|
+
}
|
|
5103
5722
|
const componentIds = componentDatas.map((cd) => componentId(cd.component));
|
|
5104
5723
|
const components = componentDatas.map((cd) => cd.component);
|
|
5105
5724
|
const arch = getOrCreateArchetype(this.graph, componentIds, components);
|
|
@@ -5117,20 +5736,30 @@ var WorldComponentAccess = class {
|
|
|
5117
5736
|
this.storage.writeRow(arch, cd.component, tableRow2, filled);
|
|
5118
5737
|
}
|
|
5119
5738
|
this.storage.writeEntitySelf(arch, tableRow2, entity);
|
|
5120
|
-
for (
|
|
5739
|
+
for (let index = 0; index < componentDatas.length; index += 1) {
|
|
5740
|
+
const cd = componentDatas[index];
|
|
5741
|
+
if (cd === void 0) continue;
|
|
5121
5742
|
if (relationshipRole(cd.component)?.kind === "source") {
|
|
5122
5743
|
const filled = fillComponentDefaults(cd.component, cd.data);
|
|
5123
5744
|
const relationshipResult = this.relationshipOnInsert(
|
|
5124
5745
|
entity,
|
|
5125
5746
|
cd.component,
|
|
5126
|
-
filled
|
|
5747
|
+
filled,
|
|
5748
|
+
relationshipPreparations[index]
|
|
5127
5749
|
);
|
|
5128
5750
|
if (!relationshipResult.ok) {
|
|
5751
|
+
for (const reservation of relationshipPreparations) {
|
|
5752
|
+
this.releaseRelationshipPreparation(reservation);
|
|
5753
|
+
}
|
|
5129
5754
|
return relationshipResult;
|
|
5130
5755
|
}
|
|
5131
5756
|
}
|
|
5132
5757
|
}
|
|
5133
5758
|
this.markStructureChanged();
|
|
5759
|
+
this.state.recordStructuralEvidence({
|
|
5760
|
+
kind: "spawn",
|
|
5761
|
+
entity
|
|
5762
|
+
});
|
|
5134
5763
|
return ok(void 0);
|
|
5135
5764
|
}
|
|
5136
5765
|
// ──────────────────────────────────────────────────────────────────────────
|
|
@@ -5411,7 +6040,7 @@ var BufferPool = class {
|
|
|
5411
6040
|
return n;
|
|
5412
6041
|
}
|
|
5413
6042
|
};
|
|
5414
|
-
var
|
|
6043
|
+
var SHARED_REF_RELEASE_EVIDENCE_CAPACITY = 4096;
|
|
5415
6044
|
var SharedRefStore = class {
|
|
5416
6045
|
payloads = /* @__PURE__ */ new Map();
|
|
5417
6046
|
refcounts = /* @__PURE__ */ new Map();
|
|
@@ -5420,7 +6049,8 @@ var SharedRefStore = class {
|
|
|
5420
6049
|
internedKeys = /* @__PURE__ */ new Map();
|
|
5421
6050
|
nextSlot = BUILTIN_BASE;
|
|
5422
6051
|
mutationEpoch = 0;
|
|
5423
|
-
|
|
6052
|
+
/** Latest published mutation epoch per live handle; not an event journal. */
|
|
6053
|
+
mutationEpochs = /* @__PURE__ */ new Map();
|
|
5424
6054
|
releaseJournal = [];
|
|
5425
6055
|
/**
|
|
5426
6056
|
* Generation table indexed by slot (D-6). Each entry tracks the current
|
|
@@ -5524,26 +6154,23 @@ var SharedRefStore = class {
|
|
|
5524
6154
|
throw new RangeError("SharedRefStore mutation epoch exhausted");
|
|
5525
6155
|
}
|
|
5526
6156
|
this.mutationEpoch += 1;
|
|
5527
|
-
this.
|
|
5528
|
-
if (this.mutationJournal.length > SHARED_REF_MUTATION_JOURNAL_CAPACITY) {
|
|
5529
|
-
this.mutationJournal.shift();
|
|
5530
|
-
}
|
|
6157
|
+
this.mutationEpochs.set(unwrapHandle(handle), this.mutationEpoch);
|
|
5531
6158
|
return ok(void 0);
|
|
5532
6159
|
}
|
|
5533
6160
|
/** Current upper bound for explicitly published payload mutations. */
|
|
5534
6161
|
getMutationEpoch() {
|
|
5535
6162
|
return this.mutationEpoch;
|
|
5536
6163
|
}
|
|
5537
|
-
/** Read
|
|
6164
|
+
/** Read each live handle whose latest published mutation is after `cursor`. */
|
|
5538
6165
|
readChangesSince(cursor) {
|
|
5539
|
-
const
|
|
5540
|
-
|
|
5541
|
-
|
|
6166
|
+
const records = [];
|
|
6167
|
+
for (const [handle, epoch] of this.mutationEpochs) {
|
|
6168
|
+
if (epoch > cursor) records.push({ epoch, handle });
|
|
5542
6169
|
}
|
|
6170
|
+
records.sort((left, right) => left.epoch - right.epoch || left.handle - right.handle);
|
|
5543
6171
|
return {
|
|
5544
|
-
status: "ok",
|
|
5545
6172
|
cursor: this.mutationEpoch,
|
|
5546
|
-
records
|
|
6173
|
+
records
|
|
5547
6174
|
};
|
|
5548
6175
|
}
|
|
5549
6176
|
/**
|
|
@@ -5609,6 +6236,7 @@ var SharedRefStore = class {
|
|
|
5609
6236
|
}
|
|
5610
6237
|
this.refcounts.delete(raw);
|
|
5611
6238
|
this.payloads.delete(raw);
|
|
6239
|
+
this.mutationEpochs.delete(raw);
|
|
5612
6240
|
const generation = storeGen + 1;
|
|
5613
6241
|
this._generations[slot] = generation;
|
|
5614
6242
|
if (!isRetiredSlot(generation)) {
|
|
@@ -5621,7 +6249,7 @@ var SharedRefStore = class {
|
|
|
5621
6249
|
evidence: "released"
|
|
5622
6250
|
});
|
|
5623
6251
|
this.releaseJournal.push(evidence);
|
|
5624
|
-
if (this.releaseJournal.length >
|
|
6252
|
+
if (this.releaseJournal.length > SHARED_REF_RELEASE_EVIDENCE_CAPACITY) {
|
|
5625
6253
|
this.releaseJournal.shift();
|
|
5626
6254
|
}
|
|
5627
6255
|
return ok(evidence);
|
|
@@ -5645,6 +6273,47 @@ var SharedRefStore = class {
|
|
|
5645
6273
|
return this.payloads.size;
|
|
5646
6274
|
}
|
|
5647
6275
|
};
|
|
6276
|
+
|
|
6277
|
+
// src/storage/structural-evidence.ts
|
|
6278
|
+
var StructuralEvidenceRing = class {
|
|
6279
|
+
constructor(capacity = 1024) {
|
|
6280
|
+
this.capacity = capacity;
|
|
6281
|
+
if (!Number.isSafeInteger(capacity) || capacity <= 0) {
|
|
6282
|
+
throw new RangeError("StructuralEvidenceRing capacity must be a positive safe integer");
|
|
6283
|
+
}
|
|
6284
|
+
this.events = new Array(capacity);
|
|
6285
|
+
}
|
|
6286
|
+
capacity;
|
|
6287
|
+
events;
|
|
6288
|
+
nextSequence = 1;
|
|
6289
|
+
get cursor() {
|
|
6290
|
+
return this.nextSequence - 1;
|
|
6291
|
+
}
|
|
6292
|
+
append(input) {
|
|
6293
|
+
const sequence = this.nextSequence;
|
|
6294
|
+
this.nextSequence += 1;
|
|
6295
|
+
this.events[(sequence - 1) % this.capacity] = { ...input, sequence };
|
|
6296
|
+
return sequence;
|
|
6297
|
+
}
|
|
6298
|
+
readAfter(cursor) {
|
|
6299
|
+
const latest = this.cursor;
|
|
6300
|
+
if (!Number.isSafeInteger(cursor) || cursor < 0 || cursor > latest) {
|
|
6301
|
+
throw new RangeError(`StructuralEvidenceRing cursor ${cursor} is outside 0..${latest}`);
|
|
6302
|
+
}
|
|
6303
|
+
const oldestAvailable = Math.max(1, latest - this.capacity + 1);
|
|
6304
|
+
if (cursor < oldestAvailable - 1)
|
|
6305
|
+
return { status: "overflow", cursor: latest, oldestAvailable };
|
|
6306
|
+
const events = [];
|
|
6307
|
+
for (let sequence = cursor + 1; sequence <= latest; sequence += 1) {
|
|
6308
|
+
const event = this.events[(sequence - 1) % this.capacity];
|
|
6309
|
+
if (event === void 0 || event.sequence !== sequence) {
|
|
6310
|
+
return { status: "overflow", cursor: latest, oldestAvailable };
|
|
6311
|
+
}
|
|
6312
|
+
events.push(event);
|
|
6313
|
+
}
|
|
6314
|
+
return { status: "ok", cursor: latest, events };
|
|
6315
|
+
}
|
|
6316
|
+
};
|
|
5648
6317
|
var UniqueRefStore = class {
|
|
5649
6318
|
payloads = /* @__PURE__ */ new Map();
|
|
5650
6319
|
freeSlots = [];
|
|
@@ -5801,7 +6470,7 @@ var WorldCore = class {
|
|
|
5801
6470
|
sharedRefs = new SharedRefStore();
|
|
5802
6471
|
storage;
|
|
5803
6472
|
componentMutationEpochs = [];
|
|
5804
|
-
|
|
6473
|
+
structuralEvidence = new StructuralEvidenceRing();
|
|
5805
6474
|
/** One packed reverse index per relationship source component. */
|
|
5806
6475
|
relationshipIndexes = /* @__PURE__ */ new Map();
|
|
5807
6476
|
mutationEpoch = 0;
|
|
@@ -5830,6 +6499,7 @@ function tableRow(world, record) {
|
|
|
5830
6499
|
return archetype?.rows[record.archetypeRow] ?? -1;
|
|
5831
6500
|
}
|
|
5832
6501
|
function spawnCore(world, componentDatas, internal) {
|
|
6502
|
+
componentDatas = expandComponentRequirements(componentDatas);
|
|
5833
6503
|
const filledData = [];
|
|
5834
6504
|
for (const cd of componentDatas) {
|
|
5835
6505
|
const preflight = world[worldInternal].preflightComponentData(null, cd);
|
|
@@ -5845,6 +6515,27 @@ function spawnCore(world, componentDatas, internal) {
|
|
|
5845
6515
|
if (enumErr !== null) return err(enumErr);
|
|
5846
6516
|
filledData.push(filled);
|
|
5847
6517
|
}
|
|
6518
|
+
const relationshipPreparations = [];
|
|
6519
|
+
if (!internal) {
|
|
6520
|
+
for (let index = 0; index < componentDatas.length; index += 1) {
|
|
6521
|
+
const componentData = componentDatas[index];
|
|
6522
|
+
const value = filledData[index];
|
|
6523
|
+
if (componentData === void 0 || value === void 0 || relationshipRole(componentData.component)?.kind !== "source") {
|
|
6524
|
+
continue;
|
|
6525
|
+
}
|
|
6526
|
+
const prepared = world[worldInternal].prepareRelationshipInsert(
|
|
6527
|
+
componentData.component,
|
|
6528
|
+
value
|
|
6529
|
+
);
|
|
6530
|
+
if (!prepared.ok) {
|
|
6531
|
+
for (const reservation of relationshipPreparations) {
|
|
6532
|
+
world[worldInternal].releaseRelationshipPreparation(reservation);
|
|
6533
|
+
}
|
|
6534
|
+
return prepared;
|
|
6535
|
+
}
|
|
6536
|
+
relationshipPreparations[index] = prepared.value;
|
|
6537
|
+
}
|
|
6538
|
+
}
|
|
5848
6539
|
const indexSlot = world[worldInternal].allocateIndex();
|
|
5849
6540
|
const record = world[worldInternal].getRecords()[indexSlot];
|
|
5850
6541
|
if (record === void 0)
|
|
@@ -5880,12 +6571,22 @@ function spawnCore(world, componentDatas, internal) {
|
|
|
5880
6571
|
const relation = world[worldInternal].relationshipOnInsert(
|
|
5881
6572
|
spawnedEntity,
|
|
5882
6573
|
cd.component,
|
|
5883
|
-
filled
|
|
6574
|
+
filled,
|
|
6575
|
+
relationshipPreparations[i]
|
|
5884
6576
|
);
|
|
5885
|
-
if (!relation.ok)
|
|
6577
|
+
if (!relation.ok) {
|
|
6578
|
+
for (const reservation of relationshipPreparations) {
|
|
6579
|
+
world[worldInternal].releaseRelationshipPreparation(reservation);
|
|
6580
|
+
}
|
|
6581
|
+
return relation;
|
|
6582
|
+
}
|
|
5886
6583
|
}
|
|
5887
6584
|
}
|
|
5888
6585
|
world[worldInternal].markStructureChanged();
|
|
6586
|
+
world[worldInternal].recordStructuralEvidence({
|
|
6587
|
+
kind: "spawn",
|
|
6588
|
+
entity: spawnedEntity
|
|
6589
|
+
});
|
|
5889
6590
|
return ok(spawnedEntity);
|
|
5890
6591
|
}
|
|
5891
6592
|
function despawnCore(world, entity, internal) {
|
|
@@ -5936,8 +6637,11 @@ function despawnCore(world, entity, internal) {
|
|
|
5936
6637
|
}
|
|
5937
6638
|
}
|
|
5938
6639
|
}
|
|
6640
|
+
world[worldInternal].recordStructuralEvidence({
|
|
6641
|
+
kind: "despawn",
|
|
6642
|
+
entity
|
|
6643
|
+
});
|
|
5939
6644
|
if (record) {
|
|
5940
|
-
world[worldInternal].removeEntityChanges(entity);
|
|
5941
6645
|
record.archetypeId = -1;
|
|
5942
6646
|
record.archetypeRow = -1;
|
|
5943
6647
|
record.generation += 1;
|
|
@@ -5949,7 +6653,8 @@ function despawnCore(world, entity, internal) {
|
|
|
5949
6653
|
}
|
|
5950
6654
|
function worldAddChild(world, parent, child, component, data) {
|
|
5951
6655
|
const holderComp = component;
|
|
5952
|
-
|
|
6656
|
+
const role = relationshipRole(holderComp);
|
|
6657
|
+
if (role?.kind !== "source") {
|
|
5953
6658
|
return err(new ComponentNotPresentError(child, component.name));
|
|
5954
6659
|
}
|
|
5955
6660
|
const parentSlot = entityIndex(parent);
|
|
@@ -5978,7 +6683,6 @@ function worldAddChild(world, parent, child, component, data) {
|
|
|
5978
6683
|
})
|
|
5979
6684
|
);
|
|
5980
6685
|
}
|
|
5981
|
-
const role = relationshipRole(holderComp);
|
|
5982
6686
|
if (child === parent && !(role?.kind === "source" && role.allowSelf)) {
|
|
5983
6687
|
return err(new RelationshipSelfCycleError(component.name, child, child));
|
|
5984
6688
|
}
|
|
@@ -6029,10 +6733,10 @@ function worldRemoveChild(world, parent, child, component) {
|
|
|
6029
6733
|
}
|
|
6030
6734
|
function worldReparent(world, child, newParent, component, data) {
|
|
6031
6735
|
const holderComp = component;
|
|
6032
|
-
|
|
6736
|
+
const role = relationshipRole(holderComp);
|
|
6737
|
+
if (role?.kind !== "source") {
|
|
6033
6738
|
return err(new ComponentNotPresentError(child, component.name));
|
|
6034
6739
|
}
|
|
6035
|
-
const role = relationshipRole(holderComp);
|
|
6036
6740
|
if (child === newParent && !(role?.kind === "source" && role.allowSelf)) {
|
|
6037
6741
|
return err(
|
|
6038
6742
|
new RelationshipSelfCycleError(component.name, child, newParent)
|
|
@@ -6062,11 +6766,14 @@ function worldReparent(world, child, newParent, component, data) {
|
|
|
6062
6766
|
})
|
|
6063
6767
|
);
|
|
6064
6768
|
}
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6769
|
+
const payload = {
|
|
6770
|
+
...data,
|
|
6771
|
+
[role.sourceField]: newParent
|
|
6772
|
+
};
|
|
6773
|
+
if (childArch.components.some((candidate) => componentId(candidate) === componentId(holderComp))) {
|
|
6774
|
+
return world.set(child, component, payload);
|
|
6068
6775
|
}
|
|
6069
|
-
return world.addComponent(child, { component, data });
|
|
6776
|
+
return world.addComponent(child, { component, data: payload });
|
|
6070
6777
|
}
|
|
6071
6778
|
function worldIterAncestors(world, entity) {
|
|
6072
6779
|
return {
|
|
@@ -6586,9 +7293,6 @@ var World = class {
|
|
|
6586
7293
|
get componentMutationEpochs() {
|
|
6587
7294
|
return this.core.componentMutationEpochs;
|
|
6588
7295
|
}
|
|
6589
|
-
get changeJournal() {
|
|
6590
|
-
return this.core.changeJournal;
|
|
6591
|
-
}
|
|
6592
7296
|
/** Free index slots (LIFO stack). */
|
|
6593
7297
|
get records() {
|
|
6594
7298
|
return this.core.records;
|
|
@@ -6668,8 +7372,8 @@ var World = class {
|
|
|
6668
7372
|
markComponentAdded: (entity, component) => this.internalmarkComponentAdded(entity, component),
|
|
6669
7373
|
markComponentsAdded: (entity, components) => this.internalmarkComponentsAdded(entity, components),
|
|
6670
7374
|
markComponentChanged: (entity, component) => this.internalmarkComponentChanged(entity, component),
|
|
6671
|
-
removeComponentChange: (entity, component) => this.internalremoveComponentChange(entity, component),
|
|
6672
7375
|
markStructureChanged: () => this.internalmarkStructureChanged(),
|
|
7376
|
+
recordStructuralEvidence: (evidence) => this.internalrecordStructuralEvidence(evidence),
|
|
6673
7377
|
routeError: (error, context) => this.internalrouteError(error, context)
|
|
6674
7378
|
});
|
|
6675
7379
|
this.clock = createWorldClock({ ...DEFAULT_TIME_POLICY, ...options.time });
|
|
@@ -6690,11 +7394,14 @@ var World = class {
|
|
|
6690
7394
|
cancelPendingEntity: this.internalcancelPendingEntity.bind(this),
|
|
6691
7395
|
despawnCore: this.internaldespawnCore.bind(this),
|
|
6692
7396
|
getArrayView: this.internalgetArrayView.bind(this),
|
|
7397
|
+
getArrayLength: this.internalgetArrayLength.bind(this),
|
|
7398
|
+
getArrayElement: this.internalgetArrayElement.bind(this),
|
|
7399
|
+
getFieldValue: this.internalgetFieldValue.bind(this),
|
|
6693
7400
|
getBufferPool: this.internalgetBufferPool.bind(this),
|
|
6694
|
-
getChangeCursor: this.internalgetChangeCursor.bind(this),
|
|
6695
7401
|
getClockWriter: this.internalgetClockWriter.bind(this),
|
|
6696
7402
|
getComponentChange: this.internalgetComponentChange.bind(this),
|
|
6697
7403
|
getComponentMutationEpoch: this.internalgetComponentMutationEpoch.bind(this),
|
|
7404
|
+
getComponentMutationEpochs: this.internalgetComponentMutationEpochs.bind(this),
|
|
6698
7405
|
getEntityArchetype: this.internalgetEntityArchetype.bind(this),
|
|
6699
7406
|
getFixedAccumulator: this.internalgetFixedAccumulator.bind(this),
|
|
6700
7407
|
getFreeIndices: this.internalgetFreeIndices.bind(this),
|
|
@@ -6709,28 +7416,30 @@ var World = class {
|
|
|
6709
7416
|
getSchedules: this.internalgetSchedules.bind(this),
|
|
6710
7417
|
getSharedRefs: this.internalgetSharedRefs.bind(this),
|
|
6711
7418
|
getStructureEpoch: this.internalgetStructureEpoch.bind(this),
|
|
7419
|
+
getStructuralEvidence: this.internalgetStructuralEvidence.bind(this),
|
|
6712
7420
|
getUniqueRefs: this.internalgetUniqueRefs.bind(this),
|
|
6713
7421
|
lookupAlive: this.internallookupAlive.bind(this),
|
|
6714
7422
|
markComponentAdded: this.internalmarkComponentAdded.bind(this),
|
|
6715
7423
|
markComponentChanged: this.internalmarkComponentChanged.bind(this),
|
|
6716
7424
|
markComponentRangeChanged: this.internalmarkComponentRangeChanged.bind(this),
|
|
6717
7425
|
markComponentsAdded: this.internalmarkComponentsAdded.bind(this),
|
|
6718
|
-
markDerivedComponentChanges: this.internalmarkDerivedComponentChanges.bind(this),
|
|
6719
7426
|
markStructureChanged: this.internalmarkStructureChanged.bind(this),
|
|
6720
7427
|
materializePendingEntity: this.internalmaterializePendingEntity.bind(this),
|
|
6721
7428
|
nextMutationEpoch: this.internalnextMutationEpoch.bind(this),
|
|
6722
7429
|
poisonExecution: this.internalpoisonExecution.bind(this),
|
|
7430
|
+
prepareRelationshipInsert: this.internalprepareRelationshipInsert.bind(this),
|
|
7431
|
+
publishDerivedRange: this.internalpublishDerivedRange.bind(this),
|
|
6723
7432
|
preflightComponentData: this.internalpreflightComponentData.bind(this),
|
|
6724
|
-
readChangesSince: this.internalreadChangesSince.bind(this),
|
|
6725
7433
|
readRow: this.internalreadRow.bind(this),
|
|
7434
|
+
recordStructuralEvidence: this.internalrecordStructuralEvidence.bind(this),
|
|
6726
7435
|
recordIsLive: this.internalrecordIsLive.bind(this),
|
|
6727
7436
|
relationshipOnInsert: this.internalrelationshipOnInsert.bind(this),
|
|
6728
7437
|
relationshipOnRemove: this.internalrelationshipOnRemove.bind(this),
|
|
6729
7438
|
releaseManagedRefsOnRow: this.internalreleaseManagedRefsOnRow.bind(this),
|
|
6730
|
-
|
|
7439
|
+
releaseRelationshipPreparation: this.internalreleaseRelationshipPreparation.bind(this),
|
|
6731
7440
|
removeComponentCore: this.internalremoveComponentCore.bind(this),
|
|
6732
|
-
removeEntityChanges: this.internalremoveEntityChanges.bind(this),
|
|
6733
7441
|
routeError: this.internalrouteError.bind(this),
|
|
7442
|
+
restoreMutationEpoch: this.internalrestoreMutationEpoch.bind(this),
|
|
6734
7443
|
setFixedAccumulator: this.internalsetFixedAccumulator.bind(this),
|
|
6735
7444
|
setQueryRow: this.internalsetQueryRow.bind(this),
|
|
6736
7445
|
spawnCore: this.internalspawnCore.bind(this),
|
|
@@ -6776,18 +7485,17 @@ var World = class {
|
|
|
6776
7485
|
internalgetMutationEpoch() {
|
|
6777
7486
|
return this.mutationEpoch;
|
|
6778
7487
|
}
|
|
6779
|
-
/** Current cursor for a persistent projection subscriber. */
|
|
6780
|
-
internalgetChangeCursor() {
|
|
6781
|
-
return this.changeJournal.cursor();
|
|
6782
|
-
}
|
|
6783
|
-
/** Read complete mutation evidence or an explicit rebuild signal. */
|
|
6784
|
-
internalreadChangesSince(cursor) {
|
|
6785
|
-
return this.changeJournal.readAfter(cursor);
|
|
6786
|
-
}
|
|
6787
7488
|
/** Structure snapshot used to invalidate borrowed query facades. */
|
|
6788
7489
|
internalgetStructureEpoch() {
|
|
6789
7490
|
return this.structureEpoch;
|
|
6790
7491
|
}
|
|
7492
|
+
/** Typed structural facts are produced by this World and consumed by projections. */
|
|
7493
|
+
internalgetStructuralEvidence() {
|
|
7494
|
+
return this.core.structuralEvidence;
|
|
7495
|
+
}
|
|
7496
|
+
internalrecordStructuralEvidence(evidence) {
|
|
7497
|
+
this.core.structuralEvidence.append(evidence);
|
|
7498
|
+
}
|
|
6791
7499
|
/** Resolve current logical identity for a packed entity handle. */
|
|
6792
7500
|
internalgetEntityArchetype(entity) {
|
|
6793
7501
|
const record = this.records[entityIndex(entity)];
|
|
@@ -6808,6 +7516,17 @@ var World = class {
|
|
|
6808
7516
|
this.mutationEpoch += 1;
|
|
6809
7517
|
return this.mutationEpoch;
|
|
6810
7518
|
}
|
|
7519
|
+
internalrestoreMutationEpoch(epoch) {
|
|
7520
|
+
this.mutationEpoch = epoch;
|
|
7521
|
+
}
|
|
7522
|
+
internalpublishDerivedRange(table, componentId2, rowStart, rowCount, epoch) {
|
|
7523
|
+
const epochs = table.storage.get(componentId2)?.epochs;
|
|
7524
|
+
if (epochs === void 0) {
|
|
7525
|
+
throw new Error(`Derived component ${componentId2} is not in the table.`);
|
|
7526
|
+
}
|
|
7527
|
+
epochs.changed.fill(epoch, rowStart, rowStart + rowCount);
|
|
7528
|
+
this.componentMutationEpochs[componentId2] = epoch;
|
|
7529
|
+
}
|
|
6811
7530
|
/** Record one successful structural mutation. */
|
|
6812
7531
|
internalmarkStructureChanged() {
|
|
6813
7532
|
this.structureEpoch += 1;
|
|
@@ -6828,7 +7547,6 @@ var World = class {
|
|
|
6828
7547
|
markComponentsAdded(this.graph, record, entity, componentIds, epoch);
|
|
6829
7548
|
for (const componentId2 of componentIds) {
|
|
6830
7549
|
this.componentMutationEpochs[componentId2] = epoch;
|
|
6831
|
-
this.changeJournal.append({ kind: "component-added", entity, componentId: componentId2 });
|
|
6832
7550
|
}
|
|
6833
7551
|
}
|
|
6834
7552
|
/** Mark an existing component as changed at the current tick. */
|
|
@@ -6842,7 +7560,6 @@ var World = class {
|
|
|
6842
7560
|
});
|
|
6843
7561
|
if (epoch !== void 0) {
|
|
6844
7562
|
this.componentMutationEpochs[componentId2] = epoch;
|
|
6845
|
-
this.changeJournal.append({ kind: "component-changed", entity, componentId: componentId2 });
|
|
6846
7563
|
}
|
|
6847
7564
|
}
|
|
6848
7565
|
/** Mark one contiguous component range with a single epoch. */
|
|
@@ -6852,18 +7569,15 @@ var World = class {
|
|
|
6852
7569
|
const epoch = this.internalnextMutationEpoch();
|
|
6853
7570
|
epochs.changed.fill(epoch, rowStart, rowStart + rowCount);
|
|
6854
7571
|
this.componentMutationEpochs[componentId2] = epoch;
|
|
6855
|
-
for (const entity of readTableEntityRange(table, rowStart, rowCount)) {
|
|
6856
|
-
this.changeJournal.append({
|
|
6857
|
-
kind: "component-changed",
|
|
6858
|
-
entity,
|
|
6859
|
-
componentId: componentId2
|
|
6860
|
-
});
|
|
6861
|
-
}
|
|
6862
7572
|
}
|
|
6863
7573
|
/** Latest mutation token for one component-owned projection. */
|
|
6864
7574
|
internalgetComponentMutationEpoch(componentId2) {
|
|
6865
7575
|
return this.componentMutationEpochs[componentId2] ?? 0;
|
|
6866
7576
|
}
|
|
7577
|
+
/** Borrow the component-version summary used to skip unrelated value columns. */
|
|
7578
|
+
internalgetComponentMutationEpochs() {
|
|
7579
|
+
return this.componentMutationEpochs;
|
|
7580
|
+
}
|
|
6867
7581
|
/** Read a materialized relationship target in O(1 + k). */
|
|
6868
7582
|
internalgetRelationshipTargetEntities(source, target) {
|
|
6869
7583
|
return this.componentAccess.relationshipTargetEntries(source, target);
|
|
@@ -6872,28 +7586,19 @@ var World = class {
|
|
|
6872
7586
|
internalgetRelationshipEpoch(source) {
|
|
6873
7587
|
return this.core.relationshipIndexes.get(componentId(source))?.epoch ?? 0;
|
|
6874
7588
|
}
|
|
6875
|
-
/** Publish changes to a value derived without an authored mutation epoch. */
|
|
6876
|
-
internalmarkDerivedComponentChanges(componentId2, entities) {
|
|
6877
|
-
for (const entity of entities) {
|
|
6878
|
-
this.changeJournal.append({ kind: "derived-component-changed", entity, componentId: componentId2 });
|
|
6879
|
-
}
|
|
6880
|
-
}
|
|
6881
7589
|
/** Query facade write after the facade has already marked evidence. */
|
|
6882
7590
|
internalsetQueryRow(entity, component, value) {
|
|
6883
|
-
return this.componentAccess.set(
|
|
7591
|
+
return this.componentAccess.set(
|
|
7592
|
+
entity,
|
|
7593
|
+
component,
|
|
7594
|
+
value,
|
|
7595
|
+
relationshipRole(component)?.kind === "source"
|
|
7596
|
+
);
|
|
6884
7597
|
}
|
|
6885
7598
|
/** Query facade read that does not re-enter the public World API. */
|
|
6886
7599
|
internalgetQueryRow(entity, component) {
|
|
6887
7600
|
return this.componentAccess.get(entity, component);
|
|
6888
7601
|
}
|
|
6889
|
-
/** Remove one component's change state after archetype removal. */
|
|
6890
|
-
internalremoveComponentChange(entity, componentId2) {
|
|
6891
|
-
this.changeJournal.append({ kind: "component-removed", entity, componentId: componentId2 });
|
|
6892
|
-
}
|
|
6893
|
-
/** Remove all change state before an entity handle is retired. */
|
|
6894
|
-
internalremoveEntityChanges(entity) {
|
|
6895
|
-
this.changeJournal.append({ kind: "entity-removed", entity });
|
|
6896
|
-
}
|
|
6897
7602
|
/** Return resource change ticks for diagnostics and resource-driven systems. */
|
|
6898
7603
|
getResourceChange(name) {
|
|
6899
7604
|
const entry = this.resources.entries.get(name);
|
|
@@ -6910,8 +7615,8 @@ var World = class {
|
|
|
6910
7615
|
*
|
|
6911
7616
|
* Not part of the public API.
|
|
6912
7617
|
*/
|
|
6913
|
-
internalrouteError(
|
|
6914
|
-
console.error(`[${ctx?.systemName ?? "World"}]`,
|
|
7618
|
+
internalrouteError(err12, ctx) {
|
|
7619
|
+
console.error(`[${ctx?.systemName ?? "World"}]`, err12);
|
|
6915
7620
|
}
|
|
6916
7621
|
// ──────────────────────────────────────────────────────────────────────────
|
|
6917
7622
|
// accessors — M1 extraction seam
|
|
@@ -7236,6 +7941,15 @@ var World = class {
|
|
|
7236
7941
|
internalgetArrayView(entity, component, fieldName) {
|
|
7237
7942
|
return this.componentAccess._getArrayView(entity, component, fieldName);
|
|
7238
7943
|
}
|
|
7944
|
+
internalgetArrayLength(entity, component, fieldName) {
|
|
7945
|
+
return this.componentAccess._getArrayLength(entity, component, fieldName);
|
|
7946
|
+
}
|
|
7947
|
+
internalgetArrayElement(entity, component, fieldName, index) {
|
|
7948
|
+
return this.componentAccess._getArrayElement(entity, component, fieldName, index);
|
|
7949
|
+
}
|
|
7950
|
+
internalgetFieldValue(entity, component, fieldName) {
|
|
7951
|
+
return this.componentAccess._getFieldValue(entity, component, fieldName);
|
|
7952
|
+
}
|
|
7239
7953
|
set(entity, component, value) {
|
|
7240
7954
|
if (isRelationshipTarget(component)) return this.relationshipTargetWriteError(component, "set");
|
|
7241
7955
|
return this.componentAccess.set(entity, component, value);
|
|
@@ -7304,8 +8018,16 @@ var World = class {
|
|
|
7304
8018
|
this.componentAccess.releaseManagedRefsOnRow(a, c, r);
|
|
7305
8019
|
}
|
|
7306
8020
|
/** */
|
|
7307
|
-
internalrelationshipOnInsert(h, c, v) {
|
|
7308
|
-
return this.componentAccess.relationshipOnInsert(h, c, v);
|
|
8021
|
+
internalrelationshipOnInsert(h, c, v, preparation) {
|
|
8022
|
+
return this.componentAccess.relationshipOnInsert(h, c, v, preparation);
|
|
8023
|
+
}
|
|
8024
|
+
/** */
|
|
8025
|
+
internalprepareRelationshipInsert(c, v) {
|
|
8026
|
+
return this.componentAccess.prepareRelationshipInsert(c, v);
|
|
8027
|
+
}
|
|
8028
|
+
/** */
|
|
8029
|
+
internalreleaseRelationshipPreparation(preparation) {
|
|
8030
|
+
this.componentAccess.releaseRelationshipPreparation(preparation);
|
|
7309
8031
|
}
|
|
7310
8032
|
/** */
|
|
7311
8033
|
internalrelationshipOnRemove(h, c, v) {
|