@forgeax/engine-ecs 0.1.26 → 0.1.28
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 +94 -35
- package/dist/__tests__/world-read.unit.test.d.ts +2 -0
- package/dist/__tests__/world-read.unit.test.d.ts.map +1 -0
- package/dist/commands.d.ts +2 -0
- package/dist/commands.d.ts.map +1 -1
- package/dist/index.mjs +3471 -4263
- package/dist/index.mjs.map +1 -1
- package/dist/internal.d.ts +2 -3
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.mjs +3 -333
- package/dist/internal.mjs.map +1 -1
- package/dist/projection/index.mjs.map +1 -1
- package/dist/shared.mjs.map +1 -1
- package/dist/world-entity-lifecycle.d.ts +3 -14
- package/dist/world-entity-lifecycle.d.ts.map +1 -1
- package/dist/world-internal.d.ts +65 -5
- package/dist/world-internal.d.ts.map +1 -1
- package/dist/world-read.d.ts +16 -0
- package/dist/world-read.d.ts.map +1 -0
- package/dist/world-read.mjs +8 -0
- package/dist/world-read.mjs.map +1 -0
- package/dist/world-scheduling.d.ts +0 -4
- package/dist/world-scheduling.d.ts.map +1 -1
- package/dist/world-storage-primitives.d.ts +26 -0
- package/dist/world-storage-primitives.d.ts.map +1 -0
- package/dist/world.d.ts +352 -157
- package/dist/world.d.ts.map +1 -1
- package/package.json +8 -4
- package/src/__tests__/command-buffer.test.ts +29 -3
- package/src/__tests__/hierarchy.unit.test.ts +3 -3
- package/src/__tests__/world-health.contract.test.ts +195 -2
- package/src/__tests__/world-read.unit.test.ts +30 -0
- package/src/commands.ts +22 -9
- package/src/internal.ts +5 -3
- package/src/world-entity-lifecycle.ts +23 -252
- package/src/world-internal-augmentation.d.ts +11 -0
- package/src/world-internal.ts +114 -63
- package/src/world-read.ts +38 -0
- package/src/world-scheduling.ts +0 -26
- package/src/world-storage-primitives.ts +179 -0
- package/src/world.ts +2590 -509
- package/dist/world-component-access.d.ts +0 -311
- package/dist/world-component-access.d.ts.map +0 -1
- package/dist/world-component-storage.d.ts +0 -298
- package/dist/world-component-storage.d.ts.map +0 -1
- package/dist/world-core.d.ts +0 -39
- package/dist/world-core.d.ts.map +0 -1
- package/src/world-component-access.ts +0 -1769
- package/src/world-component-storage.ts +0 -1264
- package/src/world-core.ts +0 -74
|
@@ -1,37 +1,24 @@
|
|
|
1
1
|
// @forgeax/engine-ecs — world-entity-lifecycle: entity lifecycle and hierarchy.
|
|
2
2
|
//
|
|
3
|
-
// Owns
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// Owns spawn preflight and hierarchy orchestration. World is the sole state
|
|
4
|
+
// owner; this module composes its typed internal capabilities into those
|
|
5
|
+
// public lifecycle behaviors.
|
|
6
6
|
|
|
7
|
-
import { err,
|
|
8
|
-
import type { Component, ComponentSchema, InputShapeOf
|
|
7
|
+
import { err, ok, pack, type Result } from '@forgeax/engine-types';
|
|
8
|
+
import type { Component, ComponentSchema, InputShapeOf } from './component';
|
|
9
9
|
import { componentId, componentSchema } from './component';
|
|
10
|
-
import { fillComponentDefaults, validateComponentDataKeys } from './component-default-fallback';
|
|
11
10
|
import { expandComponentRequirements } from './component-schema';
|
|
12
|
-
import {
|
|
13
|
-
import { Entity } from './entity';
|
|
14
|
-
import {
|
|
15
|
-
ENTITY_NULL_RAW,
|
|
16
|
-
type EntityHandle,
|
|
17
|
-
encodeEntity,
|
|
18
|
-
entityGeneration,
|
|
19
|
-
entityIndex,
|
|
20
|
-
} from './entity-handle';
|
|
11
|
+
import { ENTITY_NULL_RAW, type EntityHandle, entityGeneration, entityIndex } from './entity-handle';
|
|
21
12
|
import {
|
|
22
13
|
ComponentNotPresentError,
|
|
23
14
|
RelationshipDetachMismatchError,
|
|
24
15
|
RelationshipSelfCycleError,
|
|
25
16
|
StaleEntityError,
|
|
26
|
-
validateEnumFieldValues,
|
|
27
17
|
} from './errors';
|
|
28
|
-
import { relationshipRole
|
|
29
|
-
import {
|
|
18
|
+
import { relationshipRole } from './relationship-index';
|
|
19
|
+
import type { Archetype } from './storage/archetype';
|
|
30
20
|
import type { ArchetypeGraph } from './storage/archetype-graph';
|
|
31
|
-
import {
|
|
32
|
-
import { removeSparseTag } from './storage/change-detection';
|
|
33
|
-
import { appendTableRow, removeTableRow } from './storage/table';
|
|
34
|
-
import type { EcsError, World } from './world';
|
|
21
|
+
import type { ComponentData, EcsError, World } from './world';
|
|
35
22
|
import { worldInternal } from './world-internal';
|
|
36
23
|
|
|
37
24
|
function tableRow(world: World, record: { archetypeId: number; archetypeRow: number }): number {
|
|
@@ -40,190 +27,23 @@ function tableRow(world: World, record: { archetypeId: number; archetypeRow: num
|
|
|
40
27
|
}
|
|
41
28
|
|
|
42
29
|
/**
|
|
43
|
-
* Core implementation of `spawn
|
|
44
|
-
*
|
|
45
|
-
* @param internal - `true` when relationship maintenance creates a mirror.
|
|
30
|
+
* Core implementation of `spawn`.
|
|
46
31
|
*/
|
|
47
32
|
export function spawnCore(
|
|
48
33
|
world: World,
|
|
49
|
-
componentDatas:
|
|
50
|
-
internal: boolean,
|
|
34
|
+
componentDatas: ComponentData[],
|
|
51
35
|
): Result<EntityHandle, EcsError> {
|
|
52
36
|
componentDatas = expandComponentRequirements(componentDatas);
|
|
53
|
-
const filledData: Record<string, unknown>[] = [];
|
|
54
37
|
for (const cd of componentDatas) {
|
|
55
38
|
const preflight = world[worldInternal].preflightComponentData(null, cd);
|
|
56
39
|
if (!preflight.ok) return preflight;
|
|
57
|
-
const keyErr = validateComponentDataKeys(cd.component, cd.data as Record<string, unknown>);
|
|
58
|
-
if (keyErr !== null) return err(keyErr as unknown as EcsError);
|
|
59
|
-
const arrayErr = validateManagedArrayValues(cd.component, cd.data as Record<string, unknown>);
|
|
60
|
-
if (arrayErr !== null) return err(arrayErr as unknown as EcsError);
|
|
61
|
-
const sharedErr = validateSharedFieldValues(cd.component, cd.data as Record<string, unknown>);
|
|
62
|
-
if (sharedErr !== null) return err(sharedErr as unknown as EcsError);
|
|
63
|
-
const filled = fillComponentDefaults(cd.component, cd.data as Record<string, unknown>);
|
|
64
|
-
const enumErr = validateEnumFieldValues(cd.component, filled);
|
|
65
|
-
if (enumErr !== null) return err(enumErr as unknown as EcsError);
|
|
66
|
-
filledData.push(filled as Record<string, unknown>);
|
|
67
|
-
}
|
|
68
|
-
// Reserve every relationship target before allocating the new source row.
|
|
69
|
-
// In particular, an injected BufferPool failure must not leave a source
|
|
70
|
-
// entity, mirror component, or mutation epoch behind.
|
|
71
|
-
const relationshipPreparations: unknown[] = [];
|
|
72
|
-
if (!internal) {
|
|
73
|
-
for (let index = 0; index < componentDatas.length; index += 1) {
|
|
74
|
-
const componentData = componentDatas[index];
|
|
75
|
-
const value = filledData[index];
|
|
76
|
-
if (
|
|
77
|
-
componentData === undefined ||
|
|
78
|
-
value === undefined ||
|
|
79
|
-
relationshipRole(componentData.component as Component)?.kind !== 'source'
|
|
80
|
-
) {
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
const prepared = world[worldInternal].prepareRelationshipInsert(
|
|
84
|
-
componentData.component as Component,
|
|
85
|
-
value,
|
|
86
|
-
);
|
|
87
|
-
if (!prepared.ok) {
|
|
88
|
-
for (const reservation of relationshipPreparations) {
|
|
89
|
-
world[worldInternal].releaseRelationshipPreparation(reservation);
|
|
90
|
-
}
|
|
91
|
-
return prepared;
|
|
92
|
-
}
|
|
93
|
-
relationshipPreparations[index] = prepared.value;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
const indexSlot = world[worldInternal].allocateIndex();
|
|
97
|
-
const record = world[worldInternal].getRecords()[indexSlot];
|
|
98
|
-
if (record === undefined)
|
|
99
|
-
return err(
|
|
100
|
-
new Error('Internal: allocateIndex did not initialize record') as unknown as EcsError,
|
|
101
|
-
);
|
|
102
|
-
const componentIds = componentDatas.map((cd) => componentId(cd.component));
|
|
103
|
-
const components = componentDatas.map((cd) => cd.component);
|
|
104
|
-
const graph = world[worldInternal].getGraph();
|
|
105
|
-
const arch = getOrCreateArchetype(graph, componentIds, components);
|
|
106
|
-
const table = getTable(graph, arch.tableId);
|
|
107
|
-
const spawnedEntity = encodeEntity(indexSlot, record.generation);
|
|
108
|
-
const tableRow = appendTableRow(table, spawnedEntity);
|
|
109
|
-
const archetypeRow = appendArchetypeRow(arch, tableRow);
|
|
110
|
-
record.archetypeId = arch.id;
|
|
111
|
-
record.archetypeRow = archetypeRow;
|
|
112
|
-
for (let i = 0; i < componentDatas.length; i++) {
|
|
113
|
-
const cdi = componentDatas[i];
|
|
114
|
-
const fdi = filledData[i];
|
|
115
|
-
if (cdi === undefined || fdi === undefined) continue;
|
|
116
|
-
world[worldInternal].writeRow(arch, cdi.component, tableRow, fdi as ShapeOf<ComponentSchema>);
|
|
117
40
|
}
|
|
118
|
-
world[worldInternal].
|
|
119
|
-
world[worldInternal].
|
|
120
|
-
|
|
121
|
-
...componentDatas.map((cd) => componentId(cd.component)),
|
|
122
|
-
]);
|
|
123
|
-
for (let i = 0; i < componentDatas.length; i++) {
|
|
124
|
-
const cd = componentDatas[i];
|
|
125
|
-
const filled = filledData[i];
|
|
126
|
-
if (!cd || filled === undefined) continue;
|
|
127
|
-
if (!internal && relationshipRole(cd.component as Component)?.kind === 'source') {
|
|
128
|
-
const relation = world[worldInternal].relationshipOnInsert(
|
|
129
|
-
spawnedEntity,
|
|
130
|
-
cd.component as Component,
|
|
131
|
-
filled,
|
|
132
|
-
relationshipPreparations[i],
|
|
133
|
-
);
|
|
134
|
-
if (!relation.ok) {
|
|
135
|
-
for (const reservation of relationshipPreparations) {
|
|
136
|
-
world[worldInternal].releaseRelationshipPreparation(reservation);
|
|
137
|
-
}
|
|
138
|
-
return relation;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
world[worldInternal].markStructureChanged();
|
|
143
|
-
world[worldInternal].recordStructuralEvidence({
|
|
144
|
-
kind: 'spawn',
|
|
145
|
-
entity: spawnedEntity,
|
|
146
|
-
});
|
|
41
|
+
const spawnedEntity = world[worldInternal].allocatePendingEntity();
|
|
42
|
+
const materialized = world[worldInternal].materializeEntity(spawnedEntity, componentDatas);
|
|
43
|
+
if (!materialized.ok) return materialized;
|
|
147
44
|
return ok(spawnedEntity);
|
|
148
45
|
}
|
|
149
46
|
|
|
150
|
-
/**
|
|
151
|
-
* Core implementation of `despawn` with a linked-spawn cascade guard.
|
|
152
|
-
*
|
|
153
|
-
* @param internal - `true` while recursively retiring linked children.
|
|
154
|
-
*/
|
|
155
|
-
export function despawnCore(
|
|
156
|
-
world: World,
|
|
157
|
-
entity: EntityHandle,
|
|
158
|
-
internal: boolean,
|
|
159
|
-
): Result<void, EcsError> {
|
|
160
|
-
const slot = entityIndex(entity);
|
|
161
|
-
const gen = entityGeneration(entity);
|
|
162
|
-
const record = world[worldInternal].getRecords()[slot];
|
|
163
|
-
if (!world[worldInternal].recordIsLive(record, gen)) return ok(undefined);
|
|
164
|
-
const arch = world[worldInternal].getGraph().archetypes[record?.archetypeId];
|
|
165
|
-
const linkedChildren = arch ? relationshipLinkedSpawnChildren(world, entity, arch) : [];
|
|
166
|
-
if (arch) {
|
|
167
|
-
const graph = world[worldInternal].getGraph();
|
|
168
|
-
const table = getTable(graph, arch.tableId);
|
|
169
|
-
const archetypeRow = record.archetypeRow;
|
|
170
|
-
const tableRow = arch.rows[archetypeRow] ?? 0;
|
|
171
|
-
for (const comp of arch.components) {
|
|
172
|
-
const role = relationshipRole(comp);
|
|
173
|
-
const needsOldValue = role?.kind === 'source' && !internal;
|
|
174
|
-
if (needsOldValue) {
|
|
175
|
-
const oldValue = world[worldInternal].readRow(arch, comp, tableRow) as Record<
|
|
176
|
-
string,
|
|
177
|
-
unknown
|
|
178
|
-
>;
|
|
179
|
-
if (role?.kind === 'source' && !internal) {
|
|
180
|
-
const relation = world[worldInternal].relationshipOnRemove(entity, comp, oldValue);
|
|
181
|
-
if (!relation.ok) return relation;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
world[worldInternal].releaseManagedRefsOnRow(arch, comp, tableRow);
|
|
185
|
-
}
|
|
186
|
-
for (const component of arch.components) {
|
|
187
|
-
if (component.storage !== 'sparse') continue;
|
|
188
|
-
const set = graph.sparseTags.get(componentId(component));
|
|
189
|
-
if (set !== undefined) removeSparseTag(set, entity);
|
|
190
|
-
}
|
|
191
|
-
const archetypeSwap = removeArchetypeRow(arch, archetypeRow);
|
|
192
|
-
if (archetypeSwap !== null) {
|
|
193
|
-
const movedEntity = (table.storage.get(componentId(Entity))?.fields.get('self')?.view[
|
|
194
|
-
archetypeSwap.movedTableRow
|
|
195
|
-
] ?? 0) as EntityHandle;
|
|
196
|
-
const movedRecord = world[worldInternal].getRecords()[entityIndex(movedEntity)];
|
|
197
|
-
if (movedRecord?.generation === entityGeneration(movedEntity)) {
|
|
198
|
-
movedRecord.archetypeRow = archetypeSwap.newRow;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
const tableSwap = removeTableRow(table, tableRow);
|
|
202
|
-
if (tableSwap !== null) {
|
|
203
|
-
const movedRecord = world[worldInternal].getRecords()[entityIndex(tableSwap.movedEntity)];
|
|
204
|
-
if (movedRecord?.generation === entityGeneration(tableSwap.movedEntity)) {
|
|
205
|
-
const movedArchetype = graph.archetypes[movedRecord.archetypeId];
|
|
206
|
-
if (movedArchetype !== undefined) {
|
|
207
|
-
movedArchetype.rows[movedRecord.archetypeRow] = tableSwap.newRow;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
world[worldInternal].recordStructuralEvidence({
|
|
213
|
-
kind: 'despawn',
|
|
214
|
-
entity,
|
|
215
|
-
});
|
|
216
|
-
if (record) {
|
|
217
|
-
record.archetypeId = -1;
|
|
218
|
-
record.archetypeRow = -1;
|
|
219
|
-
record.generation += 1;
|
|
220
|
-
if (!isRetiredSlot(record.generation)) world[worldInternal].getFreeIndices().push(slot);
|
|
221
|
-
}
|
|
222
|
-
for (const child of linkedChildren) despawnCore(world, child, true);
|
|
223
|
-
world[worldInternal].markStructureChanged();
|
|
224
|
-
return ok(undefined);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
47
|
/** Attach a child and maintain the relationship mirror through component storage. */
|
|
228
48
|
export function worldAddChild<S extends ComponentSchema>(
|
|
229
49
|
world: World,
|
|
@@ -233,39 +53,19 @@ export function worldAddChild<S extends ComponentSchema>(
|
|
|
233
53
|
data: Partial<InputShapeOf<S>>,
|
|
234
54
|
): Result<void, EcsError> {
|
|
235
55
|
const holderComp = component as Component;
|
|
236
|
-
|
|
237
|
-
if (role?.kind !== 'source') {
|
|
56
|
+
if (relationshipRole(holderComp)?.kind !== 'source') {
|
|
238
57
|
return err(new ComponentNotPresentError(child as number, component.name));
|
|
239
58
|
}
|
|
240
59
|
|
|
60
|
+
const parentResult = world[worldInternal].lookupAlive(parent, 'addChild', component.name);
|
|
61
|
+
if (!parentResult.ok) return parentResult;
|
|
241
62
|
const parentSlot = entityIndex(parent);
|
|
242
63
|
const parentGeneration = entityGeneration(parent);
|
|
243
|
-
const
|
|
244
|
-
if (!
|
|
245
|
-
return err(
|
|
246
|
-
new StaleEntityError(parent as number, parentSlot, parentGeneration, {
|
|
247
|
-
operation: 'addChild',
|
|
248
|
-
component: component.name,
|
|
249
|
-
expectedGeneration: parentGeneration,
|
|
250
|
-
actualGeneration: world[worldInternal].getRecords()[parentSlot]?.generation ?? -1,
|
|
251
|
-
}),
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
|
|
64
|
+
const childResult = world[worldInternal].lookupAlive(child, 'addChild', component.name);
|
|
65
|
+
if (!childResult.ok) return childResult;
|
|
255
66
|
const childSlot = entityIndex(child);
|
|
256
|
-
const childGeneration = entityGeneration(child);
|
|
257
|
-
const childRecord = world[worldInternal].getRecords()[childSlot];
|
|
258
|
-
if (!world[worldInternal].recordIsLive(childRecord, childGeneration)) {
|
|
259
|
-
return err(
|
|
260
|
-
new StaleEntityError(child as number, childSlot, childGeneration, {
|
|
261
|
-
operation: 'addChild',
|
|
262
|
-
component: component.name,
|
|
263
|
-
expectedGeneration: childGeneration,
|
|
264
|
-
actualGeneration: world[worldInternal].getRecords()[childSlot]?.generation ?? -1,
|
|
265
|
-
}),
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
67
|
|
|
68
|
+
const role = relationshipRole(holderComp);
|
|
269
69
|
if (child === parent && !(role?.kind === 'source' && role.allowSelf)) {
|
|
270
70
|
return err(new RelationshipSelfCycleError(component.name, child as number, child as number));
|
|
271
71
|
}
|
|
@@ -346,13 +146,13 @@ export function worldReparent<S extends ComponentSchema>(
|
|
|
346
146
|
if (role?.kind !== 'source') {
|
|
347
147
|
return err(new ComponentNotPresentError(child as number, component.name));
|
|
348
148
|
}
|
|
349
|
-
if (child === newParent && !
|
|
149
|
+
if (child === newParent && !role.allowSelf) {
|
|
350
150
|
return err(
|
|
351
151
|
new RelationshipSelfCycleError(component.name, child as number, newParent as number),
|
|
352
152
|
);
|
|
353
153
|
}
|
|
354
154
|
const cycleHit =
|
|
355
|
-
child === newParent && role
|
|
155
|
+
child === newParent && role.allowSelf
|
|
356
156
|
? null
|
|
357
157
|
: relationshipChainCycleHit(
|
|
358
158
|
world,
|
|
@@ -387,7 +187,7 @@ export function worldReparent<S extends ComponentSchema>(
|
|
|
387
187
|
[role.sourceField]: newParent,
|
|
388
188
|
} as Partial<InputShapeOf<S>>;
|
|
389
189
|
if (
|
|
390
|
-
childArch.components.some((
|
|
190
|
+
childArch.components.some((component) => componentId(component) === componentId(holderComp))
|
|
391
191
|
) {
|
|
392
192
|
// Existing exclusive sources are updated through the same owner-level
|
|
393
193
|
// write barrier as `world.set`; remove+add would expose a partial mirror
|
|
@@ -495,8 +295,6 @@ export function worldIterDescendants(world: World, entity: EntityHandle): Iterab
|
|
|
495
295
|
function descendantChildren(world: World, arch: Archetype, row: number): EntityHandle[] {
|
|
496
296
|
const children: EntityHandle[] = [];
|
|
497
297
|
for (const component of arch.components) {
|
|
498
|
-
if (!arch.components.some((candidate) => componentId(candidate) === componentId(component)))
|
|
499
|
-
continue;
|
|
500
298
|
const value = world[worldInternal].readRow(arch, component, row) as Record<string, unknown>;
|
|
501
299
|
for (const [fieldName, fieldType] of Object.entries(componentSchema(component))) {
|
|
502
300
|
if (fieldType !== 'array<entity>') continue;
|
|
@@ -559,30 +357,3 @@ function relationshipChainCycleHit(
|
|
|
559
357
|
currentGeneration = entityGeneration(target);
|
|
560
358
|
}
|
|
561
359
|
}
|
|
562
|
-
|
|
563
|
-
function linkedSpawnMirrorField(mirror: Component): string | undefined {
|
|
564
|
-
const source = relationshipSource(mirror);
|
|
565
|
-
const role = source === undefined ? undefined : relationshipRole(source);
|
|
566
|
-
return role?.kind === 'source' && role.linkedSpawn ? role.targetField : undefined;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function relationshipLinkedSpawnChildren(
|
|
570
|
-
world: World,
|
|
571
|
-
entity: EntityHandle,
|
|
572
|
-
arch: Archetype,
|
|
573
|
-
): EntityHandle[] {
|
|
574
|
-
const record = world[worldInternal].getRecords()[entityIndex(entity)];
|
|
575
|
-
const row = record === undefined ? -1 : tableRow(world, record);
|
|
576
|
-
const collected: EntityHandle[] = [];
|
|
577
|
-
for (const component of arch.components) {
|
|
578
|
-
const mirrorField = linkedSpawnMirrorField(component);
|
|
579
|
-
if (mirrorField === undefined) continue;
|
|
580
|
-
const snapshot = world[worldInternal].readRow(arch, component, row) as Record<string, unknown>;
|
|
581
|
-
const list = snapshot[mirrorField];
|
|
582
|
-
if (!(list instanceof Uint32Array)) continue;
|
|
583
|
-
for (const raw of list) {
|
|
584
|
-
if (raw !== ENTITY_NULL_RAW) collected.push(raw as EntityHandle);
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
return collected;
|
|
588
|
-
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Source-only type augmentation for ECS owner modules and package-owned tests.
|
|
2
|
+
// This declaration is included in the ECS TypeScript program but is not
|
|
3
|
+
// imported by World, so declaration emit does not make public `world.d.ts`
|
|
4
|
+
// depend on the raw world-internal module.
|
|
5
|
+
import { type WorldInternal, worldInternal } from './world-internal';
|
|
6
|
+
|
|
7
|
+
declare module './world' {
|
|
8
|
+
interface World {
|
|
9
|
+
[worldInternal]: WorldInternal;
|
|
10
|
+
}
|
|
11
|
+
}
|
package/src/world-internal.ts
CHANGED
|
@@ -11,70 +11,121 @@
|
|
|
11
11
|
// The registry is package-private by convention: no root/advanced export
|
|
12
12
|
// exposes this key, while Symbol.for keeps source/dist and split bundles on
|
|
13
13
|
// one identity.
|
|
14
|
+
import type { Result } from '@forgeax/engine-types';
|
|
15
|
+
import type { BufferPool } from './buffer-pool';
|
|
16
|
+
import type { Component, ComponentSchema, ShapeOf } from './component';
|
|
17
|
+
import type { EntityHandle } from './entity-handle';
|
|
18
|
+
import type { WorldExecutionFault } from './execution/shared-kernel';
|
|
19
|
+
import type { ResourceStore } from './resource';
|
|
20
|
+
import type { Schedule } from './schedule';
|
|
21
|
+
import type { ScheduleToken } from './schedule-token';
|
|
22
|
+
import type { SharedRefStore } from './shared-ref-store';
|
|
23
|
+
import type { Archetype } from './storage/archetype';
|
|
24
|
+
import type { ArchetypeGraph } from './storage/archetype-graph';
|
|
25
|
+
import type { ChangeTicks } from './storage/change-detection';
|
|
26
|
+
import type { StructuralEvidenceRing } from './storage/structural-evidence';
|
|
27
|
+
import type { Table } from './storage/table';
|
|
28
|
+
import type { ClockWriter } from './time';
|
|
29
|
+
import type { ComponentData, EcsError, EntityRecord } from './world';
|
|
30
|
+
|
|
31
|
+
/** @internal Package-private identity; absent from the public export map. */
|
|
14
32
|
export const worldInternal: unique symbol = Symbol.for(
|
|
15
33
|
'forgeax.ecs.worldInternal',
|
|
16
34
|
) as unknown as typeof worldInternal;
|
|
17
35
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
|
40
|
-
|
|
41
|
-
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
36
|
+
/**
|
|
37
|
+
* The one package-internal capability surface owned by World.
|
|
38
|
+
*
|
|
39
|
+
* Every member is explicit so an extraction cannot silently widen the seam or
|
|
40
|
+
* leak an untyped state bag. The symbol itself remains package-private and is
|
|
41
|
+
* the only route used by query, commands, and lifecycle helpers.
|
|
42
|
+
*/
|
|
43
|
+
/** @internal Raw ECS owner seam; source-relative consumers only. */
|
|
44
|
+
export interface WorldInternal {
|
|
45
|
+
readonly allocatePendingEntity: () => EntityHandle;
|
|
46
|
+
readonly cancelPendingEntity: (entity: EntityHandle) => void;
|
|
47
|
+
readonly getArrayView: (
|
|
48
|
+
entity: EntityHandle,
|
|
49
|
+
component: Component,
|
|
50
|
+
fieldName: string,
|
|
51
|
+
) => ArrayLike<number> | undefined;
|
|
52
|
+
readonly getBufferPool: () => BufferPool;
|
|
53
|
+
readonly getClockWriter: () => ClockWriter;
|
|
54
|
+
readonly getComponentChange: (
|
|
55
|
+
entity: EntityHandle,
|
|
56
|
+
componentId: number,
|
|
57
|
+
) => ChangeTicks | undefined;
|
|
58
|
+
readonly getComponentMutationEpochs: () => readonly number[];
|
|
59
|
+
readonly getEntityArchetype: (entity: EntityHandle) => Archetype | undefined;
|
|
60
|
+
readonly getFixedAccumulator: () => number;
|
|
61
|
+
readonly getGraph: () => ArchetypeGraph;
|
|
62
|
+
readonly getMutationEpoch: () => number;
|
|
63
|
+
readonly getQueryRow: (
|
|
64
|
+
entity: EntityHandle,
|
|
65
|
+
component: Component,
|
|
66
|
+
) => Result<Record<string, unknown>, EcsError>;
|
|
67
|
+
readonly getRecords: () => EntityRecord[];
|
|
68
|
+
readonly getRelationshipEpoch: (component: Component) => number;
|
|
69
|
+
readonly getRelationshipTargetEntities: (
|
|
70
|
+
component: Component,
|
|
71
|
+
target: EntityHandle,
|
|
72
|
+
) => readonly EntityHandle[];
|
|
73
|
+
readonly getResources: () => ResourceStore;
|
|
74
|
+
readonly getSchedule: (token: ScheduleToken) => Schedule | undefined;
|
|
75
|
+
readonly getSchedules: () => ReadonlyMap<ScheduleToken, Schedule>;
|
|
76
|
+
readonly getSharedRefs: () => SharedRefStore;
|
|
77
|
+
readonly getStructureEpoch: () => number;
|
|
78
|
+
readonly getStructuralEvidence: () => StructuralEvidenceRing;
|
|
79
|
+
readonly lookupAlive: (
|
|
80
|
+
entity: EntityHandle,
|
|
81
|
+
operation: string,
|
|
82
|
+
component?: string,
|
|
83
|
+
) => Result<EntityRecord, EcsError>;
|
|
84
|
+
readonly markComponentChanged: (entity: EntityHandle, componentId: number) => void;
|
|
85
|
+
readonly markComponentRangeChanged: (
|
|
86
|
+
table: Table,
|
|
87
|
+
componentId: number,
|
|
88
|
+
rowStart: number,
|
|
89
|
+
rowCount: number,
|
|
90
|
+
) => void;
|
|
91
|
+
readonly materializeEntity: (
|
|
92
|
+
entity: EntityHandle,
|
|
93
|
+
componentDatas: ComponentData[],
|
|
94
|
+
) => Result<void, EcsError>;
|
|
95
|
+
readonly materializePendingEntity: (
|
|
96
|
+
entity: EntityHandle,
|
|
97
|
+
componentDatas: ComponentData[],
|
|
98
|
+
) => Result<void, EcsError>;
|
|
99
|
+
readonly nextMutationEpoch: () => number;
|
|
100
|
+
readonly poisonExecution: (fault: WorldExecutionFault) => void;
|
|
101
|
+
readonly publishDerivedRange: (
|
|
102
|
+
table: Table,
|
|
103
|
+
componentId: number,
|
|
104
|
+
rowStart: number,
|
|
105
|
+
rowCount: number,
|
|
106
|
+
epoch: number,
|
|
107
|
+
) => void;
|
|
108
|
+
readonly preflightComponentData: (
|
|
109
|
+
holder: EntityHandle | null,
|
|
110
|
+
componentData: ComponentData,
|
|
111
|
+
pendingEntities?: ReadonlySet<number>,
|
|
112
|
+
unavailableEntities?: ReadonlySet<number>,
|
|
113
|
+
) => Result<void, EcsError>;
|
|
114
|
+
readonly readRow: <S extends ComponentSchema>(
|
|
115
|
+
archetype: Archetype,
|
|
116
|
+
component: Component<string, S>,
|
|
117
|
+
row: number,
|
|
118
|
+
) => ShapeOf<S>;
|
|
119
|
+
readonly recordIsLive: (
|
|
120
|
+
record: EntityRecord | undefined,
|
|
121
|
+
generation: number,
|
|
122
|
+
) => record is EntityRecord;
|
|
123
|
+
readonly routeError: (error: unknown, context?: { readonly systemName: string }) => void;
|
|
124
|
+
readonly restoreMutationEpoch: (epoch: number) => void;
|
|
125
|
+
readonly setFixedAccumulator: (value: number) => void;
|
|
126
|
+
readonly setQueryRow: (
|
|
127
|
+
entity: EntityHandle,
|
|
128
|
+
component: Component,
|
|
129
|
+
value: Record<string, unknown>,
|
|
130
|
+
) => Result<void, EcsError>;
|
|
131
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @forgeax/engine-ecs — safe read-only World seam.
|
|
2
|
+
//
|
|
3
|
+
// This is the only cross-package World capability needed by relationship and
|
|
4
|
+
// scene projections. It exposes semantic scalar/array reads, never the table,
|
|
5
|
+
// archetype, buffer-pool, or mutation stores behind those reads.
|
|
6
|
+
|
|
7
|
+
import type { Component } from './component';
|
|
8
|
+
import type { EntityHandle } from './entity-handle';
|
|
9
|
+
|
|
10
|
+
/** Stable identity for the World-owned semantic read capability. */
|
|
11
|
+
export const worldRead: unique symbol = Symbol.for(
|
|
12
|
+
'forgeax.ecs.worldRead',
|
|
13
|
+
) as unknown as typeof worldRead;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Read-only semantic probes used by owner packages such as scene.
|
|
17
|
+
*
|
|
18
|
+
* Missing entities, components, fields, and out-of-range elements return
|
|
19
|
+
* `undefined`; callers never receive a storage object or mutable view.
|
|
20
|
+
*/
|
|
21
|
+
export interface WorldRead {
|
|
22
|
+
readonly getFieldValue: (
|
|
23
|
+
entity: EntityHandle,
|
|
24
|
+
component: Component,
|
|
25
|
+
fieldName: string,
|
|
26
|
+
) => number | undefined;
|
|
27
|
+
readonly getArrayLength: (
|
|
28
|
+
entity: EntityHandle,
|
|
29
|
+
component: Component,
|
|
30
|
+
fieldName: string,
|
|
31
|
+
) => number | undefined;
|
|
32
|
+
readonly getArrayElement: (
|
|
33
|
+
entity: EntityHandle,
|
|
34
|
+
component: Component,
|
|
35
|
+
fieldName: string,
|
|
36
|
+
index: number,
|
|
37
|
+
) => number | undefined;
|
|
38
|
+
}
|
package/src/world-scheduling.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// @forgeax/engine-ecs -- World schedule and resource orchestration.
|
|
2
2
|
|
|
3
|
-
import type { Handle } from '@forgeax/engine-types';
|
|
4
3
|
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
5
4
|
import type { CommandBufferImpl } from './commands';
|
|
6
5
|
import {
|
|
@@ -472,28 +471,3 @@ export function worldScheduleUsesComponent(world: World, component: object): boo
|
|
|
472
471
|
}
|
|
473
472
|
return false;
|
|
474
473
|
}
|
|
475
|
-
|
|
476
|
-
export function worldAllocUniqueRef<Target extends string, T>(
|
|
477
|
-
world: World,
|
|
478
|
-
target: Target,
|
|
479
|
-
payload: T,
|
|
480
|
-
onRelease?: (payload: T) => void,
|
|
481
|
-
): Handle<Target, 'unique'> {
|
|
482
|
-
return world[worldInternal].getUniqueRefs().alloc(target, payload, onRelease);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
export function worldAllocSharedRef<Target extends string, T>(
|
|
486
|
-
world: World,
|
|
487
|
-
target: Target,
|
|
488
|
-
payload: T,
|
|
489
|
-
): Handle<Target, 'shared'> {
|
|
490
|
-
return world[worldInternal].getSharedRefs().alloc(target, payload);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
export function worldInternSharedRef<Target extends string, T extends object>(
|
|
494
|
-
world: World,
|
|
495
|
-
target: Target,
|
|
496
|
-
payload: T,
|
|
497
|
-
): Handle<Target, 'shared'> {
|
|
498
|
-
return world[worldInternal].getSharedRefs().intern(target, payload);
|
|
499
|
-
}
|