@forgeax/engine-ecs 0.1.23 → 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 +33 -1
- package/dist/component.d.ts +9 -6
- 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/externalization/index.mjs.map +1 -1
- package/dist/index.mjs +566 -131
- package/dist/index.mjs.map +1 -1
- package/dist/internal.d.ts +3 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.mjs +6 -1
- package/dist/internal.mjs.map +1 -1
- package/dist/projection/index.mjs.map +1 -1
- package/dist/query/derived-range-writer.d.ts +14 -0
- package/dist/query/derived-range-writer.d.ts.map +1 -1
- package/dist/query/query.d.ts.map +1 -1
- package/dist/shared.mjs.map +1 -1
- package/dist/world-component-access.d.ts +63 -18
- 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-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 +5 -0
- package/dist/world.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/relationship-index.test.ts +147 -1
- package/src/component.ts +9 -6
- package/src/errors/query-and-component-errors.ts +4 -1
- package/src/internal.ts +3 -0
- package/src/query/derived-range-writer.ts +111 -0
- package/src/query/query.ts +26 -1
- package/src/world-component-access.ts +428 -127
- package/src/world-component-storage.ts +104 -18
- package/src/world-entity-lifecycle.ts +49 -9
- package/src/world-internal.ts +5 -0
- package/src/world.ts +52 -3
package/dist/index.mjs
CHANGED
|
@@ -2046,6 +2046,7 @@ function createDerivedRangeWriter(world, component, source) {
|
|
|
2046
2046
|
let boundEpoch = -1;
|
|
2047
2047
|
let bindingTables = [];
|
|
2048
2048
|
let bindings = [];
|
|
2049
|
+
let bindingByTable = /* @__PURE__ */ new Map();
|
|
2049
2050
|
let runStartBuffers = [];
|
|
2050
2051
|
let runCountBuffers = [];
|
|
2051
2052
|
const rebind = () => {
|
|
@@ -2055,6 +2056,8 @@ function createDerivedRangeWriter(world, component, source) {
|
|
|
2055
2056
|
const nextRunCountBuffers = [];
|
|
2056
2057
|
for (const table of tables) {
|
|
2057
2058
|
nextBindings.push({
|
|
2059
|
+
tableId: table.id,
|
|
2060
|
+
entities: table.storage.get(componentId(Entity))?.fields.get("self")?.view ?? new Uint32Array(0),
|
|
2058
2061
|
read: buildWholeColumnShape(table, source.readComponents),
|
|
2059
2062
|
write: buildWholeColumnShape(table, source.writeComponents),
|
|
2060
2063
|
rowCapacity: table.size
|
|
@@ -2065,6 +2068,11 @@ function createDerivedRangeWriter(world, component, source) {
|
|
|
2065
2068
|
}
|
|
2066
2069
|
bindingTables = tables;
|
|
2067
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
|
+
}
|
|
2068
2076
|
runStartBuffers = nextRunStartBuffers;
|
|
2069
2077
|
runCountBuffers = nextRunCountBuffers;
|
|
2070
2078
|
boundEpoch = source.structureEpoch();
|
|
@@ -2075,6 +2083,82 @@ function createDerivedRangeWriter(world, component, source) {
|
|
|
2075
2083
|
if (boundEpoch !== source.structureEpoch()) rebind();
|
|
2076
2084
|
return bindings;
|
|
2077
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
|
+
},
|
|
2078
2162
|
writeRange(bindingIndex, base, start, count, kernel, context) {
|
|
2079
2163
|
if (world.execution.health === "poisoned") {
|
|
2080
2164
|
return err(new WorldPoisonedError(world.identity, world.execution.fault));
|
|
@@ -2288,7 +2372,13 @@ var QueryRowFacade = class _QueryRowFacade {
|
|
|
2288
2372
|
mut(component) {
|
|
2289
2373
|
const current = this.get(component);
|
|
2290
2374
|
if (current === void 0) throw new Error(`Query row lacks ${component.name}.`);
|
|
2291
|
-
|
|
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
|
+
}
|
|
2292
2382
|
return new Proxy(current, {
|
|
2293
2383
|
set: (target, property, value) => {
|
|
2294
2384
|
if (typeof property !== "string") return false;
|
|
@@ -2325,6 +2415,9 @@ var QuerySpanFacade = class {
|
|
|
2325
2415
|
);
|
|
2326
2416
|
}
|
|
2327
2417
|
mut(component) {
|
|
2418
|
+
if (relationshipRole(component) !== void 0) {
|
|
2419
|
+
throw new RelationshipTargetReadonlyError(component.name, "query span");
|
|
2420
|
+
}
|
|
2328
2421
|
this.world[worldInternal].markComponentRangeChanged(
|
|
2329
2422
|
this.table,
|
|
2330
2423
|
componentId(component),
|
|
@@ -2650,6 +2743,9 @@ var ExecutableQuery = class {
|
|
|
2650
2743
|
spanUnavailableReason() {
|
|
2651
2744
|
if (this.compiled.sparseRoute) return "sparse-component";
|
|
2652
2745
|
if ((this.compiled.descriptor.optional?.length ?? 0) > 0) return "optional-data";
|
|
2746
|
+
if ((this.compiled.descriptor.write ?? []).some((component) => relationshipRole(component))) {
|
|
2747
|
+
return "relationship-component";
|
|
2748
|
+
}
|
|
2653
2749
|
return void 0;
|
|
2654
2750
|
}
|
|
2655
2751
|
};
|
|
@@ -3637,6 +3733,52 @@ var ComponentStorage = class {
|
|
|
3637
3733
|
const elementCount = countCol?.view[row] ?? 0;
|
|
3638
3734
|
return reinterpretSlotBytes(liveBytes, arrayMeta.elementType, elementCount);
|
|
3639
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
|
+
}
|
|
3640
3782
|
// ──────────────────────────────────────────────────────────────────────────
|
|
3641
3783
|
// Internal — archetype data read/write
|
|
3642
3784
|
// ──────────────────────────────────────────────────────────────────────────
|
|
@@ -3701,7 +3843,7 @@ var ComponentStorage = class {
|
|
|
3701
3843
|
if (!col) return;
|
|
3702
3844
|
col.view[row] = handle;
|
|
3703
3845
|
}
|
|
3704
|
-
writeRow(arch, component, row, value) {
|
|
3846
|
+
writeRow(arch, component, row, value, options) {
|
|
3705
3847
|
const localId = componentId(component);
|
|
3706
3848
|
const fieldCols = this.table(arch).storage.get(localId)?.fields;
|
|
3707
3849
|
if (!fieldCols) {
|
|
@@ -3750,6 +3892,12 @@ var ComponentStorage = class {
|
|
|
3750
3892
|
} else {
|
|
3751
3893
|
const arrayMeta = componentDefinition(component).fields[fieldName]?.arrayMeta;
|
|
3752
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
|
+
}
|
|
3753
3901
|
this.writeArrayField(arch, component, row, fieldName, fieldType, arrayMeta, raw);
|
|
3754
3902
|
} else {
|
|
3755
3903
|
col.view[row] = raw;
|
|
@@ -4125,20 +4273,19 @@ var ComponentStorage = class {
|
|
|
4125
4273
|
return readArrayElementAt(bytes, idx, elementType);
|
|
4126
4274
|
}
|
|
4127
4275
|
/**
|
|
4128
|
-
* Materialise a fresh `TypedArray`
|
|
4129
|
-
*
|
|
4130
|
-
*
|
|
4131
|
-
*
|
|
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.
|
|
4132
4281
|
*
|
|
4133
4282
|
* **Transient view contract (feat-20260602):** for fixed `array<T,N>`
|
|
4134
4283
|
* columns the returned `TypedArray` aliases the inline column buffer
|
|
4135
4284
|
* directly (`col.view.subarray(row * arity, ...)`); for variable
|
|
4136
4285
|
* `array<T>` columns it aliases the live `BufferPool` slot bytes
|
|
4137
|
-
* (zero-copy; `pool.view(slotId)` is the SSOT byte region). In both
|
|
4138
|
-
*
|
|
4139
|
-
*
|
|
4140
|
-
* and the implementation may switch to a copy in the future without
|
|
4141
|
-
* 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.
|
|
4142
4289
|
*
|
|
4143
4290
|
* For variable arrays the typed-array length matches the live count from
|
|
4144
4291
|
* the sidecar `<fieldName>:count` column; for fixed arrays it matches the
|
|
@@ -4397,6 +4544,9 @@ function relationshipPayloadWrites(data) {
|
|
|
4397
4544
|
return true;
|
|
4398
4545
|
});
|
|
4399
4546
|
}
|
|
4547
|
+
function detachRelationshipTargetArray(value) {
|
|
4548
|
+
return value instanceof Uint32Array ? value.slice() : value;
|
|
4549
|
+
}
|
|
4400
4550
|
var WorldComponentAccess = class {
|
|
4401
4551
|
constructor(state) {
|
|
4402
4552
|
this.state = state;
|
|
@@ -4437,16 +4587,19 @@ var WorldComponentAccess = class {
|
|
|
4437
4587
|
}
|
|
4438
4588
|
return index;
|
|
4439
4589
|
}
|
|
4440
|
-
/**
|
|
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
|
+
*/
|
|
4441
4596
|
relationshipTargetEntries(source, target) {
|
|
4442
4597
|
const role = relationshipRole(source);
|
|
4443
4598
|
if (role?.kind !== "source") return [];
|
|
4444
4599
|
const mirror = relationshipMirror(source);
|
|
4445
4600
|
if (mirror === void 0) return [];
|
|
4446
|
-
const
|
|
4447
|
-
|
|
4448
|
-
const entries = result.value[role.targetField];
|
|
4449
|
-
return entries !== void 0 && typeof entries === "object" ? entries : [];
|
|
4601
|
+
const entries = this._getArrayView(target, mirror, role.targetField);
|
|
4602
|
+
return entries === void 0 ? [] : entries;
|
|
4450
4603
|
}
|
|
4451
4604
|
markComponentAdded(entity, component) {
|
|
4452
4605
|
this.state.markComponentAdded(entity, componentId(component));
|
|
@@ -4526,8 +4679,7 @@ var WorldComponentAccess = class {
|
|
|
4526
4679
|
const targetRecord = this.records[entityIndex(target)];
|
|
4527
4680
|
const actualGeneration = targetRecord?.generation ?? -1;
|
|
4528
4681
|
const targetLive = this.recordIsLive(targetRecord, entityGeneration(target));
|
|
4529
|
-
|
|
4530
|
-
if (!targetIsPending && !targetLive && !holderIsPending) {
|
|
4682
|
+
if (!targetIsPending && !targetLive) {
|
|
4531
4683
|
return err(
|
|
4532
4684
|
new StaleEntityError(target, entityIndex(target), entityGeneration(target), {
|
|
4533
4685
|
operation: "relationship-insert",
|
|
@@ -4584,7 +4736,14 @@ var WorldComponentAccess = class {
|
|
|
4584
4736
|
current = next;
|
|
4585
4737
|
}
|
|
4586
4738
|
}
|
|
4587
|
-
/**
|
|
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
|
+
*/
|
|
4588
4747
|
prepareRelationshipInsert(component, value) {
|
|
4589
4748
|
const role = relationshipRole(component);
|
|
4590
4749
|
if (role?.kind !== "source") return ok(void 0);
|
|
@@ -4605,34 +4764,114 @@ var WorldComponentAccess = class {
|
|
|
4605
4764
|
);
|
|
4606
4765
|
}
|
|
4607
4766
|
const targetArch = this.graph.archetypes[targetRec.archetypeId];
|
|
4608
|
-
const
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
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)
|
|
4614
4838
|
);
|
|
4615
|
-
if (!added.ok) return added;
|
|
4616
4839
|
}
|
|
4617
|
-
const
|
|
4618
|
-
|
|
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);
|
|
4619
4846
|
}
|
|
4620
4847
|
/** Append `holder` to the materialized target list. */
|
|
4621
|
-
relationshipOnInsert(holder, component, value) {
|
|
4848
|
+
relationshipOnInsert(holder, component, value, preparation) {
|
|
4622
4849
|
const role = relationshipRole(component);
|
|
4623
4850
|
if (role?.kind !== "source") return ok(void 0);
|
|
4624
4851
|
const target = this.relationshipTargetEntity(component, value);
|
|
4625
4852
|
if (target === null) return ok(void 0);
|
|
4626
4853
|
const mirror = relationshipMirror(component);
|
|
4627
4854
|
if (mirror === void 0) return ok(void 0);
|
|
4628
|
-
|
|
4629
|
-
if (
|
|
4630
|
-
|
|
4631
|
-
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;
|
|
4632
4860
|
}
|
|
4861
|
+
if (prepared === void 0) return ok(void 0);
|
|
4633
4862
|
const targetSlot = entityIndex(target);
|
|
4634
4863
|
const targetRec = this.records[targetSlot];
|
|
4635
|
-
|
|
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
|
+
}
|
|
4636
4875
|
const targetArch = this.graph.archetypes[targetRec.archetypeId];
|
|
4637
4876
|
const mirrorLocalId = componentId(mirror);
|
|
4638
4877
|
const hasMirror = targetArch?.components.some((component2) => componentId(component2) === mirrorLocalId) ?? false;
|
|
@@ -4643,9 +4882,19 @@ var WorldComponentAccess = class {
|
|
|
4643
4882
|
component: mirror,
|
|
4644
4883
|
data: {}
|
|
4645
4884
|
},
|
|
4885
|
+
true,
|
|
4886
|
+
false,
|
|
4646
4887
|
true
|
|
4647
4888
|
);
|
|
4648
|
-
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;
|
|
4649
4898
|
}
|
|
4650
4899
|
const targetEntries = this.relationshipTargetEntries(component, target);
|
|
4651
4900
|
const slot = targetEntries.length;
|
|
@@ -4655,7 +4904,10 @@ var WorldComponentAccess = class {
|
|
|
4655
4904
|
role.targetField,
|
|
4656
4905
|
holder
|
|
4657
4906
|
);
|
|
4658
|
-
if (!mirrored.ok)
|
|
4907
|
+
if (!mirrored.ok) {
|
|
4908
|
+
this.releaseRelationshipPreparation(prepared);
|
|
4909
|
+
return mirrored;
|
|
4910
|
+
}
|
|
4659
4911
|
this.relationshipIndex(component)?.attach(holder, target, slot);
|
|
4660
4912
|
return ok(void 0);
|
|
4661
4913
|
}
|
|
@@ -4688,15 +4940,16 @@ var WorldComponentAccess = class {
|
|
|
4688
4940
|
/**
|
|
4689
4941
|
* Read component data from an entity.
|
|
4690
4942
|
*
|
|
4691
|
-
* **
|
|
4692
|
-
* `
|
|
4693
|
-
*
|
|
4694
|
-
*
|
|
4695
|
-
* `
|
|
4696
|
-
*
|
|
4697
|
-
*
|
|
4698
|
-
*
|
|
4699
|
-
* `
|
|
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.
|
|
4700
4953
|
*
|
|
4701
4954
|
* @returns `Result<ShapeOf<S>, EcsError>` —
|
|
4702
4955
|
* `ok(ShapeOf<S>)` on success;
|
|
@@ -4736,7 +4989,13 @@ var WorldComponentAccess = class {
|
|
|
4736
4989
|
if (!arch.components.some((candidate) => componentId(candidate) === localId)) {
|
|
4737
4990
|
return err(new ComponentNotPresentError(entity, component.name));
|
|
4738
4991
|
}
|
|
4739
|
-
|
|
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);
|
|
4740
4999
|
}
|
|
4741
5000
|
/**
|
|
4742
5001
|
* Column-level zero-copy view of an `array<T, N>` / `array<T>` field.
|
|
@@ -4770,9 +5029,10 @@ var WorldComponentAccess = class {
|
|
|
4770
5029
|
* Returns `undefined` when the entity is dead, the component is absent, the
|
|
4771
5030
|
* field does not exist, or the field is not an `array<...>` column.
|
|
4772
5031
|
*
|
|
4773
|
-
* @internal Engine-internal fast path; AI users read
|
|
4774
|
-
* `world.get
|
|
4775
|
-
*
|
|
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.
|
|
4776
5036
|
*/
|
|
4777
5037
|
_getArrayView(entity, component, fieldName) {
|
|
4778
5038
|
const record = this.lookupAlive(entity, "_getArrayView", component.name);
|
|
@@ -4782,6 +5042,109 @@ var WorldComponentAccess = class {
|
|
|
4782
5042
|
if (!arch) return void 0;
|
|
4783
5043
|
return this.storage.readArrayView(arch, component, this.tableRow(rec), fieldName);
|
|
4784
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
|
+
}
|
|
4785
5148
|
/**
|
|
4786
5149
|
* Write (partial) component data to an entity.
|
|
4787
5150
|
*
|
|
@@ -4820,6 +5183,20 @@ var WorldComponentAccess = class {
|
|
|
4820
5183
|
if (!arch.components.some((candidate) => componentId(candidate) === localId)) {
|
|
4821
5184
|
return err(new ComponentNotPresentError(entity, component.name));
|
|
4822
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
|
+
}
|
|
4823
5200
|
const valuePreflight = this.preflightComponentFieldValues(entity, {
|
|
4824
5201
|
component,
|
|
4825
5202
|
data: value
|
|
@@ -4986,51 +5363,6 @@ var WorldComponentAccess = class {
|
|
|
4986
5363
|
this.markComponentChanged(entity, component);
|
|
4987
5364
|
return ok(void 0);
|
|
4988
5365
|
}
|
|
4989
|
-
ensureArrayCapacity(entity, component, fieldName, minimum) {
|
|
4990
|
-
const record = this.lookupAlive(entity, "relationship-capacity", component.name);
|
|
4991
|
-
if (!record.ok) return record;
|
|
4992
|
-
const rec = record.value;
|
|
4993
|
-
const arch = this.graph.archetypes[rec.archetypeId];
|
|
4994
|
-
if (!arch) {
|
|
4995
|
-
return err(
|
|
4996
|
-
new StaleEntityError(entity, entityIndex(entity), entityGeneration(entity), {
|
|
4997
|
-
operation: "relationship-capacity",
|
|
4998
|
-
component: component.name,
|
|
4999
|
-
expectedGeneration: entityGeneration(entity),
|
|
5000
|
-
actualGeneration: rec.generation
|
|
5001
|
-
})
|
|
5002
|
-
);
|
|
5003
|
-
}
|
|
5004
|
-
const fieldCols = this.table(arch).storage.get(componentId(component))?.fields;
|
|
5005
|
-
if (!fieldCols) return err(new ComponentNotPresentError(entity, component.name));
|
|
5006
|
-
const fieldNameStr = fieldName;
|
|
5007
|
-
const col = fieldCols.get(fieldNameStr);
|
|
5008
|
-
if (!col) return err(new ComponentNotPresentError(entity, component.name));
|
|
5009
|
-
const arrayMeta = componentDefinition(component).fields[fieldNameStr]?.arrayMeta;
|
|
5010
|
-
if (arrayMeta === void 0) {
|
|
5011
|
-
return err(new ComponentNotPresentError(entity, component.name));
|
|
5012
|
-
}
|
|
5013
|
-
const meta = TYPE_METADATA[arrayMeta.elementType];
|
|
5014
|
-
if (!meta?.byteSize) {
|
|
5015
|
-
return err(new ComponentNotPresentError(entity, component.name));
|
|
5016
|
-
}
|
|
5017
|
-
const maximum = Math.floor(262144 / meta.byteSize);
|
|
5018
|
-
if (!Number.isSafeInteger(minimum) || minimum < 0 || minimum > maximum) {
|
|
5019
|
-
return err(new ManagedBufferOutOfBoundsError(minimum, maximum));
|
|
5020
|
-
}
|
|
5021
|
-
const byteLength = minimum * meta.byteSize;
|
|
5022
|
-
const slotId = col.view[this.tableRow(rec)];
|
|
5023
|
-
if (slotId === 0) {
|
|
5024
|
-
if (minimum === 0) return ok(void 0);
|
|
5025
|
-
const allocated = this.bufferPool.alloc(byteLength);
|
|
5026
|
-
if (!allocated.ok) return allocated;
|
|
5027
|
-
col.view[this.tableRow(rec)] = allocated.value.id;
|
|
5028
|
-
return ok(void 0);
|
|
5029
|
-
}
|
|
5030
|
-
if (this.bufferPool.view(slotId).byteLength >= byteLength) return ok(void 0);
|
|
5031
|
-
const grown = this.bufferPool.grow(slotId, byteLength);
|
|
5032
|
-
return grown.ok ? ok(void 0) : grown;
|
|
5033
|
-
}
|
|
5034
5366
|
/**
|
|
5035
5367
|
* Remove one variable-array element at a known slot. Relationship holders
|
|
5036
5368
|
* supply the slot from their backpointer, so this is O(1) and never scans
|
|
@@ -5098,7 +5430,7 @@ var WorldComponentAccess = class {
|
|
|
5098
5430
|
* (lazy mirror create or exclusive reparent).
|
|
5099
5431
|
* @internal
|
|
5100
5432
|
*/
|
|
5101
|
-
_addComponentCore(entity, componentData, internal, resolveRequirements = true) {
|
|
5433
|
+
_addComponentCore(entity, componentData, internal, resolveRequirements = true, skipVariableArrayInitialization = false) {
|
|
5102
5434
|
const record = this.lookupAlive(entity, "addComponent", componentData.component.name);
|
|
5103
5435
|
if (!record.ok) return record;
|
|
5104
5436
|
const rec = record.value;
|
|
@@ -5144,6 +5476,19 @@ var WorldComponentAccess = class {
|
|
|
5144
5476
|
if (enumErr !== null) {
|
|
5145
5477
|
return err(enumErr);
|
|
5146
5478
|
}
|
|
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
|
+
}
|
|
5147
5492
|
if (resolveRequirements) {
|
|
5148
5493
|
const required = expandComponentRequirements([componentData]).slice(1);
|
|
5149
5494
|
for (const requirement of required) {
|
|
@@ -5153,9 +5498,13 @@ var WorldComponentAccess = class {
|
|
|
5153
5498
|
continue;
|
|
5154
5499
|
}
|
|
5155
5500
|
const added = this._addComponentCore(entity, requirement, internal, false);
|
|
5156
|
-
if (!added.ok)
|
|
5501
|
+
if (!added.ok) {
|
|
5502
|
+
this.releaseRelationshipPreparation(relationshipPreparation);
|
|
5503
|
+
return added;
|
|
5504
|
+
}
|
|
5157
5505
|
srcArch = this.graph.archetypes[rec.archetypeId];
|
|
5158
5506
|
if (!srcArch) {
|
|
5507
|
+
this.releaseRelationshipPreparation(relationshipPreparation);
|
|
5159
5508
|
return err(
|
|
5160
5509
|
new StaleEntityError(entity, entityIndex(entity), entityGeneration(entity), {
|
|
5161
5510
|
operation: "addComponent",
|
|
@@ -5167,32 +5516,21 @@ var WorldComponentAccess = class {
|
|
|
5167
5516
|
}
|
|
5168
5517
|
}
|
|
5169
5518
|
}
|
|
5170
|
-
const localId = componentId(componentData.component);
|
|
5171
5519
|
if (srcArch.components.some((candidate) => componentId(candidate) === localId)) {
|
|
5520
|
+
this.releaseRelationshipPreparation(relationshipPreparation);
|
|
5172
5521
|
const role = relationshipRole(componentData.component);
|
|
5173
5522
|
if (role?.kind === "source" && role.exclusive && !internal) {
|
|
5174
|
-
|
|
5175
|
-
componentData.component,
|
|
5176
|
-
filled
|
|
5177
|
-
);
|
|
5178
|
-
if (!prepared.ok) return prepared;
|
|
5179
|
-
const removeR = this._removeComponentCore(
|
|
5523
|
+
return this.setRelationshipSource(
|
|
5180
5524
|
entity,
|
|
5181
5525
|
componentData.component,
|
|
5182
|
-
|
|
5526
|
+
filled,
|
|
5527
|
+
rec,
|
|
5528
|
+
srcArch,
|
|
5529
|
+
true
|
|
5183
5530
|
);
|
|
5184
|
-
if (!removeR.ok) return removeR;
|
|
5185
|
-
return this._addComponentCore(entity, componentData, false);
|
|
5186
5531
|
}
|
|
5187
5532
|
return err(new ComponentAlreadyPresentError(entity, componentData.component.name));
|
|
5188
5533
|
}
|
|
5189
|
-
if (!internal && relationshipRole(componentData.component)?.kind === "source") {
|
|
5190
|
-
const prepared = this.prepareRelationshipInsert(
|
|
5191
|
-
componentData.component,
|
|
5192
|
-
filled
|
|
5193
|
-
);
|
|
5194
|
-
if (!prepared.ok) return prepared;
|
|
5195
|
-
}
|
|
5196
5534
|
const targetArch = getAddEdge(
|
|
5197
5535
|
this.graph,
|
|
5198
5536
|
srcArch,
|
|
@@ -5209,7 +5547,8 @@ var WorldComponentAccess = class {
|
|
|
5209
5547
|
targetArch,
|
|
5210
5548
|
componentData.component,
|
|
5211
5549
|
this.tableRow(rec),
|
|
5212
|
-
filled
|
|
5550
|
+
filled,
|
|
5551
|
+
skipVariableArrayInitialization ? { skipVariableArrayInitialization: true } : void 0
|
|
5213
5552
|
);
|
|
5214
5553
|
}
|
|
5215
5554
|
this.markComponentAdded(entity, componentData.component);
|
|
@@ -5217,9 +5556,13 @@ var WorldComponentAccess = class {
|
|
|
5217
5556
|
const relationshipResult = this.relationshipOnInsert(
|
|
5218
5557
|
entity,
|
|
5219
5558
|
componentData.component,
|
|
5220
|
-
filled
|
|
5559
|
+
filled,
|
|
5560
|
+
relationshipPreparation
|
|
5221
5561
|
);
|
|
5222
|
-
if (!relationshipResult.ok)
|
|
5562
|
+
if (!relationshipResult.ok) {
|
|
5563
|
+
this.releaseRelationshipPreparation(relationshipPreparation);
|
|
5564
|
+
return relationshipResult;
|
|
5565
|
+
}
|
|
5223
5566
|
}
|
|
5224
5567
|
this.markStructureChanged();
|
|
5225
5568
|
this.state.recordStructuralEvidence({
|
|
@@ -5347,6 +5690,35 @@ var WorldComponentAccess = class {
|
|
|
5347
5690
|
const slot = entityIndex(entity);
|
|
5348
5691
|
const record = this.records[slot];
|
|
5349
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
|
+
}
|
|
5350
5722
|
const componentIds = componentDatas.map((cd) => componentId(cd.component));
|
|
5351
5723
|
const components = componentDatas.map((cd) => cd.component);
|
|
5352
5724
|
const arch = getOrCreateArchetype(this.graph, componentIds, components);
|
|
@@ -5364,15 +5736,21 @@ var WorldComponentAccess = class {
|
|
|
5364
5736
|
this.storage.writeRow(arch, cd.component, tableRow2, filled);
|
|
5365
5737
|
}
|
|
5366
5738
|
this.storage.writeEntitySelf(arch, tableRow2, entity);
|
|
5367
|
-
for (
|
|
5739
|
+
for (let index = 0; index < componentDatas.length; index += 1) {
|
|
5740
|
+
const cd = componentDatas[index];
|
|
5741
|
+
if (cd === void 0) continue;
|
|
5368
5742
|
if (relationshipRole(cd.component)?.kind === "source") {
|
|
5369
5743
|
const filled = fillComponentDefaults(cd.component, cd.data);
|
|
5370
5744
|
const relationshipResult = this.relationshipOnInsert(
|
|
5371
5745
|
entity,
|
|
5372
5746
|
cd.component,
|
|
5373
|
-
filled
|
|
5747
|
+
filled,
|
|
5748
|
+
relationshipPreparations[index]
|
|
5374
5749
|
);
|
|
5375
5750
|
if (!relationshipResult.ok) {
|
|
5751
|
+
for (const reservation of relationshipPreparations) {
|
|
5752
|
+
this.releaseRelationshipPreparation(reservation);
|
|
5753
|
+
}
|
|
5376
5754
|
return relationshipResult;
|
|
5377
5755
|
}
|
|
5378
5756
|
}
|
|
@@ -6137,6 +6515,27 @@ function spawnCore(world, componentDatas, internal) {
|
|
|
6137
6515
|
if (enumErr !== null) return err(enumErr);
|
|
6138
6516
|
filledData.push(filled);
|
|
6139
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
|
+
}
|
|
6140
6539
|
const indexSlot = world[worldInternal].allocateIndex();
|
|
6141
6540
|
const record = world[worldInternal].getRecords()[indexSlot];
|
|
6142
6541
|
if (record === void 0)
|
|
@@ -6172,9 +6571,15 @@ function spawnCore(world, componentDatas, internal) {
|
|
|
6172
6571
|
const relation = world[worldInternal].relationshipOnInsert(
|
|
6173
6572
|
spawnedEntity,
|
|
6174
6573
|
cd.component,
|
|
6175
|
-
filled
|
|
6574
|
+
filled,
|
|
6575
|
+
relationshipPreparations[i]
|
|
6176
6576
|
);
|
|
6177
|
-
if (!relation.ok)
|
|
6577
|
+
if (!relation.ok) {
|
|
6578
|
+
for (const reservation of relationshipPreparations) {
|
|
6579
|
+
world[worldInternal].releaseRelationshipPreparation(reservation);
|
|
6580
|
+
}
|
|
6581
|
+
return relation;
|
|
6582
|
+
}
|
|
6178
6583
|
}
|
|
6179
6584
|
}
|
|
6180
6585
|
world[worldInternal].markStructureChanged();
|
|
@@ -6248,7 +6653,8 @@ function despawnCore(world, entity, internal) {
|
|
|
6248
6653
|
}
|
|
6249
6654
|
function worldAddChild(world, parent, child, component, data) {
|
|
6250
6655
|
const holderComp = component;
|
|
6251
|
-
|
|
6656
|
+
const role = relationshipRole(holderComp);
|
|
6657
|
+
if (role?.kind !== "source") {
|
|
6252
6658
|
return err(new ComponentNotPresentError(child, component.name));
|
|
6253
6659
|
}
|
|
6254
6660
|
const parentSlot = entityIndex(parent);
|
|
@@ -6277,7 +6683,6 @@ function worldAddChild(world, parent, child, component, data) {
|
|
|
6277
6683
|
})
|
|
6278
6684
|
);
|
|
6279
6685
|
}
|
|
6280
|
-
const role = relationshipRole(holderComp);
|
|
6281
6686
|
if (child === parent && !(role?.kind === "source" && role.allowSelf)) {
|
|
6282
6687
|
return err(new RelationshipSelfCycleError(component.name, child, child));
|
|
6283
6688
|
}
|
|
@@ -6328,10 +6733,10 @@ function worldRemoveChild(world, parent, child, component) {
|
|
|
6328
6733
|
}
|
|
6329
6734
|
function worldReparent(world, child, newParent, component, data) {
|
|
6330
6735
|
const holderComp = component;
|
|
6331
|
-
|
|
6736
|
+
const role = relationshipRole(holderComp);
|
|
6737
|
+
if (role?.kind !== "source") {
|
|
6332
6738
|
return err(new ComponentNotPresentError(child, component.name));
|
|
6333
6739
|
}
|
|
6334
|
-
const role = relationshipRole(holderComp);
|
|
6335
6740
|
if (child === newParent && !(role?.kind === "source" && role.allowSelf)) {
|
|
6336
6741
|
return err(
|
|
6337
6742
|
new RelationshipSelfCycleError(component.name, child, newParent)
|
|
@@ -6361,11 +6766,14 @@ function worldReparent(world, child, newParent, component, data) {
|
|
|
6361
6766
|
})
|
|
6362
6767
|
);
|
|
6363
6768
|
}
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
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);
|
|
6367
6775
|
}
|
|
6368
|
-
return world.addComponent(child, { component, data });
|
|
6776
|
+
return world.addComponent(child, { component, data: payload });
|
|
6369
6777
|
}
|
|
6370
6778
|
function worldIterAncestors(world, entity) {
|
|
6371
6779
|
return {
|
|
@@ -6986,6 +7394,9 @@ var World = class {
|
|
|
6986
7394
|
cancelPendingEntity: this.internalcancelPendingEntity.bind(this),
|
|
6987
7395
|
despawnCore: this.internaldespawnCore.bind(this),
|
|
6988
7396
|
getArrayView: this.internalgetArrayView.bind(this),
|
|
7397
|
+
getArrayLength: this.internalgetArrayLength.bind(this),
|
|
7398
|
+
getArrayElement: this.internalgetArrayElement.bind(this),
|
|
7399
|
+
getFieldValue: this.internalgetFieldValue.bind(this),
|
|
6989
7400
|
getBufferPool: this.internalgetBufferPool.bind(this),
|
|
6990
7401
|
getClockWriter: this.internalgetClockWriter.bind(this),
|
|
6991
7402
|
getComponentChange: this.internalgetComponentChange.bind(this),
|
|
@@ -7016,6 +7427,7 @@ var World = class {
|
|
|
7016
7427
|
materializePendingEntity: this.internalmaterializePendingEntity.bind(this),
|
|
7017
7428
|
nextMutationEpoch: this.internalnextMutationEpoch.bind(this),
|
|
7018
7429
|
poisonExecution: this.internalpoisonExecution.bind(this),
|
|
7430
|
+
prepareRelationshipInsert: this.internalprepareRelationshipInsert.bind(this),
|
|
7019
7431
|
publishDerivedRange: this.internalpublishDerivedRange.bind(this),
|
|
7020
7432
|
preflightComponentData: this.internalpreflightComponentData.bind(this),
|
|
7021
7433
|
readRow: this.internalreadRow.bind(this),
|
|
@@ -7024,6 +7436,7 @@ var World = class {
|
|
|
7024
7436
|
relationshipOnInsert: this.internalrelationshipOnInsert.bind(this),
|
|
7025
7437
|
relationshipOnRemove: this.internalrelationshipOnRemove.bind(this),
|
|
7026
7438
|
releaseManagedRefsOnRow: this.internalreleaseManagedRefsOnRow.bind(this),
|
|
7439
|
+
releaseRelationshipPreparation: this.internalreleaseRelationshipPreparation.bind(this),
|
|
7027
7440
|
removeComponentCore: this.internalremoveComponentCore.bind(this),
|
|
7028
7441
|
routeError: this.internalrouteError.bind(this),
|
|
7029
7442
|
restoreMutationEpoch: this.internalrestoreMutationEpoch.bind(this),
|
|
@@ -7175,7 +7588,12 @@ var World = class {
|
|
|
7175
7588
|
}
|
|
7176
7589
|
/** Query facade write after the facade has already marked evidence. */
|
|
7177
7590
|
internalsetQueryRow(entity, component, value) {
|
|
7178
|
-
return this.componentAccess.set(
|
|
7591
|
+
return this.componentAccess.set(
|
|
7592
|
+
entity,
|
|
7593
|
+
component,
|
|
7594
|
+
value,
|
|
7595
|
+
relationshipRole(component)?.kind === "source"
|
|
7596
|
+
);
|
|
7179
7597
|
}
|
|
7180
7598
|
/** Query facade read that does not re-enter the public World API. */
|
|
7181
7599
|
internalgetQueryRow(entity, component) {
|
|
@@ -7523,6 +7941,15 @@ var World = class {
|
|
|
7523
7941
|
internalgetArrayView(entity, component, fieldName) {
|
|
7524
7942
|
return this.componentAccess._getArrayView(entity, component, fieldName);
|
|
7525
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
|
+
}
|
|
7526
7953
|
set(entity, component, value) {
|
|
7527
7954
|
if (isRelationshipTarget(component)) return this.relationshipTargetWriteError(component, "set");
|
|
7528
7955
|
return this.componentAccess.set(entity, component, value);
|
|
@@ -7591,8 +8018,16 @@ var World = class {
|
|
|
7591
8018
|
this.componentAccess.releaseManagedRefsOnRow(a, c, r);
|
|
7592
8019
|
}
|
|
7593
8020
|
/** */
|
|
7594
|
-
internalrelationshipOnInsert(h, c, v) {
|
|
7595
|
-
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);
|
|
7596
8031
|
}
|
|
7597
8032
|
/** */
|
|
7598
8033
|
internalrelationshipOnRemove(h, c, v) {
|