@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
package/dist/world.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Handle, Result } from '@forgeax/engine-types';
|
|
2
2
|
import { type Component, ComponentCatalog, type ComponentSchema, type InputShapeOf, type ShapeOf } from './component';
|
|
3
3
|
import { type EntityHandle } from './entity-handle';
|
|
4
|
-
import type { CommandFailedError,
|
|
5
|
-
import { ChangeEpochExhaustedError, RelationshipTargetReadonlyError } from './errors';
|
|
4
|
+
import type { CommandFailedError, ComponentFieldInvalidValueError, ComponentNotDefinedError, ComponentNumericValueInvalidError, DerivedRangeOutOfBoundsError, ManagedArrayInvalidValueError, ManagedBufferShrinkNotSupportedError, RelationshipDetachMismatchError, RelationshipMirrorComponentNotRegisteredError, RelationshipMirrorFieldTypeMismatchError, ScheduleMutationError, ScheduleScopeMismatchError, SharedKernelEligibilityError, SharedKernelFailureError, SystemFailedError, SystemSetNotRegisteredError, TimeConfigInvalidError, TimeDeltaInvalidError, UniqueRefDoubleReleaseError, UniqueRefReleasedError } from './errors';
|
|
5
|
+
import { ChangeEpochExhaustedError, ComponentAlreadyPresentError, ComponentNotPresentError, FixedSizeMismatchError, ManagedBufferOutOfBoundsError, RelationshipSelfCycleError, RelationshipTargetReadonlyError, RemoveEssentialComponentError, StaleEntityError, WorldPoisonedError } from './errors';
|
|
6
6
|
import { type WorldExecutionState } from './execution/shared-kernel';
|
|
7
7
|
import type { QueryDescriptor } from './query/query';
|
|
8
8
|
import { type Query, type QueryCreationError } from './query/query';
|
|
@@ -11,7 +11,7 @@ import { type SystemDescriptor, type SystemSet } from './schedule';
|
|
|
11
11
|
import type { SharedRefStore } from './shared-ref-store';
|
|
12
12
|
import { type ChangeTicks } from './storage/change-detection';
|
|
13
13
|
import { type WorldOptions } from './time';
|
|
14
|
-
import { type
|
|
14
|
+
import { type WorldRead, worldRead } from './world-read';
|
|
15
15
|
/**
|
|
16
16
|
* Union of all EcsError types that World methods can return via Result.
|
|
17
17
|
* AI users: switch on `.code` for programmatic branching.
|
|
@@ -144,8 +144,10 @@ export interface WorldScheduleData {
|
|
|
144
144
|
* permanently (it is never pushed back to `freeIndices`). The single liveness
|
|
145
145
|
* predicate is therefore "handle gen matches AND archetypeId !== -1" -- see
|
|
146
146
|
* `World.recordIsLive`. A deferred-spawn allocation is "pending" when
|
|
147
|
-
* archetypeId === -1 (not yet materialized into an archetype row)
|
|
148
|
-
*
|
|
147
|
+
* archetypeId === -1 (not yet materialized into an archetype row). During a
|
|
148
|
+
* failed append, `archetypeId` is reserved before the first storage write and
|
|
149
|
+
* `archetypeRow` remains `-1`; that marker prevents command abort from
|
|
150
|
+
* reclaiming a reservation that may already have touched table storage.
|
|
149
151
|
*/
|
|
150
152
|
export interface EntityRecord {
|
|
151
153
|
generation: number;
|
|
@@ -153,37 +155,32 @@ export interface EntityRecord {
|
|
|
153
155
|
archetypeRow: number;
|
|
154
156
|
}
|
|
155
157
|
/**
|
|
156
|
-
* The World owns
|
|
157
|
-
*
|
|
158
|
-
* - all archetypes (via ArchetypeGraph);
|
|
159
|
-
* - the entity index table (records by index slot);
|
|
160
|
-
* - the free-list of recyclable entity slots.
|
|
158
|
+
* The World owns the component registry, archetype graph, entity index table,
|
|
159
|
+
* and recyclable free-list.
|
|
161
160
|
*/
|
|
162
161
|
export declare class World {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
private readonly
|
|
162
|
+
readonly [worldRead]: WorldRead;
|
|
163
|
+
/** World-local state is kept together so one owner closes each mutation. */
|
|
164
|
+
private executionState;
|
|
165
|
+
private readonly graph;
|
|
166
|
+
private readonly records;
|
|
167
|
+
private readonly freeIndices;
|
|
168
|
+
/** BufferPool for schema-declared variable buffers and arrays. */
|
|
169
|
+
private readonly bufferPool;
|
|
170
|
+
/** Per-World managed unique-ref store used by lifecycle mutations. */
|
|
171
|
+
private readonly uniqueRefs;
|
|
172
|
+
/** Per-World shared-ref store; public read-only for direct handle operations. */
|
|
173
|
+
readonly sharedRefs: SharedRefStore;
|
|
174
|
+
private readonly componentMutationEpochs;
|
|
175
|
+
private readonly structuralEvidence;
|
|
176
|
+
/** One packed reverse index per relationship source component. */
|
|
177
|
+
private readonly relationshipIndexes;
|
|
178
|
+
private mutationEpoch;
|
|
179
|
+
private structureEpoch;
|
|
180
|
+
/** Keep identity as a prototype getter; it is a diagnostic capability, not enumerable state. */
|
|
167
181
|
get identity(): string;
|
|
168
182
|
/** Plugin-owned component discovery scoped to this World and removed through leases. */
|
|
169
183
|
readonly components: ComponentCatalog;
|
|
170
|
-
private executionState;
|
|
171
|
-
/** Monotonic clock advanced exactly once per successful mutation. */
|
|
172
|
-
/** Monotonic revision for successful entity/component structure writes. */
|
|
173
|
-
/** Last mutation epoch for each component id, used by component-owned projections. */
|
|
174
|
-
/** Ordered, bounded evidence consumed by persistent engine-owned projections. */
|
|
175
|
-
private get mutationEpoch();
|
|
176
|
-
private set mutationEpoch(value);
|
|
177
|
-
private get structureEpoch();
|
|
178
|
-
private set structureEpoch(value);
|
|
179
|
-
private get componentMutationEpochs();
|
|
180
|
-
/** Free index slots (LIFO stack). */
|
|
181
|
-
private get records();
|
|
182
|
-
private get freeIndices();
|
|
183
|
-
/**
|
|
184
|
-
* Relationship-sync reentry guard (feat-20260531 M2 / plan-strategy D-7).
|
|
185
|
-
/** The archetype graph: manages all archetypes + edge caching. */
|
|
186
|
-
private get graph();
|
|
187
184
|
/** DAG schedules for the two built-in execution scopes. */
|
|
188
185
|
private readonly schedules;
|
|
189
186
|
/** Resource store: typed key-value global singletons. */
|
|
@@ -191,56 +188,23 @@ export declare class World {
|
|
|
191
188
|
private readonly clock;
|
|
192
189
|
/** Remainder carried between fixed-step runs. */
|
|
193
190
|
private fixedAccumulator;
|
|
194
|
-
/**
|
|
195
|
-
* ECS-managed handle store (M1). Owned by the World - constructed eagerly
|
|
196
|
-
* so every spawn / despawn / set path can dispatch managed-ref releases
|
|
197
|
-
* without caller-side wiring. AI users obtain `Handle<T,'unique'>` values
|
|
198
|
-
* by accessing the store through internal channels (the surface is
|
|
199
|
-
* private; managed-ref-bearing fields read through `world.get`).
|
|
200
|
-
*/
|
|
201
|
-
private get uniqueRefs();
|
|
202
|
-
/**
|
|
203
|
-
* Per-World `SharedRefStore` (feat-20260614 M3). Backs every `shared<T>`
|
|
204
|
-
* schema field + the `world.allocSharedRef` facade. Public read-only so AI
|
|
205
|
-
* users can `retain` / `release` / `resolve` user-tier handles directly off
|
|
206
|
-
* the world (the surface is small enough that hiding it behind another
|
|
207
|
-
* facade would be a phantom indirection - charter F1 single-entry
|
|
208
|
-
* indexability).
|
|
209
|
-
*
|
|
210
|
-
* Final release publishes structured evidence; there is no callback surface.
|
|
211
|
-
* M6 D-15: the store manages only user-tier slots
|
|
212
|
-
* (`>= BUILTIN_BASE`); builtin handles are process-static in their
|
|
213
|
-
* authoring package and never reference-counted.
|
|
214
|
-
*/
|
|
215
|
-
get sharedRefs(): SharedRefStore;
|
|
216
|
-
/**
|
|
217
|
-
* BufferPool backing every `buffer:<N>` schema-vocab field (M2). Eagerly
|
|
218
|
-
* constructed (per-World, D-2). `spawn` allocs slots for buffer fields and
|
|
219
|
-
* stores the slot id in the u32 column; `despawn` / `removeComponent`
|
|
220
|
-
* release the slots; `set(e, C, { field: Uint8Array })` copies bytes into
|
|
221
|
-
* the live view without re-allocating (schema-declared byteLength is
|
|
222
|
-
* fixed in v1; runtime grow is reserved for the M4 carry-over path).
|
|
223
|
-
*/
|
|
224
|
-
private get bufferPool();
|
|
225
|
-
private readonly componentAccess;
|
|
226
191
|
constructor(options?: WorldOptions);
|
|
227
192
|
/** Immutable integrity state for execution coordinators and headless callers. */
|
|
228
193
|
get execution(): WorldExecutionState;
|
|
229
194
|
/** Resolve a schedule token owned by this World realm without package singleton identity. */
|
|
230
195
|
scheduleToken(name: import('./schedule-token').ScheduleName): import('./schedule-token').ScheduleToken;
|
|
231
|
-
/**
|
|
196
|
+
/** Seal the first execution fault; application code recovers with a new World. */
|
|
232
197
|
private internalpoisonExecution;
|
|
198
|
+
/**
|
|
199
|
+
* A poisoned identity is diagnostic evidence, not a mutable recovery path.
|
|
200
|
+
* Public entity mutation therefore returns the same structured fence as
|
|
201
|
+
* `update()` instead of allocating a new reservation or touching a partial
|
|
202
|
+
* row. Recovery remains construction of a fresh World.
|
|
203
|
+
*/
|
|
204
|
+
private poisonedResult;
|
|
233
205
|
query<const R extends readonly Component[] = readonly [], const W extends readonly Component[] = readonly [], const O extends readonly Component[] = readonly []>(descriptor: QueryDescriptor<R, W, O>): Result<Query<R, W, O>, QueryCreationError>;
|
|
234
|
-
/** Expose archetype graph for query engine. Not part of public API. */
|
|
235
|
-
private internalgetGraph;
|
|
236
206
|
private componentIsInUse;
|
|
237
|
-
|
|
238
|
-
private internalgetMutationEpoch;
|
|
239
|
-
/** Structure snapshot used to invalidate borrowed query facades. */
|
|
240
|
-
private internalgetStructureEpoch;
|
|
241
|
-
/** Typed structural facts are produced by this World and consumed by projections. */
|
|
242
|
-
private internalgetStructuralEvidence;
|
|
243
|
-
private internalrecordStructuralEvidence;
|
|
207
|
+
private recordStructuralEvidence;
|
|
244
208
|
/** Resolve current logical identity for a packed entity handle. */
|
|
245
209
|
private internalgetEntityArchetype;
|
|
246
210
|
/** Component change state for query filters. */
|
|
@@ -250,55 +214,21 @@ export declare class World {
|
|
|
250
214
|
private internalrestoreMutationEpoch;
|
|
251
215
|
private internalpublishDerivedRange;
|
|
252
216
|
/** Record one successful structural mutation. */
|
|
253
|
-
private
|
|
217
|
+
private advanceStructureEpoch;
|
|
254
218
|
/** Current structural revision for mounted World projections. */
|
|
255
219
|
getStructureEpoch(): number;
|
|
256
|
-
/** Mark a component as both added and changed at the current tick. */
|
|
257
|
-
private internalmarkComponentAdded;
|
|
258
220
|
/** Mark one mutation's component instances with a shared epoch. */
|
|
259
221
|
private internalmarkComponentsAdded;
|
|
260
222
|
/** Mark an existing component as changed at the current tick. */
|
|
261
223
|
private internalmarkComponentChanged;
|
|
262
224
|
/** Mark one contiguous component range with a single epoch. */
|
|
263
225
|
private internalmarkComponentRangeChanged;
|
|
264
|
-
/** Latest mutation token for one component-owned projection. */
|
|
265
|
-
private internalgetComponentMutationEpoch;
|
|
266
|
-
/** Borrow the component-version summary used to skip unrelated value columns. */
|
|
267
|
-
private internalgetComponentMutationEpochs;
|
|
268
|
-
/** Read a materialized relationship target in O(1 + k). */
|
|
269
|
-
private internalgetRelationshipTargetEntities;
|
|
270
|
-
/** Monotonic epoch for the materialized relationship index. */
|
|
271
|
-
private internalgetRelationshipEpoch;
|
|
272
226
|
/** Query facade write after the facade has already marked evidence. */
|
|
273
227
|
private internalsetQueryRow;
|
|
274
|
-
/** Query facade read that does not re-enter the public World API. */
|
|
275
|
-
private internalgetQueryRow;
|
|
276
228
|
/** Return resource change ticks for diagnostics and resource-driven systems. */
|
|
277
229
|
getResourceChange(name: string): ChangeTicks | undefined;
|
|
278
|
-
/**
|
|
279
|
-
|
|
280
|
-
* an engine-internal subsystem (e.g. RenderSystem extract stage, w15).
|
|
281
|
-
*
|
|
282
|
-
* Mirrors the private `errorHandler(err, ctx)` call sites inside `World`
|
|
283
|
-
* itself; the dedicated accessor avoids exposing `errorHandler` directly
|
|
284
|
-
* and keeps the routing contract under the `_xxx` `@internal` umbrella so
|
|
285
|
-
* AI users do not discover it through IDE autocomplete on `World`.
|
|
286
|
-
*
|
|
287
|
-
* Not part of the public API.
|
|
288
|
-
*/
|
|
289
|
-
private internalrouteError;
|
|
290
|
-
/** */ private internalgetRecords;
|
|
291
|
-
/** */ private internalgetFreeIndices;
|
|
292
|
-
/** */ private internalgetResources;
|
|
293
|
-
/** */ private internalgetFixedAccumulator;
|
|
294
|
-
/** */ private internalsetFixedAccumulator;
|
|
295
|
-
/** */ private internalgetUniqueRefs;
|
|
296
|
-
/** */ private internalgetBufferPool;
|
|
297
|
-
/** Scheduler-owned mutable clock capability. */
|
|
298
|
-
private internalgetClockWriter;
|
|
299
|
-
/** */ private internalgetSchedule;
|
|
300
|
-
/** */ private internalgetSchedules;
|
|
301
|
-
/** */ private internalgetSharedRefs;
|
|
230
|
+
/** Route an expected internal failure through the host-owned error channel. */
|
|
231
|
+
private routeError;
|
|
302
232
|
/**
|
|
303
233
|
* Register a system with query descriptor and optional ordering constraints.
|
|
304
234
|
*
|
|
@@ -516,45 +446,326 @@ export declare class World {
|
|
|
516
446
|
internSharedRef<Target extends string, T extends object>(target: Target, payload: T): Handle<Target, 'shared'>;
|
|
517
447
|
private relationshipTargetWriteError;
|
|
518
448
|
private relationshipTargetPayloadWrites;
|
|
449
|
+
/** Test live component presence without constructing a Result error. */
|
|
450
|
+
hasComponent(entity: EntityHandle, component: Component): boolean;
|
|
451
|
+
private table;
|
|
452
|
+
private tableRow;
|
|
453
|
+
private markComponentChanged;
|
|
454
|
+
private relationshipIndex;
|
|
455
|
+
/** Read the World-owned materialized target array; never consults a shadow list. */
|
|
456
|
+
private relationshipTargetEntries;
|
|
457
|
+
/** Read a relationship target length without materialising its array view. */
|
|
458
|
+
private relationshipTargetLength;
|
|
459
|
+
private relationshipTargetEntity;
|
|
460
|
+
private preflightComponentFieldValues;
|
|
461
|
+
/**
|
|
462
|
+
* Validate one structural component payload without touching archetypes,
|
|
463
|
+
* columns, relationship mirrors, epochs, or managed-reference stores.
|
|
464
|
+
* CommandBuffer uses this same owner-level gate as the direct World facade;
|
|
465
|
+
* the optional pending set lets a batch refer to an entity reserved earlier
|
|
466
|
+
* in that batch without mistaking it for a stale live handle.
|
|
467
|
+
*/
|
|
468
|
+
private preflightComponentData;
|
|
469
|
+
private relationshipCycleHit;
|
|
470
|
+
/**
|
|
471
|
+
* Commit a relationship source through its owner-specific write path.
|
|
472
|
+
* Relationship sources have one entity field, so dispatching before the
|
|
473
|
+
* generic field loop avoids paying the ordinary component-field traversal on
|
|
474
|
+
* every hierarchy reparent while keeping mirror/index publication here.
|
|
475
|
+
*/
|
|
476
|
+
private setRelationshipSource;
|
|
477
|
+
/** Prepare the target side before a source archetype mutation commits. */
|
|
478
|
+
private prepareRelationshipInsert;
|
|
479
|
+
/** Append `holder` to the materialized target list. */
|
|
480
|
+
private relationshipOnInsert;
|
|
481
|
+
/** Remove `holder` from the materialized target list. */
|
|
482
|
+
private relationshipOnRemove;
|
|
483
|
+
private linkedSpawnMirrorField;
|
|
484
|
+
private relationshipLinkedSpawnChildren;
|
|
485
|
+
/**
|
|
486
|
+
* Read component data from an entity.
|
|
487
|
+
*
|
|
488
|
+
* **Transient view contract (feat-20260602):** for fixed-capacity
|
|
489
|
+
* `array<T,N>` and `buffer<N>` fields, the returned `TypedArray` (and any
|
|
490
|
+
* subarray of it) aliases the archetype column buffer directly. The view is
|
|
491
|
+
* valid only until the next structural change (`spawn` / `despawn` /
|
|
492
|
+
* `addComponent` / `removeComponent`). Holding a view across a structural
|
|
493
|
+
* change is undefined behaviour -- the backing `ArrayBuffer` is detached on
|
|
494
|
+
* column growth, and swap-remove at the same row index points to the wrong
|
|
495
|
+
* entity. **Re-fetch `world.get(e, C)` on every access.** See
|
|
496
|
+
* `packages/ecs/README.md` Transient view contract section.
|
|
497
|
+
*
|
|
498
|
+
* @returns `Result<ShapeOf<S>, EcsError>` —
|
|
499
|
+
* `ok(ShapeOf<S>)` on success;
|
|
500
|
+
* `err(StaleEntityError)` (`.code = 'stale-entity'`) if entity is dead;
|
|
501
|
+
* `err(ComponentNotPresentError)` (`.code = 'component-not-present'`) if
|
|
502
|
+
* the entity does not have the component (a never-present component on
|
|
503
|
+
* this entity degrades to the same `component-not-present` path — there is
|
|
504
|
+
* no separate "not registered" failure; components are global at
|
|
505
|
+
* `defineComponent` time).
|
|
506
|
+
*
|
|
507
|
+
* @example
|
|
508
|
+
* ```ts
|
|
509
|
+
* const Position = defineComponent('Position', { x: 'f32', y: 'f32' });
|
|
510
|
+
* const world = new World();
|
|
511
|
+
* const e = world.spawn({ component: Position, data: { x: 1, y: 2 } }).unwrap();
|
|
512
|
+
* const r = world.get(e, Position);
|
|
513
|
+
* if (!r.ok) { return; } // r.error.code === 'stale-entity' on dead handle
|
|
514
|
+
* const pos = r.value;
|
|
515
|
+
* ```
|
|
516
|
+
*/
|
|
519
517
|
get<S extends ComponentSchema>(entity: EntityHandle, component: Component<string, S>): Result<ShapeOf<S>, EcsError>;
|
|
520
518
|
/**
|
|
521
|
-
*
|
|
519
|
+
* Column-level zero-copy view of an `array<T, N>` / `array<T>` field.
|
|
520
|
+
*
|
|
521
|
+
* Resolves the live byte region for `(entity, component, fieldName)`
|
|
522
|
+
* directly at the column level and returns the element-typed TypedArray
|
|
523
|
+
* aliasing it (`view.buffer` is the SSOT byte region; mutations route
|
|
524
|
+
* through `world.set`). Unlike `get`, this does NOT build the
|
|
525
|
+
* `{}` whole-component object nor walk every schema field. Per-frame
|
|
526
|
+
* consumers that need one column (the resolved world mat4) take this path to
|
|
527
|
+
* avoid the `get` overhead (1 `{}` alloc + N-field readRow walk).
|
|
528
|
+
*
|
|
529
|
+
* Fixed `array<T,N>` columns (feat-20260602) store their elements inline, so
|
|
530
|
+
* the view aliases the archetype column buffer directly (no BufferPool
|
|
531
|
+
* indirection); variable `array<T>` columns still alias the BufferPool slot.
|
|
532
|
+
* The returned view's element type follows the schema element type
|
|
533
|
+
* (`array<entity,N>` -> `Uint32Array`, `array<f32,N>` -> `Float32Array`,
|
|
534
|
+
* etc.) -- the prior f32-only early-return gate is removed.
|
|
535
|
+
*
|
|
536
|
+
* **Transient view contract:** the returned `TypedArray` aliases the column
|
|
537
|
+
* buffer and is valid only until the next structural change (`spawn` /
|
|
538
|
+
* `despawn` / `addComponent` / `removeComponent`). Column growth
|
|
539
|
+
* (`growColumn`) detaches the old `ArrayBuffer` via `transfer()`; a
|
|
540
|
+
* swap-remove at the same row index leaves the view pointing to the wrong
|
|
541
|
+
* entity. **Callers must re-fetch `getArrayView` on every access** and must
|
|
542
|
+
* not hold the view across any operation that may cause archetype migration.
|
|
543
|
+
* All existing per-frame consumers (`propagateTransforms` / `render-extract`
|
|
544
|
+
* / `pick`) already conform -- they fetch the view inside a single pass with
|
|
545
|
+
* no intervening structural changes.
|
|
546
|
+
*
|
|
547
|
+
* Returns `undefined` when the entity is dead, the component is absent, the
|
|
548
|
+
* field does not exist, or the field is not an `array<...>` column.
|
|
549
|
+
*
|
|
550
|
+
* Engine-internal fast path; AI users read the typed view through
|
|
551
|
+
* `world.get(e, GlobalTransform).world`. The accessor is the zero-materialization
|
|
552
|
+
* route the propagate kernel and render walk use.
|
|
553
|
+
*/
|
|
554
|
+
private getArrayView;
|
|
555
|
+
/**
|
|
556
|
+
* Write (partial) component data to an entity.
|
|
557
|
+
*
|
|
558
|
+
* @returns `Result<void, EcsError>` —
|
|
559
|
+
* `ok(void)` on success;
|
|
560
|
+
* `err(StaleEntityError)` (`.code = 'stale-entity'`) if entity is dead;
|
|
561
|
+
* `err(ComponentNotPresentError)` (`.code = 'component-not-present'`) if
|
|
562
|
+
* entity does not have the component (F-02: no longer silently ignores).
|
|
563
|
+
*
|
|
564
|
+
* @example
|
|
565
|
+
* ```ts
|
|
566
|
+
* const Position = defineComponent('Position', { x: 'f32', y: 'f32' });
|
|
567
|
+
* const world = new World();
|
|
568
|
+
* const e = world.spawn({ component: Position, data: { x: 0, y: 0 } }).unwrap();
|
|
569
|
+
* const r = world.set(e, Position, { x: 10 });
|
|
570
|
+
* if (!r.ok) { return; } // r.error.code === 'stale-entity' on dead handle
|
|
571
|
+
* r.unwrap();
|
|
572
|
+
* ```
|
|
573
|
+
*/
|
|
574
|
+
set<S extends ComponentSchema, C extends Component<string, S>>(entity: EntityHandle, component: C & WritableComponent<C>, value: Partial<InputShapeOf<S>>, markChanged?: boolean): Result<void, EcsError>;
|
|
575
|
+
/**
|
|
576
|
+
* Append `value` to the variable `array<T>` field `fieldName` on `entity`.
|
|
522
577
|
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
578
|
+
* BufferPool grow is amortized O(1) via the size-class freelist (research
|
|
579
|
+
* Finding 5). Relationship target arrays grow byte-wise.
|
|
580
|
+
*
|
|
581
|
+
* @returns `Result<void, EcsError>` with the normal stale/component errors.
|
|
582
|
+
*
|
|
583
|
+
* The helper is called only by relationship synchronization.
|
|
584
|
+
*/
|
|
585
|
+
private appendArrayElement;
|
|
586
|
+
private ensureArrayCapacity;
|
|
587
|
+
/**
|
|
588
|
+
* Remove one variable-array element at a known slot. Relationship holders
|
|
589
|
+
* supply the slot from their backpointer, so this is O(1) and never scans
|
|
590
|
+
* the materialized target array.
|
|
591
|
+
*/
|
|
592
|
+
private removeArrayElementAt;
|
|
593
|
+
/**
|
|
594
|
+
* Add a component to an existing entity, triggering archetype migration.
|
|
595
|
+
*
|
|
596
|
+
* @returns `Result<void, EcsError>` —
|
|
597
|
+
* `ok(void)` on success;
|
|
598
|
+
* `err(StaleEntityError)` (`.code = 'stale-entity'`) if entity is dead;
|
|
599
|
+
* `err(ComponentAlreadyPresentError)` (`.code = 'component-already-present'`)
|
|
600
|
+
* if entity already has the component (E-03).
|
|
601
|
+
*
|
|
602
|
+
* @example
|
|
603
|
+
* ```ts
|
|
604
|
+
* const Position = defineComponent('Position', { x: 'f32', y: 'f32' });
|
|
605
|
+
* const Velocity = defineComponent('Velocity', { dx: 'f32', dy: 'f32' });
|
|
606
|
+
* const world = new World();
|
|
607
|
+
* const e = world.spawn({ component: Position, data: { x: 0, y: 0 } }).unwrap();
|
|
608
|
+
* const r = world.addComponent(e, { component: Velocity, data: { dx: 1, dy: 0 } });
|
|
609
|
+
* if (!r.ok) { return; } // r.error.code === 'stale-entity' on dead handle
|
|
610
|
+
* r.unwrap();
|
|
611
|
+
* ```
|
|
529
612
|
*/
|
|
530
|
-
hasComponent(entity: EntityHandle, component: Component): boolean;
|
|
531
|
-
private internalgetArrayView;
|
|
532
|
-
private internalgetArrayLength;
|
|
533
|
-
private internalgetArrayElement;
|
|
534
|
-
private internalgetFieldValue;
|
|
535
|
-
set<S extends ComponentSchema, C extends Component<string, S>>(entity: EntityHandle, component: C & WritableComponent<C>, value: Partial<InputShapeOf<S>>): Result<void, EcsError>;
|
|
536
613
|
addComponent<S extends ComponentSchema, C extends Component<string, S>>(entity: EntityHandle, componentData: ComponentData<S> & {
|
|
537
614
|
component: C & WritableComponent<C>;
|
|
538
615
|
}): Result<void, EcsError>;
|
|
539
|
-
|
|
616
|
+
/**
|
|
617
|
+
* Core implementation of `addComponent` with reentry guard.
|
|
618
|
+
*
|
|
619
|
+
* @param internal — `true` when called from relationship maintenance
|
|
620
|
+
* (lazy mirror create or exclusive reparent).
|
|
621
|
+
*/
|
|
622
|
+
private addComponentCore;
|
|
623
|
+
/**
|
|
624
|
+
* Remove a component from an existing entity, triggering archetype migration.
|
|
625
|
+
*
|
|
626
|
+
* @returns `Result<void, EcsError>` —
|
|
627
|
+
* `ok(void)` on success;
|
|
628
|
+
* `err(StaleEntityError)` (`.code = 'stale-entity'`) if entity is dead;
|
|
629
|
+
* `err(ComponentNotPresentError)` (`.code = 'component-not-present'`)
|
|
630
|
+
* if entity doesn't have the component (E-04).
|
|
631
|
+
*
|
|
632
|
+
* @example
|
|
633
|
+
* ```ts
|
|
634
|
+
* const Position = defineComponent('Position', { x: 'f32', y: 'f32' });
|
|
635
|
+
* const world = new World();
|
|
636
|
+
* const e = world.spawn({ component: Position, data: { x: 0, y: 0 } }).unwrap();
|
|
637
|
+
* const r = world.removeComponent(e, Position);
|
|
638
|
+
* if (!r.ok) { return; } // r.error.code === 'stale-entity' on dead handle
|
|
639
|
+
* r.unwrap();
|
|
640
|
+
* ```
|
|
641
|
+
*/
|
|
540
642
|
removeComponent<S extends ComponentSchema, C extends Component<string, S>>(entity: EntityHandle, component: C & WritableComponent<C>): Result<void, EcsError>;
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
/**
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
/**
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
643
|
+
/**
|
|
644
|
+
* Core implementation of `removeComponent` with reentry guard.
|
|
645
|
+
*
|
|
646
|
+
* @param internal — `true` when called from relationship maintenance
|
|
647
|
+
* (exclusive reparent).
|
|
648
|
+
*/
|
|
649
|
+
private removeComponentCore;
|
|
650
|
+
/**
|
|
651
|
+
* Allocate a pending entity for deferred spawn.
|
|
652
|
+
* Returns an Entity handle. The entity is "pending" because
|
|
653
|
+
* archetypeId === -1 (set by allocateIndex); no separate flag needed.
|
|
654
|
+
*/
|
|
655
|
+
private allocatePendingEntity;
|
|
656
|
+
/**
|
|
657
|
+
* Return a deferred-spawn reservation to the free-list without publishing a
|
|
658
|
+
* row or advancing an epoch. CommandBuffer.abort is the sole caller; a
|
|
659
|
+
* materialized entity is intentionally left untouched so an unexpected
|
|
660
|
+
* post-write failure poisons the World instead of attempting an unsafe undo.
|
|
661
|
+
*/
|
|
662
|
+
private cancelPendingEntity;
|
|
663
|
+
/**
|
|
664
|
+
* Materialize one already-reserved entity. Synchronous spawn and deferred
|
|
665
|
+
* command flush share this exact insertion/publication path; only the
|
|
666
|
+
* caller's validation and reservation boundary differs.
|
|
667
|
+
*
|
|
668
|
+
*/
|
|
669
|
+
private materializeEntity;
|
|
670
|
+
private poisonAfterEntityMutation;
|
|
671
|
+
/** Deferred commands use the common materialization owner. */
|
|
672
|
+
private materializePendingEntity;
|
|
673
|
+
private allocateIndex;
|
|
674
|
+
/**
|
|
675
|
+
* Single liveness predicate (feat-20260602 / plan-strategy D-4): a slot is
|
|
676
|
+
* live for a given handle generation iff the record exists, its generation
|
|
677
|
+
* still matches the handle (despawn bumps generation, so a stale or recycled
|
|
678
|
+
* handle fails here), and the slot is materialized into an archetype
|
|
679
|
+
* (archetypeId !== -1). Replaces the former `record.alive && record.generation
|
|
680
|
+
* === gen` conjunction and the intermediate `!record.pending` clause. An
|
|
681
|
+
* append in progress keeps `archetypeRow === -1` until both storage indexes
|
|
682
|
+
* exist.
|
|
683
|
+
*/
|
|
684
|
+
private recordIsLive;
|
|
685
|
+
private lookupAlive;
|
|
686
|
+
private readArrayView;
|
|
687
|
+
/** Read one scalar column without constructing a component snapshot. */
|
|
688
|
+
private internalgetFieldValue;
|
|
689
|
+
/** Read an array's live logical length without allocating a TypedArray. */
|
|
690
|
+
private internalgetArrayLength;
|
|
691
|
+
/** Read one array element directly from its column or BufferPool slot. */
|
|
692
|
+
private internalgetArrayElement;
|
|
693
|
+
private readRow;
|
|
694
|
+
/**
|
|
695
|
+
* Write the full packed entity handle into the row's essential id=0 `Entity`
|
|
696
|
+
* column (`self` field). Called by `spawn` / `materializePendingEntity`
|
|
697
|
+
* after the row is appended (feat-20260602 / plan-strategy D-3). The column
|
|
698
|
+
* always exists -- `createArchetype` folds the Entity column into every
|
|
699
|
+
* archetype -- so this is a direct u32 store, no readRow/writeRow walk.
|
|
700
|
+
*/
|
|
701
|
+
private writeEntitySelf;
|
|
702
|
+
private writeRow;
|
|
703
|
+
/** Release all ECS-owned field handles before a row is removed or overwritten. */
|
|
704
|
+
private releaseManagedRefsOnRow;
|
|
705
|
+
/**
|
|
706
|
+
* Release one ECS-owned field according to its schema. Inline buffers have
|
|
707
|
+
* no pool slot; shared-array elements still release their handles. Store
|
|
708
|
+
* failures use the host error channel so row cleanup remains total.
|
|
709
|
+
*/
|
|
710
|
+
private releaseManagedFieldOnRow;
|
|
711
|
+
/** Release one unique-ref handle; sentinel 0 is ignored. */
|
|
712
|
+
private releaseManagedRefHandle;
|
|
713
|
+
/** Release one shared-ref handle, preserving builtin slots and refcounts. */
|
|
714
|
+
private releaseSharedRefHandle;
|
|
715
|
+
/** Retain one shared-ref scalar handle for a World-owned field. */
|
|
716
|
+
private retainSharedScalarHandle;
|
|
717
|
+
/** Release one variable-buffer slot; id 0 is the unallocated sentinel. */
|
|
718
|
+
private releaseManagedBufferSlot;
|
|
719
|
+
/**
|
|
720
|
+
* Attach field context to a `ManagedArrayErrorEnvelope`.
|
|
721
|
+
* The envelope shape (`code / hint / expected / detail`) already mirrors
|
|
722
|
+
* the EcsError contract; this helper only attaches the systemName context
|
|
723
|
+
* so AI users can correlate the error with the holder component / field.
|
|
724
|
+
*/
|
|
725
|
+
private routeArrayError;
|
|
726
|
+
/**
|
|
727
|
+
* Write an array field for spawn or set. Fixed arrays stay inline; variable
|
|
728
|
+
* arrays use one BufferPool slot plus a live-count sidecar.
|
|
729
|
+
*/
|
|
730
|
+
private writeArrayField;
|
|
731
|
+
/**
|
|
732
|
+
* Walk the first `count` u32 handles in `bytes` and call
|
|
733
|
+
* `SharedRefStore.retain` on each non-sentinel slot id (feat-20260614 M4 /
|
|
734
|
+
* D-3). Failures route via the error channel so the write chain stays
|
|
735
|
+
* total; charter explicit-failure boundary lets AI users see structured
|
|
736
|
+
* `shared-ref-released` payloads when retaining a stale handle.
|
|
737
|
+
*
|
|
738
|
+
* Helper-internal -- only called from `writeArrayField`'s `'shared'` arm.
|
|
739
|
+
*/
|
|
740
|
+
private retainSharedArrayElements;
|
|
741
|
+
/**
|
|
742
|
+
* Walk the first `count` u32 handles in `bytes` and call
|
|
743
|
+
* `SharedRefStore.release` on each non-sentinel slot id (feat-20260614 M4 /
|
|
744
|
+
* D-3). Mirrors `retainSharedArrayElements`; called from
|
|
745
|
+
* `releaseManagedFieldOnRow`'s array arm BEFORE the BufferPool slot is
|
|
746
|
+
* released so the underlying bytes are still valid.
|
|
747
|
+
*/
|
|
748
|
+
private releaseSharedArrayElements;
|
|
749
|
+
/**
|
|
750
|
+
* Materialize an array snapshot. Fixed arrays alias their inline column;
|
|
751
|
+
* variable arrays alias the live BufferPool slot and use the count sidecar.
|
|
752
|
+
* Both views are transient and must not be held across structural changes.
|
|
753
|
+
*/
|
|
754
|
+
private materializeArrayView;
|
|
755
|
+
/**
|
|
756
|
+
* Copy surviving component columns into the target row, then swap-remove
|
|
757
|
+
* the source row. Managed handles and variable-array sidecars are copied
|
|
758
|
+
* verbatim; release remains the responsibility of remove/despawn paths.
|
|
759
|
+
*/
|
|
760
|
+
private migrateEntity;
|
|
761
|
+
private moveEntityArchetype;
|
|
762
|
+
/**
|
|
763
|
+
* Retire one live entity and any linked-spawn descendants. The complete
|
|
764
|
+
* row/relationship/managed-data mutation stays on World so a failure after
|
|
765
|
+
* the first write can poison this identity instead of crossing an extraction
|
|
766
|
+
* owner boundary.
|
|
767
|
+
*/
|
|
768
|
+
private despawnEntity;
|
|
558
769
|
/**
|
|
559
770
|
* Spawn an entity with one or more components.
|
|
560
771
|
* Multi-component spawn directly targets the correct archetype (AC-06).
|
|
@@ -577,13 +788,6 @@ export declare class World {
|
|
|
577
788
|
data: Partial<InputShapeOf<SArr[K]>>;
|
|
578
789
|
};
|
|
579
790
|
}): Result<EntityHandle, EcsError>;
|
|
580
|
-
/**
|
|
581
|
-
* Core implementation of `spawn` with reentry guard.
|
|
582
|
-
*
|
|
583
|
-
* @param internal — `true` when called from relationship maintenance
|
|
584
|
-
* (lazy mirror create or exclusive reparent).
|
|
585
|
-
*/
|
|
586
|
-
private internalspawnCore;
|
|
587
791
|
/**
|
|
588
792
|
* Despawn an entity. Stale handles are silently ignored (E-01, AC-17).
|
|
589
793
|
* Generation retirement: gen=255 → index permanently retired (D-08/E-08).
|
|
@@ -602,15 +806,6 @@ export declare class World {
|
|
|
602
806
|
despawn(entity: EntityHandle): Result<void, EcsError>;
|
|
603
807
|
/** Despawn every live entity through the normal lifecycle and ref cleanup path. */
|
|
604
808
|
despawnAll(): Result<void, EcsError>;
|
|
605
|
-
/**
|
|
606
|
-
* Core implementation of `despawn` with reentry guard.
|
|
607
|
-
*
|
|
608
|
-
* @param internal — `true` when called from within linkedSpawn cascade.
|
|
609
|
-
* Nested despawn skips relationship pruning after the parent is retired;
|
|
610
|
-
* the linkedSpawn collection still walks the subtree so grandchildren
|
|
611
|
-
* cascade correctly (tweak-20260714 M2, R-6).
|
|
612
|
-
*/
|
|
613
|
-
private internaldespawnCore;
|
|
614
809
|
addChild<S extends ComponentSchema>(parent: EntityHandle, child: EntityHandle, component: Component<string, S>, data: Partial<InputShapeOf<S>>): Result<void, EcsError>;
|
|
615
810
|
removeChild<S extends ComponentSchema>(parent: EntityHandle, child: EntityHandle, component: Component<string, S>): Result<void, EcsError>;
|
|
616
811
|
reparent<S extends ComponentSchema>(child: EntityHandle, newParent: EntityHandle, component: Component<string, S>, data: Partial<InputShapeOf<S>>): Result<void, EcsError>;
|