@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
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
2
2
|
import type { Component } from '../component';
|
|
3
3
|
import * as componentOwner from '../component';
|
|
4
|
+
import { componentId } from '../component';
|
|
5
|
+
import { Entity } from '../entity';
|
|
6
|
+
import type { EntityHandle } from '../entity-handle';
|
|
4
7
|
import {
|
|
5
8
|
DerivedRangeOutOfBoundsError,
|
|
6
9
|
SharedKernelFailureError,
|
|
@@ -13,11 +16,21 @@ import type { MutableColumnShape, ReadonlyColumnShape } from './query';
|
|
|
13
16
|
|
|
14
17
|
/** Package-internal whole-column storage owned by one dense query table. */
|
|
15
18
|
export interface DerivedColumnBinding<R extends Component, C extends Component> {
|
|
19
|
+
/** Physical table identity for package-internal projection consumers. */
|
|
20
|
+
readonly tableId: number;
|
|
21
|
+
/** Whole entity column; every dense table carries the essential Entity row. */
|
|
22
|
+
readonly entities: Readonly<Uint32Array>;
|
|
16
23
|
readonly read: ReadonlyColumnShape<R>;
|
|
17
24
|
readonly write: MutableColumnShape<C>;
|
|
18
25
|
readonly rowCapacity: number;
|
|
19
26
|
}
|
|
20
27
|
|
|
28
|
+
/** Reusable identity cursor for O(1) dense-table lookup without a row facade. */
|
|
29
|
+
export interface DerivedRangeCursor {
|
|
30
|
+
bindingIndex: number;
|
|
31
|
+
row: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
export type DerivedRangeKernel<R extends Component, C extends Component> = (
|
|
22
35
|
binding: DerivedColumnBinding<R, C>,
|
|
23
36
|
base: number,
|
|
@@ -97,6 +110,10 @@ function countAllocation(name: keyof DerivedRangeAllocationTrace): void {
|
|
|
97
110
|
|
|
98
111
|
export interface DerivedRangeWriter<R extends Component, C extends Component> {
|
|
99
112
|
readonly bindings: readonly DerivedColumnBinding<R, C>[];
|
|
113
|
+
/** Locate a live entity in its bound table and return its physical row. */
|
|
114
|
+
locateEntity(entity: EntityHandle, cursor: DerivedRangeCursor): boolean;
|
|
115
|
+
/** Publish rows whose values were written directly into a derived column. */
|
|
116
|
+
publishChangedRows(bindingIndex: number, changed: Uint8Array): Result<void, EcsError>;
|
|
100
117
|
writeRange(
|
|
101
118
|
bindingIndex: number,
|
|
102
119
|
base: number,
|
|
@@ -130,6 +147,7 @@ export function createDerivedRangeWriter<R extends Component, C extends Componen
|
|
|
130
147
|
let boundEpoch = -1;
|
|
131
148
|
let bindingTables: readonly Table[] = [];
|
|
132
149
|
let bindings: readonly DerivedColumnBinding<R, C>[] = [];
|
|
150
|
+
let bindingByTable = new Map<number, number>();
|
|
133
151
|
let runStartBuffers: readonly Int32Array[] = [];
|
|
134
152
|
let runCountBuffers: readonly Int32Array[] = [];
|
|
135
153
|
|
|
@@ -141,6 +159,9 @@ export function createDerivedRangeWriter<R extends Component, C extends Componen
|
|
|
141
159
|
const nextRunCountBuffers: Int32Array[] = [];
|
|
142
160
|
for (const table of tables) {
|
|
143
161
|
nextBindings.push({
|
|
162
|
+
tableId: table.id,
|
|
163
|
+
entities: (table.storage.get(componentId(Entity))?.fields.get('self')?.view ??
|
|
164
|
+
new Uint32Array(0)) as Uint32Array,
|
|
144
165
|
read: buildWholeColumnShape(table, source.readComponents) as ReadonlyColumnShape<R>,
|
|
145
166
|
write: buildWholeColumnShape(table, source.writeComponents) as MutableColumnShape<C>,
|
|
146
167
|
rowCapacity: table.size,
|
|
@@ -153,6 +174,11 @@ export function createDerivedRangeWriter<R extends Component, C extends Componen
|
|
|
153
174
|
}
|
|
154
175
|
bindingTables = tables;
|
|
155
176
|
bindings = nextBindings;
|
|
177
|
+
bindingByTable = new Map<number, number>();
|
|
178
|
+
for (let index = 0; index < nextBindings.length; index += 1) {
|
|
179
|
+
const binding = nextBindings[index];
|
|
180
|
+
if (binding !== undefined) bindingByTable.set(binding.tableId, index);
|
|
181
|
+
}
|
|
156
182
|
runStartBuffers = nextRunStartBuffers;
|
|
157
183
|
runCountBuffers = nextRunCountBuffers;
|
|
158
184
|
boundEpoch = source.structureEpoch();
|
|
@@ -165,6 +191,91 @@ export function createDerivedRangeWriter<R extends Component, C extends Componen
|
|
|
165
191
|
if (boundEpoch !== source.structureEpoch()) rebind();
|
|
166
192
|
return bindings;
|
|
167
193
|
},
|
|
194
|
+
locateEntity(entity, cursor) {
|
|
195
|
+
if (world.execution.health === 'poisoned') return false;
|
|
196
|
+
if (boundEpoch !== source.structureEpoch()) rebind();
|
|
197
|
+
const archetype = world[worldInternal].getEntityArchetype(entity);
|
|
198
|
+
if (archetype === undefined) return false;
|
|
199
|
+
const bindingIndex = bindingByTable.get(archetype.tableId);
|
|
200
|
+
if (bindingIndex === undefined) return false;
|
|
201
|
+
const record = world[worldInternal].getRecords()[(entity as unknown as number) & 0x00ffffff];
|
|
202
|
+
if (record === undefined || record.archetypeId !== archetype.id) return false;
|
|
203
|
+
const row = archetype.rows[record.archetypeRow];
|
|
204
|
+
const binding = bindings[bindingIndex];
|
|
205
|
+
if (binding === undefined || row === undefined || row < 0 || row >= binding.rowCapacity) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
cursor.bindingIndex = bindingIndex;
|
|
209
|
+
cursor.row = row;
|
|
210
|
+
return true;
|
|
211
|
+
},
|
|
212
|
+
publishChangedRows(bindingIndex, changed) {
|
|
213
|
+
if (world.execution.health === 'poisoned') {
|
|
214
|
+
return err(new WorldPoisonedError(world.identity, world.execution.fault));
|
|
215
|
+
}
|
|
216
|
+
if (boundEpoch !== source.structureEpoch()) rebind();
|
|
217
|
+
const binding = bindings[bindingIndex];
|
|
218
|
+
const table = bindingTables[bindingIndex];
|
|
219
|
+
const runStarts = runStartBuffers[bindingIndex];
|
|
220
|
+
const runCounts = runCountBuffers[bindingIndex];
|
|
221
|
+
if (
|
|
222
|
+
binding === undefined ||
|
|
223
|
+
table === undefined ||
|
|
224
|
+
runStarts === undefined ||
|
|
225
|
+
runCounts === undefined ||
|
|
226
|
+
!Number.isSafeInteger(bindingIndex) ||
|
|
227
|
+
bindingIndex < 0 ||
|
|
228
|
+
changed.length < binding.rowCapacity ||
|
|
229
|
+
table.storage.get(componentOwner.componentId(component)) === undefined
|
|
230
|
+
) {
|
|
231
|
+
return err(new DerivedRangeOutOfBoundsError(0, changed.length, binding?.rowCapacity ?? 0));
|
|
232
|
+
}
|
|
233
|
+
let runCount = 0;
|
|
234
|
+
let runStart = -1;
|
|
235
|
+
for (let row = 0; row < binding.rowCapacity; row += 1) {
|
|
236
|
+
if ((changed[row] ?? 0) !== 0) {
|
|
237
|
+
if (runStart < 0) runStart = row;
|
|
238
|
+
} else if (runStart >= 0) {
|
|
239
|
+
runStarts[runCount] = runStart;
|
|
240
|
+
runCounts[runCount] = row - runStart;
|
|
241
|
+
runCount += 1;
|
|
242
|
+
runStart = -1;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (runStart >= 0) {
|
|
246
|
+
runStarts[runCount] = runStart;
|
|
247
|
+
runCounts[runCount] = binding.rowCapacity - runStart;
|
|
248
|
+
runCount += 1;
|
|
249
|
+
}
|
|
250
|
+
if (runCount === 0) return ok(undefined);
|
|
251
|
+
const previousEpoch = world[worldInternal].getMutationEpoch();
|
|
252
|
+
let epoch: number;
|
|
253
|
+
try {
|
|
254
|
+
epoch = world[worldInternal].nextMutationEpoch();
|
|
255
|
+
const componentIdentifier = componentOwner.componentId(component);
|
|
256
|
+
for (let index = 0; index < runCount; index += 1) {
|
|
257
|
+
world[worldInternal].publishDerivedRange(
|
|
258
|
+
table,
|
|
259
|
+
componentIdentifier,
|
|
260
|
+
runStarts[index] ?? 0,
|
|
261
|
+
runCounts[index] ?? 0,
|
|
262
|
+
epoch,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
changed.fill(0, 0, binding.rowCapacity);
|
|
266
|
+
return ok(undefined);
|
|
267
|
+
} catch (cause) {
|
|
268
|
+
world[worldInternal].restoreMutationEpoch(previousEpoch);
|
|
269
|
+
world[worldInternal].poisonExecution({
|
|
270
|
+
code: 'shared-kernel-failed',
|
|
271
|
+
kernelName: `derived-range:${component.name}:publish`,
|
|
272
|
+
cause,
|
|
273
|
+
partialWrite: true,
|
|
274
|
+
retryable: false,
|
|
275
|
+
});
|
|
276
|
+
return err(new SharedKernelFailureError(component.name, world.identity, cause, true));
|
|
277
|
+
}
|
|
278
|
+
},
|
|
168
279
|
writeRange(bindingIndex, base, start, count, kernel, context) {
|
|
169
280
|
if (world.execution.health === 'poisoned') {
|
|
170
281
|
return err(new WorldPoisonedError(world.identity, world.execution.fault));
|
package/src/query/query.ts
CHANGED
|
@@ -18,8 +18,10 @@ import {
|
|
|
18
18
|
QueryIterationInvalidatedError,
|
|
19
19
|
QuerySpanUnavailableError,
|
|
20
20
|
type QuerySpanUnavailableReason,
|
|
21
|
+
RelationshipTargetReadonlyError,
|
|
21
22
|
} from '../errors';
|
|
22
23
|
import { DERIVED_WRITER } from '../internal';
|
|
24
|
+
import { isRelationshipTarget, relationshipRole } from '../relationship-index';
|
|
23
25
|
import type { Archetype, ArchetypeId } from '../storage/archetype';
|
|
24
26
|
import type { ArchetypeGraph } from '../storage/archetype-graph';
|
|
25
27
|
import { sparseTagIndex } from '../storage/change-detection';
|
|
@@ -229,7 +231,17 @@ class QueryRowFacade<
|
|
|
229
231
|
mut<C extends W[number]>(component: C): MutableRowShape<C> {
|
|
230
232
|
const current = this.get(component);
|
|
231
233
|
if (current === undefined) throw new Error(`Query row lacks ${component.name}.`);
|
|
232
|
-
|
|
234
|
+
if (isRelationshipTarget(component)) {
|
|
235
|
+
throw new RelationshipTargetReadonlyError(component.name, 'query row');
|
|
236
|
+
}
|
|
237
|
+
// Relationship sources must let the owner validate before recording
|
|
238
|
+
// authored evidence. A stale/cyclic target therefore has zero mutation
|
|
239
|
+
// side effects, while ordinary components retain the existing eager
|
|
240
|
+
// `mut()` evidence semantics.
|
|
241
|
+
const relationshipSource = relationshipRole(component)?.kind === 'source';
|
|
242
|
+
if (!relationshipSource) {
|
|
243
|
+
this.world[worldInternal].markComponentChanged(this.entity, componentId(component));
|
|
244
|
+
}
|
|
233
245
|
return new Proxy(current, {
|
|
234
246
|
set: (target, property, value) => {
|
|
235
247
|
if (typeof property !== 'string') return false;
|
|
@@ -275,6 +287,12 @@ class QuerySpanFacade<R extends readonly Component[], W extends readonly Compone
|
|
|
275
287
|
}
|
|
276
288
|
|
|
277
289
|
mut<C extends W[number]>(component: C): MutableColumnShape<C> {
|
|
290
|
+
// A span exposes live column storage. Relationship source and target
|
|
291
|
+
// columns must never obtain that raw write capability: source writes need
|
|
292
|
+
// target-side maintenance and target writes are engine-owned projections.
|
|
293
|
+
if (relationshipRole(component) !== undefined) {
|
|
294
|
+
throw new RelationshipTargetReadonlyError(component.name, 'query span');
|
|
295
|
+
}
|
|
278
296
|
this.world[worldInternal].markComponentRangeChanged(
|
|
279
297
|
this.table,
|
|
280
298
|
componentId(component),
|
|
@@ -662,6 +680,13 @@ class ExecutableQuery<
|
|
|
662
680
|
private spanUnavailableReason(): QuerySpanUnavailableReason | undefined {
|
|
663
681
|
if (this.compiled.sparseRoute) return 'sparse-component';
|
|
664
682
|
if ((this.compiled.descriptor.optional?.length ?? 0) > 0) return 'optional-data';
|
|
683
|
+
// A writable relationship source would bypass the source owner (and its
|
|
684
|
+
// materialized target/backpointer), while a target is an ECS-owned
|
|
685
|
+
// projection. Keep read-only relationship inputs usable by Scene's
|
|
686
|
+
// derived writer; only the descriptor's writable role is ineligible.
|
|
687
|
+
if ((this.compiled.descriptor.write ?? []).some((component) => relationshipRole(component))) {
|
|
688
|
+
return 'relationship-component';
|
|
689
|
+
}
|
|
665
690
|
return undefined;
|
|
666
691
|
}
|
|
667
692
|
}
|