@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.
Files changed (50) hide show
  1. package/README.md +94 -35
  2. package/dist/__tests__/world-read.unit.test.d.ts +2 -0
  3. package/dist/__tests__/world-read.unit.test.d.ts.map +1 -0
  4. package/dist/commands.d.ts +2 -0
  5. package/dist/commands.d.ts.map +1 -1
  6. package/dist/index.mjs +3471 -4263
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/internal.d.ts +2 -3
  9. package/dist/internal.d.ts.map +1 -1
  10. package/dist/internal.mjs +3 -333
  11. package/dist/internal.mjs.map +1 -1
  12. package/dist/projection/index.mjs.map +1 -1
  13. package/dist/shared.mjs.map +1 -1
  14. package/dist/world-entity-lifecycle.d.ts +3 -14
  15. package/dist/world-entity-lifecycle.d.ts.map +1 -1
  16. package/dist/world-internal.d.ts +65 -5
  17. package/dist/world-internal.d.ts.map +1 -1
  18. package/dist/world-read.d.ts +16 -0
  19. package/dist/world-read.d.ts.map +1 -0
  20. package/dist/world-read.mjs +8 -0
  21. package/dist/world-read.mjs.map +1 -0
  22. package/dist/world-scheduling.d.ts +0 -4
  23. package/dist/world-scheduling.d.ts.map +1 -1
  24. package/dist/world-storage-primitives.d.ts +26 -0
  25. package/dist/world-storage-primitives.d.ts.map +1 -0
  26. package/dist/world.d.ts +352 -157
  27. package/dist/world.d.ts.map +1 -1
  28. package/package.json +8 -4
  29. package/src/__tests__/command-buffer.test.ts +29 -3
  30. package/src/__tests__/hierarchy.unit.test.ts +3 -3
  31. package/src/__tests__/world-health.contract.test.ts +195 -2
  32. package/src/__tests__/world-read.unit.test.ts +30 -0
  33. package/src/commands.ts +22 -9
  34. package/src/internal.ts +5 -3
  35. package/src/world-entity-lifecycle.ts +23 -252
  36. package/src/world-internal-augmentation.d.ts +11 -0
  37. package/src/world-internal.ts +114 -63
  38. package/src/world-read.ts +38 -0
  39. package/src/world-scheduling.ts +0 -26
  40. package/src/world-storage-primitives.ts +179 -0
  41. package/src/world.ts +2590 -509
  42. package/dist/world-component-access.d.ts +0 -311
  43. package/dist/world-component-access.d.ts.map +0 -1
  44. package/dist/world-component-storage.d.ts +0 -298
  45. package/dist/world-component-storage.d.ts.map +0 -1
  46. package/dist/world-core.d.ts +0 -39
  47. package/dist/world-core.d.ts.map +0 -1
  48. package/src/world-component-access.ts +0 -1769
  49. package/src/world-component-storage.ts +0 -1264
  50. 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 entity materialization/retirement plus hierarchy orchestration. Component
4
- // storage remains in WorldComponentAccess; this module composes its typed
5
- // relationship mutations into public lifecycle behavior.
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, isRetiredSlot, ok, pack, type Result } from '@forgeax/engine-types';
8
- import type { Component, ComponentSchema, InputShapeOf, ShapeOf } from './component';
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 { validateManagedArrayValues, validateSharedFieldValues } from './component-value-validate';
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, relationshipSource } from './relationship-index';
29
- import { type Archetype, appendArchetypeRow, removeArchetypeRow } from './storage/archetype';
18
+ import { relationshipRole } from './relationship-index';
19
+ import type { Archetype } from './storage/archetype';
30
20
  import type { ArchetypeGraph } from './storage/archetype-graph';
31
- import { getOrCreateArchetype, getTable } from './storage/archetype-graph';
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` with a relationship reentry guard.
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: { component: Component; data: Partial<Record<string, unknown>> }[],
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].writeEntitySelf(arch, tableRow, spawnedEntity);
119
- world[worldInternal].markComponentsAdded(spawnedEntity, [
120
- componentId(Entity),
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
- const role = relationshipRole(holderComp);
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 parentRecord = world[worldInternal].getRecords()[parentSlot];
244
- if (!world[worldInternal].recordIsLive(parentRecord, parentGeneration)) {
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 && !(role?.kind === 'source' && role.allowSelf)) {
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?.kind === 'source' && role.allowSelf
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((candidate) => componentId(candidate) === componentId(holderComp))
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
+ }
@@ -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
- type InternalName =
19
- | 'addComponentCore'
20
- | 'allocateIndex'
21
- | 'allocatePendingEntity'
22
- | 'cancelPendingEntity'
23
- | 'despawnCore'
24
- | 'getArrayView'
25
- | 'getArrayLength'
26
- | 'getArrayElement'
27
- | 'getFieldValue'
28
- | 'getBufferPool'
29
- | 'getClockWriter'
30
- | 'getComponentChange'
31
- | 'getComponentMutationEpoch'
32
- | 'getComponentMutationEpochs'
33
- | 'getEntityArchetype'
34
- | 'getFixedAccumulator'
35
- | 'getFreeIndices'
36
- | 'getGraph'
37
- | 'getMutationEpoch'
38
- | 'getQueryRow'
39
- | 'getRecords'
40
- | 'getRelationshipEpoch'
41
- | 'getRelationshipTargetEntities'
42
- | 'getResources'
43
- | 'getSchedule'
44
- | 'getSchedules'
45
- | 'getSharedRefs'
46
- | 'getStructureEpoch'
47
- | 'getStructuralEvidence'
48
- | 'getUniqueRefs'
49
- | 'lookupAlive'
50
- | 'markComponentAdded'
51
- | 'markComponentChanged'
52
- | 'markComponentRangeChanged'
53
- | 'markComponentsAdded'
54
- | 'markStructureChanged'
55
- | 'materializePendingEntity'
56
- | 'nextMutationEpoch'
57
- | 'poisonExecution'
58
- | 'publishDerivedRange'
59
- | 'prepareRelationshipInsert'
60
- | 'preflightComponentData'
61
- | 'readRow'
62
- | 'recordStructuralEvidence'
63
- | 'recordIsLive'
64
- | 'relationshipOnInsert'
65
- | 'relationshipOnRemove'
66
- | 'releaseManagedRefsOnRow'
67
- | 'releaseRelationshipPreparation'
68
- | 'removeComponentCore'
69
- | 'routeError'
70
- | 'restoreMutationEpoch'
71
- | 'setFixedAccumulator'
72
- | 'setQueryRow'
73
- | 'spawnCore'
74
- | 'writeEntitySelf'
75
- | 'writeRow';
76
-
77
- export type WorldInternal = {
78
- // biome-ignore lint/suspicious/noExplicitAny: this closed package-internal seam preserves each method's existing inferred result type.
79
- readonly [K in InternalName]: (...args: any[]) => any;
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
+ }
@@ -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
- }