@forgeax/engine-ecs 0.1.20 → 0.1.23
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 +50 -16
- package/dist/__tests__/changed-block-summary.candidate.test.d.ts +2 -0
- package/dist/__tests__/changed-block-summary.candidate.test.d.ts.map +1 -0
- package/dist/__tests__/component-requirements.unit.test.d.ts +2 -0
- package/dist/__tests__/component-requirements.unit.test.d.ts.map +1 -0
- package/dist/__tests__/component-version-surface.test.d.ts +2 -0
- package/dist/__tests__/component-version-surface.test.d.ts.map +1 -0
- package/dist/__tests__/derived-range-writer.contract.test-d.d.ts +2 -0
- package/dist/__tests__/derived-range-writer.contract.test-d.d.ts.map +1 -0
- package/dist/__tests__/derived-range-writer.contract.test.d.ts +2 -0
- package/dist/__tests__/derived-range-writer.contract.test.d.ts.map +1 -0
- package/dist/__tests__/structural-evidence.contract.test-d.d.ts +2 -0
- package/dist/__tests__/structural-evidence.contract.test-d.d.ts.map +1 -0
- package/dist/__tests__/structural-evidence.contract.test.d.ts +2 -0
- package/dist/__tests__/structural-evidence.contract.test.d.ts.map +1 -0
- package/dist/component-schema.d.ts +17 -0
- package/dist/component-schema.d.ts.map +1 -1
- package/dist/component.d.ts +7 -1
- package/dist/component.d.ts.map +1 -1
- package/dist/errors/query-and-component-errors.d.ts +1 -1
- package/dist/errors/query-and-component-errors.d.ts.map +1 -1
- package/dist/errors/validation-errors.d.ts +24 -0
- package/dist/errors/validation-errors.d.ts.map +1 -1
- package/dist/errors.d.ts +14 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/externalization/index.mjs.map +1 -1
- package/dist/index.mjs +415 -128
- package/dist/index.mjs.map +1 -1
- package/dist/internal.d.ts +16 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.mjs +15 -2
- package/dist/internal.mjs.map +1 -1
- package/dist/projection/index.d.ts +27 -52
- package/dist/projection/index.d.ts.map +1 -1
- package/dist/projection/index.mjs +66 -76
- package/dist/projection/index.mjs.map +1 -1
- package/dist/query/derived-range-writer.d.ts +48 -0
- package/dist/query/derived-range-writer.d.ts.map +1 -0
- package/dist/query/query.d.ts +1 -1
- package/dist/query/query.d.ts.map +1 -1
- package/dist/relationship-index.d.ts +8 -0
- package/dist/relationship-index.d.ts.map +1 -1
- package/dist/shared-ref-store.d.ts +5 -9
- package/dist/shared-ref-store.d.ts.map +1 -1
- package/dist/shared.mjs.map +1 -1
- package/dist/storage/change-detection.d.ts +1 -3
- package/dist/storage/change-detection.d.ts.map +1 -1
- package/dist/storage/structural-evidence.d.ts +30 -0
- package/dist/storage/structural-evidence.d.ts.map +1 -0
- package/dist/world-component-access.d.ts +4 -3
- package/dist/world-component-access.d.ts.map +1 -1
- package/dist/world-core.d.ts +2 -2
- package/dist/world-core.d.ts.map +1 -1
- package/dist/world-entity-lifecycle.d.ts.map +1 -1
- package/dist/world-internal.d.ts +1 -1
- package/dist/world-internal.d.ts.map +1 -1
- package/dist/world.d.ts +10 -14
- package/dist/world.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/changed-block-summary.candidate.test.ts +57 -0
- package/src/__tests__/component-requirements.unit.test.ts +76 -0
- package/src/__tests__/component-version-surface.test.ts +45 -0
- package/src/__tests__/derived-range-writer.contract.test-d.ts +35 -0
- package/src/__tests__/derived-range-writer.contract.test.ts +339 -0
- package/src/__tests__/errors.unit.test.ts +21 -5
- package/src/__tests__/externalization-render-read-lease.test-d.ts +1 -1
- package/src/__tests__/externalization-render-read-lease.unit.test.ts +15 -15
- package/src/__tests__/query-storage-trends.perf.test.ts +19 -0
- package/src/__tests__/query.unit.test.ts +10 -6
- package/src/__tests__/shared-ref-store.unit.test.ts +3 -8
- package/src/__tests__/structural-evidence.contract.test-d.ts +6 -0
- package/src/__tests__/structural-evidence.contract.test.ts +49 -0
- package/src/component-schema.ts +51 -0
- package/src/component.ts +12 -2
- package/src/errors/query-and-component-errors.ts +1 -1
- package/src/errors/validation-errors.ts +30 -0
- package/src/errors.ts +29 -5
- package/src/internal.ts +56 -0
- package/src/projection/index.ts +68 -149
- package/src/query/derived-range-writer.ts +332 -0
- package/src/query/query.ts +89 -15
- package/src/relationship-index.ts +13 -1
- package/src/shared-ref-store.ts +16 -24
- package/src/storage/change-detection.ts +0 -15
- package/src/storage/structural-evidence.ts +64 -0
- package/src/world-component-access.ts +53 -5
- package/src/world-core.ts +2 -2
- package/src/world-entity-lifecycle.ts +10 -1
- package/src/world-internal.ts +5 -5
- package/src/world.ts +45 -50
- package/dist/.tsbuildinfo +0 -1
- package/dist/__tests__/world-change-journal.unit.test.d.ts +0 -2
- package/dist/__tests__/world-change-journal.unit.test.d.ts.map +0 -1
- package/dist/__tests__/world-internal-identity.unit.test.d.ts +0 -2
- package/dist/__tests__/world-internal-identity.unit.test.d.ts.map +0 -1
- package/dist/world-change-journal.d.ts +0 -34
- package/dist/world-change-journal.d.ts.map +0 -1
- package/src/__tests__/world-change-journal.unit.test.ts +0 -97
- package/src/__tests__/world-internal-identity.unit.test.ts +0 -17
- package/src/world-change-journal.ts +0 -84
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
}
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
TYPE_METADATA,
|
|
23
23
|
} from './component';
|
|
24
24
|
import { fillComponentDefaults, validateComponentDataKeys } from './component-default-fallback';
|
|
25
|
-
import { componentDefinition } from './component-schema';
|
|
25
|
+
import { componentDefinition, expandComponentRequirements } from './component-schema';
|
|
26
26
|
import { validateManagedArrayValues, validateSharedFieldValues } from './component-value-validate';
|
|
27
27
|
import { Entity as EntityComponent } from './entity';
|
|
28
28
|
import {
|
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
} from './storage/archetype-graph';
|
|
64
64
|
import { removeSparseTag } from './storage/change-detection';
|
|
65
65
|
import { arrayCountColumnName, type FieldView, normalizeBufferWrite } from './storage/column';
|
|
66
|
+
import type { StructuralEvidenceInput } from './storage/structural-evidence';
|
|
66
67
|
import { appendTableRow, type Table } from './storage/table';
|
|
67
68
|
import type { UniqueRefStore } from './unique-ref-store';
|
|
68
69
|
import type { ComponentData, EcsError, EntityRecord } from './world';
|
|
@@ -106,8 +107,8 @@ export interface ComponentAccessState {
|
|
|
106
107
|
readonly markComponentAdded: (entity: EntityHandle, componentId: number) => void;
|
|
107
108
|
readonly markComponentsAdded: (entity: EntityHandle, componentIds: readonly number[]) => void;
|
|
108
109
|
readonly markComponentChanged: (entity: EntityHandle, componentId: number) => void;
|
|
109
|
-
readonly removeComponentChange: (entity: EntityHandle, componentId: number) => void;
|
|
110
110
|
readonly markStructureChanged: () => void;
|
|
111
|
+
readonly recordStructuralEvidence: (evidence: StructuralEvidenceInput) => void;
|
|
111
112
|
routeError(err: unknown, ctx: ErrorContext): void;
|
|
112
113
|
}
|
|
113
114
|
|
|
@@ -568,7 +569,7 @@ export class WorldComponentAccess {
|
|
|
568
569
|
* field does not exist, or the field is not an `array<...>` column.
|
|
569
570
|
*
|
|
570
571
|
* @internal Engine-internal fast path; AI users read the typed view through
|
|
571
|
-
* `world.get(e,
|
|
572
|
+
* `world.get(e, GlobalTransform).world`. The accessor is the zero-materialization
|
|
572
573
|
* route the propagate kernel and render walk use.
|
|
573
574
|
*/
|
|
574
575
|
_getArrayView(
|
|
@@ -1011,12 +1012,13 @@ export class WorldComponentAccess {
|
|
|
1011
1012
|
entity: EntityHandle,
|
|
1012
1013
|
componentData: ComponentData<S>,
|
|
1013
1014
|
internal: boolean,
|
|
1015
|
+
resolveRequirements = true,
|
|
1014
1016
|
): Result<void, EcsError> {
|
|
1015
1017
|
const record = this.lookupAlive(entity, 'addComponent', componentData.component.name);
|
|
1016
1018
|
if (!record.ok) return record;
|
|
1017
1019
|
|
|
1018
1020
|
const rec = record.value;
|
|
1019
|
-
|
|
1021
|
+
let srcArch = this.graph.archetypes[rec.archetypeId];
|
|
1020
1022
|
/* istanbul ignore next -- defensive: alive record always has valid archetypeId */
|
|
1021
1023
|
if (!srcArch) {
|
|
1022
1024
|
return err(
|
|
@@ -1066,6 +1068,38 @@ export class WorldComponentAccess {
|
|
|
1066
1068
|
return err(enumErr as unknown as EcsError);
|
|
1067
1069
|
}
|
|
1068
1070
|
|
|
1071
|
+
// Generic component requirements are resolved once at the structural
|
|
1072
|
+
// boundary. Explicit data remains authoritative; only missing required
|
|
1073
|
+
// identities are added before the requested component is migrated.
|
|
1074
|
+
if (resolveRequirements) {
|
|
1075
|
+
const required = expandComponentRequirements([componentData]).slice(1);
|
|
1076
|
+
for (const requirement of required) {
|
|
1077
|
+
if (
|
|
1078
|
+
srcArch.components.some(
|
|
1079
|
+
(candidate) => componentId(candidate) === componentId(requirement.component),
|
|
1080
|
+
)
|
|
1081
|
+
) {
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
// The closure is expanded once above. Bypass requirement expansion for
|
|
1085
|
+
// each member so malformed dependency cycles remain finite and the
|
|
1086
|
+
// structural work still happens in one deterministic sequence.
|
|
1087
|
+
const added = this._addComponentCore(entity, requirement as ComponentData, internal, false);
|
|
1088
|
+
if (!added.ok) return added;
|
|
1089
|
+
srcArch = this.graph.archetypes[rec.archetypeId];
|
|
1090
|
+
if (!srcArch) {
|
|
1091
|
+
return err(
|
|
1092
|
+
new StaleEntityError(entity as number, entityIndex(entity), entityGeneration(entity), {
|
|
1093
|
+
operation: 'addComponent',
|
|
1094
|
+
component: componentData.component.name,
|
|
1095
|
+
expectedGeneration: rec.generation,
|
|
1096
|
+
actualGeneration: rec.generation,
|
|
1097
|
+
}),
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1069
1103
|
// Check if entity already has this component (using World-local ID).
|
|
1070
1104
|
const localId = componentId(componentData.component);
|
|
1071
1105
|
if (srcArch.components.some((candidate) => componentId(candidate) === localId)) {
|
|
@@ -1144,6 +1178,11 @@ export class WorldComponentAccess {
|
|
|
1144
1178
|
}
|
|
1145
1179
|
|
|
1146
1180
|
this.markStructureChanged();
|
|
1181
|
+
this.state.recordStructuralEvidence({
|
|
1182
|
+
kind: 'component-added',
|
|
1183
|
+
entity,
|
|
1184
|
+
componentId: localId,
|
|
1185
|
+
});
|
|
1147
1186
|
return ok(undefined);
|
|
1148
1187
|
}
|
|
1149
1188
|
|
|
@@ -1249,8 +1288,12 @@ export class WorldComponentAccess {
|
|
|
1249
1288
|
} else {
|
|
1250
1289
|
this.storage.migrateEntity(rec, srcArch, targetArch);
|
|
1251
1290
|
}
|
|
1252
|
-
this.state.removeComponentChange(entity, componentId(component));
|
|
1253
1291
|
this.markStructureChanged();
|
|
1292
|
+
this.state.recordStructuralEvidence({
|
|
1293
|
+
kind: 'component-removed',
|
|
1294
|
+
entity,
|
|
1295
|
+
componentId: localId,
|
|
1296
|
+
});
|
|
1254
1297
|
return ok(undefined);
|
|
1255
1298
|
}
|
|
1256
1299
|
|
|
@@ -1292,6 +1335,7 @@ export class WorldComponentAccess {
|
|
|
1292
1335
|
entity: EntityHandle,
|
|
1293
1336
|
componentDatas: ComponentData[],
|
|
1294
1337
|
): Result<void, EcsError> {
|
|
1338
|
+
componentDatas = expandComponentRequirements(componentDatas);
|
|
1295
1339
|
const slot = entityIndex(entity);
|
|
1296
1340
|
const record = this.records[slot];
|
|
1297
1341
|
if (!record || record.archetypeId !== -1) return ok(undefined);
|
|
@@ -1341,6 +1385,10 @@ export class WorldComponentAccess {
|
|
|
1341
1385
|
}
|
|
1342
1386
|
}
|
|
1343
1387
|
this.markStructureChanged();
|
|
1388
|
+
this.state.recordStructuralEvidence({
|
|
1389
|
+
kind: 'spawn',
|
|
1390
|
+
entity,
|
|
1391
|
+
});
|
|
1344
1392
|
return ok(undefined);
|
|
1345
1393
|
}
|
|
1346
1394
|
|
package/src/world-core.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { RelationshipIndex } from './relationship-index';
|
|
|
4
4
|
import type { SharedRefStore } from './shared-ref-store';
|
|
5
5
|
import { SharedRefStore as SharedRefStoreImpl } from './shared-ref-store';
|
|
6
6
|
import { type ArchetypeGraph, createArchetypeGraph } from './storage/archetype-graph';
|
|
7
|
-
import {
|
|
7
|
+
import { StructuralEvidenceRing } from './storage/structural-evidence';
|
|
8
8
|
import type { UniqueRefStore } from './unique-ref-store';
|
|
9
9
|
import { UniqueRefStore as UniqueRefStoreImpl } from './unique-ref-store';
|
|
10
10
|
import type { EntityRecord } from './world';
|
|
@@ -43,7 +43,7 @@ export class WorldCore {
|
|
|
43
43
|
readonly sharedRefs: SharedRefStore = new SharedRefStoreImpl();
|
|
44
44
|
readonly storage: WorldStorageCore;
|
|
45
45
|
readonly componentMutationEpochs: number[] = [];
|
|
46
|
-
readonly
|
|
46
|
+
readonly structuralEvidence = new StructuralEvidenceRing();
|
|
47
47
|
/** One packed reverse index per relationship source component. */
|
|
48
48
|
readonly relationshipIndexes = new Map<number, RelationshipIndex>();
|
|
49
49
|
mutationEpoch = 0;
|
|
@@ -8,6 +8,7 @@ import { err, isRetiredSlot, ok, pack, type Result } from '@forgeax/engine-types
|
|
|
8
8
|
import type { Component, ComponentSchema, InputShapeOf, ShapeOf } from './component';
|
|
9
9
|
import { componentId, componentSchema } from './component';
|
|
10
10
|
import { fillComponentDefaults, validateComponentDataKeys } from './component-default-fallback';
|
|
11
|
+
import { expandComponentRequirements } from './component-schema';
|
|
11
12
|
import { validateManagedArrayValues, validateSharedFieldValues } from './component-value-validate';
|
|
12
13
|
import { Entity } from './entity';
|
|
13
14
|
import {
|
|
@@ -48,6 +49,7 @@ export function spawnCore(
|
|
|
48
49
|
componentDatas: { component: Component; data: Partial<Record<string, unknown>> }[],
|
|
49
50
|
internal: boolean,
|
|
50
51
|
): Result<EntityHandle, EcsError> {
|
|
52
|
+
componentDatas = expandComponentRequirements(componentDatas);
|
|
51
53
|
const filledData: Record<string, unknown>[] = [];
|
|
52
54
|
for (const cd of componentDatas) {
|
|
53
55
|
const preflight = world[worldInternal].preflightComponentData(null, cd);
|
|
@@ -104,6 +106,10 @@ export function spawnCore(
|
|
|
104
106
|
}
|
|
105
107
|
}
|
|
106
108
|
world[worldInternal].markStructureChanged();
|
|
109
|
+
world[worldInternal].recordStructuralEvidence({
|
|
110
|
+
kind: 'spawn',
|
|
111
|
+
entity: spawnedEntity,
|
|
112
|
+
});
|
|
107
113
|
return ok(spawnedEntity);
|
|
108
114
|
}
|
|
109
115
|
|
|
@@ -169,8 +175,11 @@ export function despawnCore(
|
|
|
169
175
|
}
|
|
170
176
|
}
|
|
171
177
|
}
|
|
178
|
+
world[worldInternal].recordStructuralEvidence({
|
|
179
|
+
kind: 'despawn',
|
|
180
|
+
entity,
|
|
181
|
+
});
|
|
172
182
|
if (record) {
|
|
173
|
-
world[worldInternal].removeEntityChanges(entity);
|
|
174
183
|
record.archetypeId = -1;
|
|
175
184
|
record.archetypeRow = -1;
|
|
176
185
|
record.generation += 1;
|
package/src/world-internal.ts
CHANGED
|
@@ -23,10 +23,10 @@ type InternalName =
|
|
|
23
23
|
| 'despawnCore'
|
|
24
24
|
| 'getArrayView'
|
|
25
25
|
| 'getBufferPool'
|
|
26
|
-
| 'getChangeCursor'
|
|
27
26
|
| 'getClockWriter'
|
|
28
27
|
| 'getComponentChange'
|
|
29
28
|
| 'getComponentMutationEpoch'
|
|
29
|
+
| 'getComponentMutationEpochs'
|
|
30
30
|
| 'getEntityArchetype'
|
|
31
31
|
| 'getFixedAccumulator'
|
|
32
32
|
| 'getFreeIndices'
|
|
@@ -41,28 +41,28 @@ type InternalName =
|
|
|
41
41
|
| 'getSchedules'
|
|
42
42
|
| 'getSharedRefs'
|
|
43
43
|
| 'getStructureEpoch'
|
|
44
|
+
| 'getStructuralEvidence'
|
|
44
45
|
| 'getUniqueRefs'
|
|
45
46
|
| 'lookupAlive'
|
|
46
47
|
| 'markComponentAdded'
|
|
47
48
|
| 'markComponentChanged'
|
|
48
49
|
| 'markComponentRangeChanged'
|
|
49
50
|
| 'markComponentsAdded'
|
|
50
|
-
| 'markDerivedComponentChanges'
|
|
51
51
|
| 'markStructureChanged'
|
|
52
52
|
| 'materializePendingEntity'
|
|
53
53
|
| 'nextMutationEpoch'
|
|
54
54
|
| 'poisonExecution'
|
|
55
|
+
| 'publishDerivedRange'
|
|
55
56
|
| 'preflightComponentData'
|
|
56
|
-
| 'readChangesSince'
|
|
57
57
|
| 'readRow'
|
|
58
|
+
| 'recordStructuralEvidence'
|
|
58
59
|
| 'recordIsLive'
|
|
59
60
|
| 'relationshipOnInsert'
|
|
60
61
|
| 'relationshipOnRemove'
|
|
61
62
|
| 'releaseManagedRefsOnRow'
|
|
62
|
-
| 'removeComponentChange'
|
|
63
63
|
| 'removeComponentCore'
|
|
64
|
-
| 'removeEntityChanges'
|
|
65
64
|
| 'routeError'
|
|
65
|
+
| 'restoreMutationEpoch'
|
|
66
66
|
| 'setFixedAccumulator'
|
|
67
67
|
| 'setQueryRow'
|
|
68
68
|
| 'spawnCore'
|
package/src/world.ts
CHANGED
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
ComponentNotDefinedError,
|
|
29
29
|
ComponentNotPresentError,
|
|
30
30
|
ComponentNumericValueInvalidError,
|
|
31
|
+
DerivedRangeOutOfBoundsError,
|
|
31
32
|
FixedSizeMismatchError,
|
|
32
33
|
ManagedArrayInvalidValueError,
|
|
33
34
|
ManagedBufferOutOfBoundsError,
|
|
@@ -71,9 +72,11 @@ import {
|
|
|
71
72
|
markComponentChanged,
|
|
72
73
|
markComponentsAdded,
|
|
73
74
|
readComponentChange,
|
|
74
|
-
readTableEntityRange,
|
|
75
|
-
type WorldChangeRead,
|
|
76
75
|
} from './storage/change-detection';
|
|
76
|
+
import type {
|
|
77
|
+
StructuralEvidenceInput,
|
|
78
|
+
StructuralEvidenceRing,
|
|
79
|
+
} from './storage/structural-evidence';
|
|
77
80
|
import {
|
|
78
81
|
type ClockWriter,
|
|
79
82
|
createWorldClock,
|
|
@@ -144,6 +147,8 @@ export type EcsError =
|
|
|
144
147
|
| ScheduleScopeMismatchError
|
|
145
148
|
| SharedKernelEligibilityError
|
|
146
149
|
| SharedKernelFailureError
|
|
150
|
+
| DerivedRangeOutOfBoundsError
|
|
151
|
+
| ChangeEpochExhaustedError
|
|
147
152
|
| WorldPoisonedError;
|
|
148
153
|
|
|
149
154
|
/** Component data for spawn/addComponent: component token + initial values.
|
|
@@ -324,9 +329,6 @@ export class World {
|
|
|
324
329
|
private get componentMutationEpochs() {
|
|
325
330
|
return this.core.componentMutationEpochs;
|
|
326
331
|
}
|
|
327
|
-
private get changeJournal() {
|
|
328
|
-
return this.core.changeJournal;
|
|
329
|
-
}
|
|
330
332
|
/** Free index slots (LIFO stack). */
|
|
331
333
|
private get records() {
|
|
332
334
|
return this.core.records;
|
|
@@ -409,9 +411,8 @@ export class World {
|
|
|
409
411
|
this.internalmarkComponentsAdded(entity, components),
|
|
410
412
|
markComponentChanged: (entity, component) =>
|
|
411
413
|
this.internalmarkComponentChanged(entity, component),
|
|
412
|
-
removeComponentChange: (entity, component) =>
|
|
413
|
-
this.internalremoveComponentChange(entity, component),
|
|
414
414
|
markStructureChanged: () => this.internalmarkStructureChanged(),
|
|
415
|
+
recordStructuralEvidence: (evidence) => this.internalrecordStructuralEvidence(evidence),
|
|
415
416
|
routeError: (error, context) => this.internalrouteError(error as EcsError, context),
|
|
416
417
|
});
|
|
417
418
|
this.clock = createWorldClock({ ...DEFAULT_TIME_POLICY, ...options.time });
|
|
@@ -433,10 +434,10 @@ export class World {
|
|
|
433
434
|
despawnCore: this.internaldespawnCore.bind(this),
|
|
434
435
|
getArrayView: this.internalgetArrayView.bind(this),
|
|
435
436
|
getBufferPool: this.internalgetBufferPool.bind(this),
|
|
436
|
-
getChangeCursor: this.internalgetChangeCursor.bind(this),
|
|
437
437
|
getClockWriter: this.internalgetClockWriter.bind(this),
|
|
438
438
|
getComponentChange: this.internalgetComponentChange.bind(this),
|
|
439
439
|
getComponentMutationEpoch: this.internalgetComponentMutationEpoch.bind(this),
|
|
440
|
+
getComponentMutationEpochs: this.internalgetComponentMutationEpochs.bind(this),
|
|
440
441
|
getEntityArchetype: this.internalgetEntityArchetype.bind(this),
|
|
441
442
|
getFixedAccumulator: this.internalgetFixedAccumulator.bind(this),
|
|
442
443
|
getFreeIndices: this.internalgetFreeIndices.bind(this),
|
|
@@ -451,28 +452,28 @@ export class World {
|
|
|
451
452
|
getSchedules: this.internalgetSchedules.bind(this),
|
|
452
453
|
getSharedRefs: this.internalgetSharedRefs.bind(this),
|
|
453
454
|
getStructureEpoch: this.internalgetStructureEpoch.bind(this),
|
|
455
|
+
getStructuralEvidence: this.internalgetStructuralEvidence.bind(this),
|
|
454
456
|
getUniqueRefs: this.internalgetUniqueRefs.bind(this),
|
|
455
457
|
lookupAlive: this.internallookupAlive.bind(this),
|
|
456
458
|
markComponentAdded: this.internalmarkComponentAdded.bind(this),
|
|
457
459
|
markComponentChanged: this.internalmarkComponentChanged.bind(this),
|
|
458
460
|
markComponentRangeChanged: this.internalmarkComponentRangeChanged.bind(this),
|
|
459
461
|
markComponentsAdded: this.internalmarkComponentsAdded.bind(this),
|
|
460
|
-
markDerivedComponentChanges: this.internalmarkDerivedComponentChanges.bind(this),
|
|
461
462
|
markStructureChanged: this.internalmarkStructureChanged.bind(this),
|
|
462
463
|
materializePendingEntity: this.internalmaterializePendingEntity.bind(this),
|
|
463
464
|
nextMutationEpoch: this.internalnextMutationEpoch.bind(this),
|
|
464
465
|
poisonExecution: this.internalpoisonExecution.bind(this),
|
|
466
|
+
publishDerivedRange: this.internalpublishDerivedRange.bind(this),
|
|
465
467
|
preflightComponentData: this.internalpreflightComponentData.bind(this),
|
|
466
|
-
readChangesSince: this.internalreadChangesSince.bind(this),
|
|
467
468
|
readRow: this.internalreadRow.bind(this),
|
|
469
|
+
recordStructuralEvidence: this.internalrecordStructuralEvidence.bind(this),
|
|
468
470
|
recordIsLive: this.internalrecordIsLive.bind(this),
|
|
469
471
|
relationshipOnInsert: this.internalrelationshipOnInsert.bind(this),
|
|
470
472
|
relationshipOnRemove: this.internalrelationshipOnRemove.bind(this),
|
|
471
473
|
releaseManagedRefsOnRow: this.internalreleaseManagedRefsOnRow.bind(this),
|
|
472
|
-
removeComponentChange: this.internalremoveComponentChange.bind(this),
|
|
473
474
|
removeComponentCore: this.internalremoveComponentCore.bind(this),
|
|
474
|
-
removeEntityChanges: this.internalremoveEntityChanges.bind(this),
|
|
475
475
|
routeError: this.internalrouteError.bind(this),
|
|
476
|
+
restoreMutationEpoch: this.internalrestoreMutationEpoch.bind(this),
|
|
476
477
|
setFixedAccumulator: this.internalsetFixedAccumulator.bind(this),
|
|
477
478
|
setQueryRow: this.internalsetQueryRow.bind(this),
|
|
478
479
|
spawnCore: this.internalspawnCore.bind(this),
|
|
@@ -536,19 +537,18 @@ export class World {
|
|
|
536
537
|
return this.mutationEpoch;
|
|
537
538
|
}
|
|
538
539
|
|
|
539
|
-
/**
|
|
540
|
-
private
|
|
541
|
-
return this.
|
|
540
|
+
/** Structure snapshot used to invalidate borrowed query facades. */
|
|
541
|
+
private internalgetStructureEpoch(): number {
|
|
542
|
+
return this.structureEpoch;
|
|
542
543
|
}
|
|
543
544
|
|
|
544
|
-
/**
|
|
545
|
-
private
|
|
546
|
-
return this.
|
|
545
|
+
/** Typed structural facts are produced by this World and consumed by projections. */
|
|
546
|
+
private internalgetStructuralEvidence(): StructuralEvidenceRing {
|
|
547
|
+
return this.core.structuralEvidence;
|
|
547
548
|
}
|
|
548
549
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
return this.structureEpoch;
|
|
550
|
+
private internalrecordStructuralEvidence(evidence: StructuralEvidenceInput): void {
|
|
551
|
+
this.core.structuralEvidence.append(evidence);
|
|
552
552
|
}
|
|
553
553
|
|
|
554
554
|
/** Resolve current logical identity for a packed entity handle. */
|
|
@@ -577,6 +577,25 @@ export class World {
|
|
|
577
577
|
return this.mutationEpoch;
|
|
578
578
|
}
|
|
579
579
|
|
|
580
|
+
private internalrestoreMutationEpoch(epoch: number): void {
|
|
581
|
+
this.mutationEpoch = epoch;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
private internalpublishDerivedRange(
|
|
585
|
+
table: ArchetypeGraph['tables'][number],
|
|
586
|
+
componentId: number,
|
|
587
|
+
rowStart: number,
|
|
588
|
+
rowCount: number,
|
|
589
|
+
epoch: number,
|
|
590
|
+
): void {
|
|
591
|
+
const epochs = table.storage.get(componentId)?.epochs;
|
|
592
|
+
if (epochs === undefined) {
|
|
593
|
+
throw new Error(`Derived component ${componentId} is not in the table.`);
|
|
594
|
+
}
|
|
595
|
+
epochs.changed.fill(epoch, rowStart, rowStart + rowCount);
|
|
596
|
+
this.componentMutationEpochs[componentId] = epoch;
|
|
597
|
+
}
|
|
598
|
+
|
|
580
599
|
/** Record one successful structural mutation. */
|
|
581
600
|
private internalmarkStructureChanged(): void {
|
|
582
601
|
this.structureEpoch += 1;
|
|
@@ -600,7 +619,6 @@ export class World {
|
|
|
600
619
|
markComponentsAdded(this.graph, record, entity, componentIds, epoch);
|
|
601
620
|
for (const componentId of componentIds) {
|
|
602
621
|
this.componentMutationEpochs[componentId] = epoch;
|
|
603
|
-
this.changeJournal.append({ kind: 'component-added', entity, componentId });
|
|
604
622
|
}
|
|
605
623
|
}
|
|
606
624
|
|
|
@@ -615,7 +633,6 @@ export class World {
|
|
|
615
633
|
});
|
|
616
634
|
if (epoch !== undefined) {
|
|
617
635
|
this.componentMutationEpochs[componentId] = epoch;
|
|
618
|
-
this.changeJournal.append({ kind: 'component-changed', entity, componentId });
|
|
619
636
|
}
|
|
620
637
|
}
|
|
621
638
|
|
|
@@ -631,13 +648,6 @@ export class World {
|
|
|
631
648
|
const epoch = this.internalnextMutationEpoch();
|
|
632
649
|
epochs.changed.fill(epoch, rowStart, rowStart + rowCount);
|
|
633
650
|
this.componentMutationEpochs[componentId] = epoch;
|
|
634
|
-
for (const entity of readTableEntityRange(table, rowStart, rowCount)) {
|
|
635
|
-
this.changeJournal.append({
|
|
636
|
-
kind: 'component-changed',
|
|
637
|
-
entity,
|
|
638
|
-
componentId,
|
|
639
|
-
});
|
|
640
|
-
}
|
|
641
651
|
}
|
|
642
652
|
|
|
643
653
|
/** Latest mutation token for one component-owned projection. */
|
|
@@ -645,6 +655,11 @@ export class World {
|
|
|
645
655
|
return this.componentMutationEpochs[componentId] ?? 0;
|
|
646
656
|
}
|
|
647
657
|
|
|
658
|
+
/** Borrow the component-version summary used to skip unrelated value columns. */
|
|
659
|
+
private internalgetComponentMutationEpochs(): readonly number[] {
|
|
660
|
+
return this.componentMutationEpochs;
|
|
661
|
+
}
|
|
662
|
+
|
|
648
663
|
/** Read a materialized relationship target in O(1 + k). */
|
|
649
664
|
private internalgetRelationshipTargetEntities(
|
|
650
665
|
source: Component,
|
|
@@ -658,16 +673,6 @@ export class World {
|
|
|
658
673
|
return this.core.relationshipIndexes.get(componentId(source))?.epoch ?? 0;
|
|
659
674
|
}
|
|
660
675
|
|
|
661
|
-
/** Publish changes to a value derived without an authored mutation epoch. */
|
|
662
|
-
private internalmarkDerivedComponentChanges(
|
|
663
|
-
componentId: number,
|
|
664
|
-
entities: Iterable<EntityHandle>,
|
|
665
|
-
): void {
|
|
666
|
-
for (const entity of entities) {
|
|
667
|
-
this.changeJournal.append({ kind: 'derived-component-changed', entity, componentId });
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
676
|
/** Query facade write after the facade has already marked evidence. */
|
|
672
677
|
private internalsetQueryRow(
|
|
673
678
|
entity: EntityHandle,
|
|
@@ -685,16 +690,6 @@ export class World {
|
|
|
685
690
|
return this.componentAccess.get(entity, component) as Result<Record<string, unknown>, EcsError>;
|
|
686
691
|
}
|
|
687
692
|
|
|
688
|
-
/** Remove one component's change state after archetype removal. */
|
|
689
|
-
private internalremoveComponentChange(entity: EntityHandle, componentId: number): void {
|
|
690
|
-
this.changeJournal.append({ kind: 'component-removed', entity, componentId });
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
/** Remove all change state before an entity handle is retired. */
|
|
694
|
-
private internalremoveEntityChanges(entity: EntityHandle): void {
|
|
695
|
-
this.changeJournal.append({ kind: 'entity-removed', entity });
|
|
696
|
-
}
|
|
697
|
-
|
|
698
693
|
/** Return resource change ticks for diagnostics and resource-driven systems. */
|
|
699
694
|
getResourceChange(name: string): ChangeTicks | undefined {
|
|
700
695
|
const entry = this.resources.entries.get(name);
|