@forgeax/engine-ecs 0.1.33 → 0.1.35
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 +35 -18
- package/dist/__tests__/query-idle.unit.test.d.ts +2 -0
- package/dist/__tests__/query-idle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/set-allocation.unit.test.d.ts +2 -0
- package/dist/__tests__/set-allocation.unit.test.d.ts.map +1 -0
- package/dist/__tests__/state-projection.unit.test.d.ts +2 -0
- package/dist/__tests__/state-projection.unit.test.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +579 -623
- package/dist/index.mjs.map +1 -1
- package/dist/projection/index.d.ts +2 -18
- package/dist/projection/index.d.ts.map +1 -1
- package/dist/projection/index.mjs +433 -23
- package/dist/projection/index.mjs.map +1 -1
- package/dist/projection/state-projection.d.ts +37 -0
- package/dist/projection/state-projection.d.ts.map +1 -0
- package/dist/query/query.d.ts.map +1 -1
- package/dist/shared-ref-store.d.ts +0 -24
- package/dist/shared-ref-store.d.ts.map +1 -1
- package/dist/shared.mjs.map +1 -1
- package/dist/storage/archetype-graph.d.ts +2 -0
- package/dist/storage/archetype-graph.d.ts.map +1 -1
- package/dist/storage/change-detection.d.ts +4 -0
- package/dist/storage/change-detection.d.ts.map +1 -1
- package/dist/storage/table.d.ts +6 -3
- package/dist/storage/table.d.ts.map +1 -1
- package/dist/world-internal.d.ts +0 -2
- package/dist/world-internal.d.ts.map +1 -1
- package/dist/world.d.ts +0 -2
- package/dist/world.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/component-version-surface.test.ts +1 -7
- package/src/__tests__/derived-range-writer.contract.test.ts +6 -0
- package/src/__tests__/ecs-core-reduction.characterization.test.ts +2 -3
- package/src/__tests__/execution-conflict-boundary.unit.test.ts +4 -0
- package/src/__tests__/externalization-render-read-lease.unit.test.ts +4 -10
- package/src/__tests__/query-idle.unit.test.ts +19 -0
- package/src/__tests__/set-allocation.unit.test.ts +30 -0
- package/src/__tests__/shared-ref-lifetime.unit.test.ts +1 -2
- package/src/__tests__/shared-ref-store.unit.test.ts +2 -34
- package/src/__tests__/state-projection.unit.test.ts +159 -0
- package/src/__tests__/world-health.contract.test.ts +0 -23
- package/src/index.ts +1 -1
- package/src/projection/index.ts +9 -39
- package/src/projection/state-projection.ts +250 -0
- package/src/query/query.ts +23 -16
- package/src/shared-ref-store.ts +0 -70
- package/src/storage/archetype-graph.ts +15 -1
- package/src/storage/change-detection.ts +36 -4
- package/src/storage/table.ts +20 -1
- package/src/world-internal.ts +0 -2
- package/src/world.ts +35 -38
- package/dist/__tests__/structural-evidence.contract.test-d.d.ts +0 -2
- package/dist/__tests__/structural-evidence.contract.test-d.d.ts.map +0 -1
- package/dist/__tests__/structural-evidence.contract.test.d.ts +0 -2
- package/dist/__tests__/structural-evidence.contract.test.d.ts.map +0 -1
- package/dist/storage/structural-evidence.d.ts +0 -30
- package/dist/storage/structural-evidence.d.ts.map +0 -1
- package/src/__tests__/structural-evidence.contract.test-d.ts +0 -6
- package/src/__tests__/structural-evidence.contract.test.ts +0 -49
- package/src/storage/structural-evidence.ts +0 -64
|
@@ -29,6 +29,8 @@ export interface ArchetypeGraph {
|
|
|
29
29
|
/** Global generation counter. Incremented on each new archetype. */
|
|
30
30
|
generation: number;
|
|
31
31
|
tables: Table[];
|
|
32
|
+
activeTables: Set<Table>;
|
|
33
|
+
activeTablesByComponent: Map<ComponentId, Set<Table>>;
|
|
32
34
|
tableDedupByKey: Map<string, TableId>;
|
|
33
35
|
tableGeneration: number;
|
|
34
36
|
sparseTags: Map<ComponentId, SparseTagSet>;
|
|
@@ -44,6 +46,8 @@ export function createArchetypeGraph(shared = false): ArchetypeGraph {
|
|
|
44
46
|
dedupByKey: new Map(),
|
|
45
47
|
generation: 0,
|
|
46
48
|
tables: [],
|
|
49
|
+
activeTables: new Set(),
|
|
50
|
+
activeTablesByComponent: new Map(),
|
|
47
51
|
tableDedupByKey: new Map(),
|
|
48
52
|
tableGeneration: 0,
|
|
49
53
|
sparseTags: new Map(),
|
|
@@ -67,7 +71,17 @@ export function getOrCreateTable(
|
|
|
67
71
|
const key = tableKey(tableComponents.map((component) => componentOwner.componentId(component)));
|
|
68
72
|
const existingId = graph.tableDedupByKey.get(key);
|
|
69
73
|
if (existingId !== undefined) return getTable(graph, existingId);
|
|
70
|
-
const
|
|
74
|
+
const directories = [graph.activeTables];
|
|
75
|
+
for (const component of tableComponents) {
|
|
76
|
+
const id = componentOwner.componentId(component);
|
|
77
|
+
let directory = graph.activeTablesByComponent.get(id);
|
|
78
|
+
if (directory === undefined) {
|
|
79
|
+
directory = new Set();
|
|
80
|
+
graph.activeTablesByComponent.set(id, directory);
|
|
81
|
+
}
|
|
82
|
+
directories.push(directory);
|
|
83
|
+
}
|
|
84
|
+
const table = createTable(tableComponents, graph.tables.length, graph.shared, directories);
|
|
71
85
|
graph.tables.push(table);
|
|
72
86
|
graph.tableDedupByKey.set(key, table.id);
|
|
73
87
|
graph.tableGeneration += 1;
|
|
@@ -7,15 +7,22 @@ import { type EntityHandle, entityIndex } from '../entity-handle';
|
|
|
7
7
|
import type { ArchetypeGraph } from './archetype-graph';
|
|
8
8
|
import { getOrCreateSparseTagSet } from './archetype-graph';
|
|
9
9
|
|
|
10
|
+
export const PROJECTION_BLOCK_SIZE = 256;
|
|
11
|
+
|
|
10
12
|
const INITIAL_SPARSE_CAPACITY = 64;
|
|
11
13
|
|
|
12
14
|
export interface ComponentEpochColumns {
|
|
13
15
|
added: Float64Array;
|
|
14
16
|
changed: Float64Array;
|
|
17
|
+
blocks: Float64Array;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
export function createComponentEpochColumns(capacity: number): ComponentEpochColumns {
|
|
18
|
-
return {
|
|
21
|
+
return {
|
|
22
|
+
added: new Float64Array(capacity),
|
|
23
|
+
changed: new Float64Array(capacity),
|
|
24
|
+
blocks: new Float64Array(Math.ceil(capacity / PROJECTION_BLOCK_SIZE)),
|
|
25
|
+
};
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
export function growComponentEpochColumns(
|
|
@@ -26,7 +33,9 @@ export function growComponentEpochColumns(
|
|
|
26
33
|
const changed = new Float64Array(capacity);
|
|
27
34
|
added.set(columns.added);
|
|
28
35
|
changed.set(columns.changed);
|
|
29
|
-
|
|
36
|
+
const blocks = new Float64Array(Math.ceil(capacity / PROJECTION_BLOCK_SIZE));
|
|
37
|
+
blocks.set(columns.blocks);
|
|
38
|
+
return { added, changed, blocks };
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
export function copyComponentEpoch(
|
|
@@ -37,6 +46,8 @@ export function copyComponentEpoch(
|
|
|
37
46
|
): void {
|
|
38
47
|
target.added[targetRow] = source.added[sourceRow] ?? 0;
|
|
39
48
|
target.changed[targetRow] = source.changed[sourceRow] ?? 0;
|
|
49
|
+
const block = Math.floor(targetRow / PROJECTION_BLOCK_SIZE);
|
|
50
|
+
target.blocks[block] = Math.max(target.blocks[block] ?? 0, target.changed[targetRow] ?? 0);
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
export interface SparseTagSet {
|
|
@@ -190,7 +201,7 @@ export function markComponentsAdded(
|
|
|
190
201
|
const epochs = table?.storage.get(componentId)?.epochs;
|
|
191
202
|
if (epochs === undefined) continue;
|
|
192
203
|
epochs.added[tableRow] = epoch;
|
|
193
|
-
epochs
|
|
204
|
+
publishComponentRange(epochs, tableRow, 1, epoch);
|
|
194
205
|
}
|
|
195
206
|
}
|
|
196
207
|
|
|
@@ -212,5 +223,26 @@ export function markComponentChanged(
|
|
|
212
223
|
const epochs = graph.tables[archetype.tableId]?.storage.get(componentId)?.epochs;
|
|
213
224
|
if (epochs === undefined) return;
|
|
214
225
|
const tableRow = archetype.rows[location.archetypeRow] ?? -1;
|
|
215
|
-
epochs
|
|
226
|
+
publishComponentRange(epochs, tableRow, 1, epoch());
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Row evidence and its conservative block summary share the World epoch. */
|
|
230
|
+
export function publishComponentRange(
|
|
231
|
+
columns: ComponentEpochColumns,
|
|
232
|
+
start: number,
|
|
233
|
+
count: number,
|
|
234
|
+
epoch: number,
|
|
235
|
+
): void {
|
|
236
|
+
if (count === 0) return;
|
|
237
|
+
if (count === 1) {
|
|
238
|
+
columns.changed[start] = epoch;
|
|
239
|
+
columns.blocks[Math.floor(start / PROJECTION_BLOCK_SIZE)] = epoch;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
columns.changed.fill(epoch, start, start + count);
|
|
243
|
+
columns.blocks.fill(
|
|
244
|
+
epoch,
|
|
245
|
+
Math.floor(start / PROJECTION_BLOCK_SIZE),
|
|
246
|
+
Math.ceil((start + count) / PROJECTION_BLOCK_SIZE),
|
|
247
|
+
);
|
|
216
248
|
}
|
package/src/storage/table.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
copyComponentEpoch,
|
|
18
18
|
createComponentEpochColumns,
|
|
19
19
|
growComponentEpochColumns,
|
|
20
|
+
PROJECTION_BLOCK_SIZE,
|
|
20
21
|
} from './change-detection';
|
|
21
22
|
import { arrayCountColumnName, type Column, createColumn, growColumn } from './column';
|
|
22
23
|
|
|
@@ -38,6 +39,8 @@ export interface Table {
|
|
|
38
39
|
size: number;
|
|
39
40
|
capacity: number;
|
|
40
41
|
version: number;
|
|
42
|
+
membership: Float64Array;
|
|
43
|
+
readonly activeDirectories: readonly Set<Table>[];
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
export function tableKey(componentIds: ReadonlyArray<ComponentId>): string {
|
|
@@ -55,6 +58,7 @@ export function createTable(
|
|
|
55
58
|
components: ReadonlyArray<Component>,
|
|
56
59
|
id: TableId,
|
|
57
60
|
shared = false,
|
|
61
|
+
activeDirectories: readonly Set<Table>[] = [new Set<Table>()],
|
|
58
62
|
): Table {
|
|
59
63
|
const sortedComponents = canonicalComponents(components);
|
|
60
64
|
const capacity = INITIAL_CAPACITY;
|
|
@@ -102,23 +106,31 @@ export function createTable(
|
|
|
102
106
|
size: 0,
|
|
103
107
|
capacity,
|
|
104
108
|
version: 0,
|
|
109
|
+
membership: new Float64Array(Math.ceil(capacity / PROJECTION_BLOCK_SIZE)),
|
|
110
|
+
activeDirectories,
|
|
105
111
|
};
|
|
106
112
|
}
|
|
107
113
|
|
|
108
|
-
export function appendTableRow(table: Table, entity: EntityHandle): number {
|
|
114
|
+
export function appendTableRow(table: Table, entity: EntityHandle, epoch = 0): number {
|
|
109
115
|
if (table.size === table.capacity) growTable(table, table.capacity * 2);
|
|
110
116
|
const row = table.size;
|
|
111
117
|
const self = table.storage.get(componentId(Entity))?.fields.get('self');
|
|
112
118
|
if (self !== undefined) self.view[row] = entity as number;
|
|
113
119
|
table.size = row + 1;
|
|
120
|
+
if (row === 0) for (const directory of table.activeDirectories) directory.add(table);
|
|
121
|
+
markTableMembership(table, row, epoch);
|
|
114
122
|
return row;
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
export function removeTableRow(
|
|
118
126
|
table: Table,
|
|
119
127
|
row: number,
|
|
128
|
+
epoch = 0,
|
|
120
129
|
): { movedEntity: EntityHandle; newRow: number } | null {
|
|
121
130
|
const lastRow = table.size - 1;
|
|
131
|
+
markTableMembership(table, row, epoch);
|
|
132
|
+
markTableMembership(table, lastRow, epoch);
|
|
133
|
+
if (lastRow === 0) for (const directory of table.activeDirectories) directory.delete(table);
|
|
122
134
|
if (row === lastRow) {
|
|
123
135
|
table.size = lastRow;
|
|
124
136
|
return null;
|
|
@@ -148,6 +160,13 @@ export function growTable(table: Table, targetCapacity: number): void {
|
|
|
148
160
|
componentStorage.fields = fields;
|
|
149
161
|
componentStorage.epochs = growComponentEpochColumns(componentStorage.epochs, capacity);
|
|
150
162
|
}
|
|
163
|
+
const membership = new Float64Array(Math.ceil(capacity / PROJECTION_BLOCK_SIZE));
|
|
164
|
+
membership.set(table.membership);
|
|
165
|
+
table.membership = membership;
|
|
151
166
|
table.capacity = capacity;
|
|
152
167
|
table.version += 1;
|
|
153
168
|
}
|
|
169
|
+
|
|
170
|
+
export function markTableMembership(table: Table, row: number, epoch: number): void {
|
|
171
|
+
table.membership[Math.floor(row / PROJECTION_BLOCK_SIZE)] = epoch;
|
|
172
|
+
}
|
package/src/world-internal.ts
CHANGED
|
@@ -23,7 +23,6 @@ import type { SharedRefStore } from './shared-ref-store';
|
|
|
23
23
|
import type { Archetype } from './storage/archetype';
|
|
24
24
|
import type { ArchetypeGraph } from './storage/archetype-graph';
|
|
25
25
|
import type { ChangeTicks } from './storage/change-detection';
|
|
26
|
-
import type { StructuralEvidenceRing } from './storage/structural-evidence';
|
|
27
26
|
import type { Table } from './storage/table';
|
|
28
27
|
import type { ClockWriter } from './time';
|
|
29
28
|
import type { ComponentData, EcsError, EntityRecord } from './world';
|
|
@@ -75,7 +74,6 @@ export interface WorldInternal {
|
|
|
75
74
|
readonly getSchedules: () => ReadonlyMap<ScheduleToken, Schedule>;
|
|
76
75
|
readonly getSharedRefs: () => SharedRefStore;
|
|
77
76
|
readonly getStructureEpoch: () => number;
|
|
78
|
-
readonly getStructuralEvidence: () => StructuralEvidenceRing;
|
|
79
77
|
readonly lookupAlive: (
|
|
80
78
|
entity: EntityHandle,
|
|
81
79
|
operation: string,
|
package/src/world.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { publishComponentRange } from './storage/change-detection';
|
|
1
2
|
// @forgeax/engine-ecs — World: top-level ECS container.
|
|
2
3
|
//
|
|
3
4
|
// World owns entities, archetypes (via ArchetypeGraph), and component registry.
|
|
@@ -131,9 +132,7 @@ import {
|
|
|
131
132
|
type FieldView,
|
|
132
133
|
normalizeBufferWrite,
|
|
133
134
|
} from './storage/column';
|
|
134
|
-
import type
|
|
135
|
-
import { StructuralEvidenceRing as StructuralEvidenceRingImpl } from './storage/structural-evidence';
|
|
136
|
-
import { appendTableRow, removeTableRow, type Table } from './storage/table';
|
|
135
|
+
import { appendTableRow, markTableMembership, removeTableRow, type Table } from './storage/table';
|
|
137
136
|
import {
|
|
138
137
|
createWorldClock,
|
|
139
138
|
DEFAULT_TIME_POLICY,
|
|
@@ -373,7 +372,6 @@ export class World {
|
|
|
373
372
|
/** Per-World shared-ref store; public read-only for direct handle operations. */
|
|
374
373
|
readonly sharedRefs: SharedRefStore = new SharedRefStoreImpl();
|
|
375
374
|
private readonly componentMutationEpochs: number[] = [];
|
|
376
|
-
private readonly structuralEvidence = new StructuralEvidenceRingImpl();
|
|
377
375
|
/** One packed reverse index per relationship source component. */
|
|
378
376
|
private readonly relationshipIndexes = new Map<number, RelationshipIndex>();
|
|
379
377
|
private mutationEpoch = 0;
|
|
@@ -430,7 +428,6 @@ export class World {
|
|
|
430
428
|
getSchedules: () => this.schedules,
|
|
431
429
|
getSharedRefs: () => this.sharedRefs,
|
|
432
430
|
getStructureEpoch: this.getStructureEpoch.bind(this),
|
|
433
|
-
getStructuralEvidence: () => this.structuralEvidence,
|
|
434
431
|
lookupAlive: this.lookupAlive.bind(this),
|
|
435
432
|
markComponentChanged: this.internalmarkComponentChanged.bind(this),
|
|
436
433
|
markComponentRangeChanged: this.internalmarkComponentRangeChanged.bind(this),
|
|
@@ -512,10 +509,6 @@ export class World {
|
|
|
512
509
|
return worldScheduleUsesComponent(this, component);
|
|
513
510
|
}
|
|
514
511
|
|
|
515
|
-
private recordStructuralEvidence(evidence: StructuralEvidenceInput): void {
|
|
516
|
-
this.structuralEvidence.append(evidence);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
512
|
/** Resolve current logical identity for a packed entity handle. */
|
|
520
513
|
private internalgetEntityArchetype(entity: EntityHandle): Archetype | undefined {
|
|
521
514
|
const record = this.records[entityIndex(entity)];
|
|
@@ -557,7 +550,7 @@ export class World {
|
|
|
557
550
|
if (epochs === undefined) {
|
|
558
551
|
throw new Error(`Derived component ${componentId} is not in the table.`);
|
|
559
552
|
}
|
|
560
|
-
epochs
|
|
553
|
+
publishComponentRange(epochs, rowStart, rowCount, epoch);
|
|
561
554
|
this.componentMutationEpochs[componentId] = epoch;
|
|
562
555
|
}
|
|
563
556
|
|
|
@@ -606,7 +599,7 @@ export class World {
|
|
|
606
599
|
const epochs = table.storage.get(componentId)?.epochs;
|
|
607
600
|
if (epochs === undefined || rowCount === 0) return;
|
|
608
601
|
const epoch = this.internalnextMutationEpoch();
|
|
609
|
-
epochs
|
|
602
|
+
publishComponentRange(epochs, rowStart, rowCount, epoch);
|
|
610
603
|
this.componentMutationEpochs[componentId] = epoch;
|
|
611
604
|
}
|
|
612
605
|
|
|
@@ -1410,11 +1403,6 @@ export class World {
|
|
|
1410
1403
|
if (mirrorAdded) {
|
|
1411
1404
|
this.internalmarkComponentsAdded(target, [mirrorLocalId]);
|
|
1412
1405
|
this.advanceStructureEpoch();
|
|
1413
|
-
this.recordStructuralEvidence({
|
|
1414
|
-
kind: 'component-added',
|
|
1415
|
-
entity: target,
|
|
1416
|
-
componentId: mirrorLocalId,
|
|
1417
|
-
});
|
|
1418
1406
|
}
|
|
1419
1407
|
return ok(undefined);
|
|
1420
1408
|
}
|
|
@@ -1651,12 +1639,8 @@ export class World {
|
|
|
1651
1639
|
data: value,
|
|
1652
1640
|
});
|
|
1653
1641
|
if (!valuePreflight.ok) return valuePreflight;
|
|
1654
|
-
|
|
1655
|
-
const
|
|
1656
|
-
...currentValue,
|
|
1657
|
-
...(value as Record<string, unknown>),
|
|
1658
|
-
};
|
|
1659
|
-
const enumError = validateEnumFieldValues(component, mergedValue, entity as number);
|
|
1642
|
+
// Enum constraints are field-local; untouched fields are already validated.
|
|
1643
|
+
const enumError = validateEnumFieldValues(component, value, entity as number);
|
|
1660
1644
|
if (enumError !== null) return err(enumError as unknown as EcsError);
|
|
1661
1645
|
|
|
1662
1646
|
if (component.storage === 'sparse') {
|
|
@@ -2191,11 +2175,6 @@ export class World {
|
|
|
2191
2175
|
// components-added epoch/evidence record that claims the edge exists.
|
|
2192
2176
|
this.internalmarkComponentsAdded(entity, [componentId(componentData.component)]);
|
|
2193
2177
|
this.advanceStructureEpoch();
|
|
2194
|
-
this.recordStructuralEvidence({
|
|
2195
|
-
kind: 'component-added',
|
|
2196
|
-
entity,
|
|
2197
|
-
componentId: localId,
|
|
2198
|
-
});
|
|
2199
2178
|
}
|
|
2200
2179
|
return ok(undefined);
|
|
2201
2180
|
}
|
|
@@ -2308,12 +2287,8 @@ export class World {
|
|
|
2308
2287
|
this.migrateEntity(rec, srcArch, targetArch);
|
|
2309
2288
|
}
|
|
2310
2289
|
if (!internal) {
|
|
2290
|
+
this.internalnextMutationEpoch();
|
|
2311
2291
|
this.advanceStructureEpoch();
|
|
2312
|
-
this.recordStructuralEvidence({
|
|
2313
|
-
kind: 'component-removed',
|
|
2314
|
-
entity,
|
|
2315
|
-
componentId: localId,
|
|
2316
|
-
});
|
|
2317
2292
|
}
|
|
2318
2293
|
return ok(undefined);
|
|
2319
2294
|
}
|
|
@@ -2378,7 +2353,7 @@ export class World {
|
|
|
2378
2353
|
record.archetypeId = arch.id;
|
|
2379
2354
|
storageTouched = true;
|
|
2380
2355
|
const table = this.table(arch);
|
|
2381
|
-
const tableRow = appendTableRow(table, entity);
|
|
2356
|
+
const tableRow = appendTableRow(table, entity, this.mutationEpoch + 1);
|
|
2382
2357
|
const archetypeRow = appendArchetypeRow(arch, tableRow);
|
|
2383
2358
|
record.archetypeRow = archetypeRow;
|
|
2384
2359
|
|
|
@@ -2415,7 +2390,6 @@ export class World {
|
|
|
2415
2390
|
...componentDatas.map((cd) => componentId(cd.component)),
|
|
2416
2391
|
]);
|
|
2417
2392
|
this.advanceStructureEpoch();
|
|
2418
|
-
this.recordStructuralEvidence({ kind: 'spawn', entity });
|
|
2419
2393
|
return ok(undefined);
|
|
2420
2394
|
} catch (error) {
|
|
2421
2395
|
if (storageTouched) this.poisonAfterEntityMutation('World.materializeEntity', error);
|
|
@@ -3021,6 +2995,28 @@ export class World {
|
|
|
3021
2995
|
const isVariable = arrayMeta.length === undefined;
|
|
3022
2996
|
const fixedLength = arrayMeta.length ?? 0;
|
|
3023
2997
|
|
|
2998
|
+
// Same-typed inline storage can copy elements without packing temporary bytes.
|
|
2999
|
+
// TypedArray.set preserves overlapping source views; different element types
|
|
3000
|
+
// retain the byte-copy contract below.
|
|
3001
|
+
if (
|
|
3002
|
+
!isVariable &&
|
|
3003
|
+
metaKey !== 'shared' &&
|
|
3004
|
+
meta.viewCtor !== undefined &&
|
|
3005
|
+
col.view instanceof meta.viewCtor &&
|
|
3006
|
+
(raw == null || Array.isArray(raw) || raw instanceof meta.viewCtor)
|
|
3007
|
+
) {
|
|
3008
|
+
const start = row * col.arity;
|
|
3009
|
+
const count = raw == null ? 0 : Math.min(raw.length, col.arity);
|
|
3010
|
+
if (Array.isArray(raw)) {
|
|
3011
|
+
for (let i = 0; i < count; i++)
|
|
3012
|
+
col.view[start + i] = typeof raw[i] === 'number' ? raw[i] : 0;
|
|
3013
|
+
} else if (raw != null) {
|
|
3014
|
+
col.view.set(raw.length <= col.arity ? raw : raw.subarray(0, count), start);
|
|
3015
|
+
}
|
|
3016
|
+
col.view.fill(0, start + count, start + col.arity);
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3024
3020
|
// Determine the payload's logical element count. Accept any TypedArray
|
|
3025
3021
|
// (Float32Array / Uint32Array / etc.) plus plain numeric arrays; an
|
|
3026
3022
|
// undefined / missing payload is treated as a length-0 init. Bytes are
|
|
@@ -3230,7 +3226,7 @@ export class World {
|
|
|
3230
3226
|
const entity = (srcTable.storage.get(componentId(EntityComponent))?.fields.get('self')?.view[
|
|
3231
3227
|
oldTableRow
|
|
3232
3228
|
] ?? 0) as EntityHandle;
|
|
3233
|
-
const newTableRow = appendTableRow(targetTable, entity);
|
|
3229
|
+
const newTableRow = appendTableRow(targetTable, entity, this.mutationEpoch + 1);
|
|
3234
3230
|
const newArchetypeRow = appendArchetypeRow(targetArch, newTableRow);
|
|
3235
3231
|
|
|
3236
3232
|
// Copy shared component data.
|
|
@@ -3276,7 +3272,7 @@ export class World {
|
|
|
3276
3272
|
movedRecord.archetypeRow = archetypeSwap.newRow;
|
|
3277
3273
|
}
|
|
3278
3274
|
}
|
|
3279
|
-
const tableSwap = removeTableRow(srcTable, oldTableRow);
|
|
3275
|
+
const tableSwap = removeTableRow(srcTable, oldTableRow, this.mutationEpoch + 1);
|
|
3280
3276
|
if (tableSwap !== null) {
|
|
3281
3277
|
const movedRecord = this.records[entityIndex(tableSwap.movedEntity)];
|
|
3282
3278
|
if (movedRecord?.generation === entityGeneration(tableSwap.movedEntity)) {
|
|
@@ -3313,6 +3309,7 @@ export class World {
|
|
|
3313
3309
|
}
|
|
3314
3310
|
record.archetypeId = targetArch.id;
|
|
3315
3311
|
record.archetypeRow = appendArchetypeRow(targetArch, tableRow);
|
|
3312
|
+
markTableMembership(table, tableRow, this.mutationEpoch + 1);
|
|
3316
3313
|
}
|
|
3317
3314
|
|
|
3318
3315
|
/**
|
|
@@ -3366,7 +3363,7 @@ export class World {
|
|
|
3366
3363
|
movedRecord.archetypeRow = archetypeSwap.newRow;
|
|
3367
3364
|
}
|
|
3368
3365
|
}
|
|
3369
|
-
const tableSwap = removeTableRow(table, tableRow);
|
|
3366
|
+
const tableSwap = removeTableRow(table, tableRow, this.mutationEpoch + 1);
|
|
3370
3367
|
if (tableSwap !== null) {
|
|
3371
3368
|
const movedRecord = this.records[entityIndex(tableSwap.movedEntity)];
|
|
3372
3369
|
if (movedRecord?.generation === entityGeneration(tableSwap.movedEntity)) {
|
|
@@ -3377,7 +3374,6 @@ export class World {
|
|
|
3377
3374
|
}
|
|
3378
3375
|
}
|
|
3379
3376
|
}
|
|
3380
|
-
this.recordStructuralEvidence({ kind: 'despawn', entity });
|
|
3381
3377
|
record.archetypeId = -1;
|
|
3382
3378
|
record.archetypeRow = -1;
|
|
3383
3379
|
record.generation += 1;
|
|
@@ -3386,6 +3382,7 @@ export class World {
|
|
|
3386
3382
|
const childResult = this.despawnEntity(child, true);
|
|
3387
3383
|
if (!childResult.ok) return childResult;
|
|
3388
3384
|
}
|
|
3385
|
+
this.internalnextMutationEpoch();
|
|
3389
3386
|
this.advanceStructureEpoch();
|
|
3390
3387
|
return ok(undefined);
|
|
3391
3388
|
} catch (error) {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"structural-evidence.contract.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/structural-evidence.contract.test-d.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"structural-evidence.contract.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/structural-evidence.contract.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { EntityHandle } from '../entity-handle';
|
|
2
|
-
export type StructuralEvidenceKind = 'spawn' | 'despawn' | 'component-added' | 'component-removed';
|
|
3
|
-
export interface StructuralEvidenceInput {
|
|
4
|
-
readonly kind: StructuralEvidenceKind;
|
|
5
|
-
readonly entity: EntityHandle;
|
|
6
|
-
readonly componentId?: number;
|
|
7
|
-
}
|
|
8
|
-
export interface StructuralEvidence extends StructuralEvidenceInput {
|
|
9
|
-
readonly sequence: number;
|
|
10
|
-
}
|
|
11
|
-
export type StructuralEvidenceRead = {
|
|
12
|
-
readonly status: 'ok';
|
|
13
|
-
readonly cursor: number;
|
|
14
|
-
readonly events: readonly StructuralEvidence[];
|
|
15
|
-
} | {
|
|
16
|
-
readonly status: 'overflow';
|
|
17
|
-
readonly cursor: number;
|
|
18
|
-
readonly oldestAvailable: number;
|
|
19
|
-
};
|
|
20
|
-
/** Bounded producer-owned structural evidence; consumers never infer facts by scanning. */
|
|
21
|
-
export declare class StructuralEvidenceRing {
|
|
22
|
-
readonly capacity: number;
|
|
23
|
-
private readonly events;
|
|
24
|
-
private nextSequence;
|
|
25
|
-
constructor(capacity?: number);
|
|
26
|
-
get cursor(): number;
|
|
27
|
-
append(input: StructuralEvidenceInput): number;
|
|
28
|
-
readAfter(cursor: number): StructuralEvidenceRead;
|
|
29
|
-
}
|
|
30
|
-
//# sourceMappingURL=structural-evidence.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"structural-evidence.d.ts","sourceRoot":"","sources":["../../src/storage/structural-evidence.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,MAAM,sBAAsB,GAAG,OAAO,GAAG,SAAS,GAAG,iBAAiB,GAAG,mBAAmB,CAAC;AAEnG,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,kBAAmB,SAAQ,uBAAuB;IACjE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,MAAM,sBAAsB,GAC9B;IACE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,SAAS,kBAAkB,EAAE,CAAC;CAChD,GACD;IAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/F,2FAA2F;AAC3F,qBAAa,sBAAsB;IAIrB,QAAQ,CAAC,QAAQ;IAH7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwC;IAC/D,OAAO,CAAC,YAAY,CAAK;gBAEJ,QAAQ,SAAO;IAOpC,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,MAAM,CAAC,KAAK,EAAE,uBAAuB,GAAG,MAAM;IAO9C,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,sBAAsB;CAkBlD"}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { StructuralEvidence, StructuralEvidenceKind } from '../storage/structural-evidence';
|
|
2
|
-
|
|
3
|
-
const kind: StructuralEvidenceKind = 'component-added';
|
|
4
|
-
const evidence: StructuralEvidence = { sequence: 1, kind, entity: 0 as never, componentId: 4 };
|
|
5
|
-
// @ts-expect-error evidence sequence is producer-owned.
|
|
6
|
-
evidence.sequence = 2;
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { encodeEntity } from '../entity-handle';
|
|
3
|
-
import { StructuralEvidenceRing } from '../storage/structural-evidence';
|
|
4
|
-
|
|
5
|
-
describe('typed structural evidence ring', () => {
|
|
6
|
-
it('records lifecycle, component membership, packed identity, and sequence facts', () => {
|
|
7
|
-
const ring = new StructuralEvidenceRing(8);
|
|
8
|
-
const entity = encodeEntity(4, 2);
|
|
9
|
-
const cursor = ring.cursor;
|
|
10
|
-
ring.append({ kind: 'spawn', entity });
|
|
11
|
-
ring.append({ kind: 'component-added', entity, componentId: 7 });
|
|
12
|
-
ring.append({ kind: 'component-removed', entity, componentId: 7 });
|
|
13
|
-
ring.append({ kind: 'despawn', entity });
|
|
14
|
-
const read = ring.readAfter(cursor);
|
|
15
|
-
expect(read.status).toBe('ok');
|
|
16
|
-
if (read.status !== 'ok') return;
|
|
17
|
-
expect(
|
|
18
|
-
read.events.map(({ kind, entity: packed, sequence }) => ({ kind, packed, sequence })),
|
|
19
|
-
).toEqual([
|
|
20
|
-
{ kind: 'spawn', packed: entity, sequence: 1 },
|
|
21
|
-
{ kind: 'component-added', packed: entity, sequence: 2 },
|
|
22
|
-
{ kind: 'component-removed', packed: entity, sequence: 3 },
|
|
23
|
-
{ kind: 'despawn', packed: entity, sequence: 4 },
|
|
24
|
-
]);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('fails closed on overflow and exposes the cursor for consumer reconciliation', () => {
|
|
28
|
-
const ring = new StructuralEvidenceRing(2);
|
|
29
|
-
const cursor = ring.cursor;
|
|
30
|
-
const entity = encodeEntity(1, 4);
|
|
31
|
-
const reused = encodeEntity(1, 5);
|
|
32
|
-
ring.append({ kind: 'spawn', entity });
|
|
33
|
-
ring.append({ kind: 'despawn', entity });
|
|
34
|
-
ring.append({ kind: 'spawn', entity: reused });
|
|
35
|
-
expect(ring.readAfter(cursor)).toMatchObject({
|
|
36
|
-
status: 'overflow',
|
|
37
|
-
cursor: 3,
|
|
38
|
-
oldestAvailable: 2,
|
|
39
|
-
});
|
|
40
|
-
expect(ring.cursor).toBe(3);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('rejects cursors outside the producer-owned sequence range', () => {
|
|
44
|
-
const ring = new StructuralEvidenceRing(4);
|
|
45
|
-
const entity = encodeEntity(3, 1);
|
|
46
|
-
ring.append({ kind: 'spawn', entity });
|
|
47
|
-
expect(() => ring.readAfter(2)).toThrow(RangeError);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import type { EntityHandle } from '../entity-handle';
|
|
2
|
-
|
|
3
|
-
export type StructuralEvidenceKind = 'spawn' | 'despawn' | 'component-added' | 'component-removed';
|
|
4
|
-
|
|
5
|
-
export interface StructuralEvidenceInput {
|
|
6
|
-
readonly kind: StructuralEvidenceKind;
|
|
7
|
-
readonly entity: EntityHandle;
|
|
8
|
-
readonly componentId?: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface StructuralEvidence extends StructuralEvidenceInput {
|
|
12
|
-
readonly sequence: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export type StructuralEvidenceRead =
|
|
16
|
-
| {
|
|
17
|
-
readonly status: 'ok';
|
|
18
|
-
readonly cursor: number;
|
|
19
|
-
readonly events: readonly StructuralEvidence[];
|
|
20
|
-
}
|
|
21
|
-
| { readonly status: 'overflow'; readonly cursor: number; readonly oldestAvailable: number };
|
|
22
|
-
|
|
23
|
-
/** Bounded producer-owned structural evidence; consumers never infer facts by scanning. */
|
|
24
|
-
export class StructuralEvidenceRing {
|
|
25
|
-
private readonly events: Array<StructuralEvidence | undefined>;
|
|
26
|
-
private nextSequence = 1;
|
|
27
|
-
|
|
28
|
-
constructor(readonly capacity = 1024) {
|
|
29
|
-
if (!Number.isSafeInteger(capacity) || capacity <= 0) {
|
|
30
|
-
throw new RangeError('StructuralEvidenceRing capacity must be a positive safe integer');
|
|
31
|
-
}
|
|
32
|
-
this.events = new Array<StructuralEvidence | undefined>(capacity);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
get cursor(): number {
|
|
36
|
-
return this.nextSequence - 1;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
append(input: StructuralEvidenceInput): number {
|
|
40
|
-
const sequence = this.nextSequence;
|
|
41
|
-
this.nextSequence += 1;
|
|
42
|
-
this.events[(sequence - 1) % this.capacity] = { ...input, sequence };
|
|
43
|
-
return sequence;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
readAfter(cursor: number): StructuralEvidenceRead {
|
|
47
|
-
const latest = this.cursor;
|
|
48
|
-
if (!Number.isSafeInteger(cursor) || cursor < 0 || cursor > latest) {
|
|
49
|
-
throw new RangeError(`StructuralEvidenceRing cursor ${cursor} is outside 0..${latest}`);
|
|
50
|
-
}
|
|
51
|
-
const oldestAvailable = Math.max(1, latest - this.capacity + 1);
|
|
52
|
-
if (cursor < oldestAvailable - 1)
|
|
53
|
-
return { status: 'overflow', cursor: latest, oldestAvailable };
|
|
54
|
-
const events: StructuralEvidence[] = [];
|
|
55
|
-
for (let sequence = cursor + 1; sequence <= latest; sequence += 1) {
|
|
56
|
-
const event = this.events[(sequence - 1) % this.capacity];
|
|
57
|
-
if (event === undefined || event.sequence !== sequence) {
|
|
58
|
-
return { status: 'overflow', cursor: latest, oldestAvailable };
|
|
59
|
-
}
|
|
60
|
-
events.push(event);
|
|
61
|
-
}
|
|
62
|
-
return { status: 'ok', cursor: latest, events };
|
|
63
|
-
}
|
|
64
|
-
}
|