@codehz/ecs 0.8.1 → 0.9.0
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.en.md +26 -3
- package/README.md +28 -3
- package/dist/builder.d.mts +296 -46
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +1 -1
- package/dist/world.mjs +452 -179
- package/dist/world.mjs.map +1 -1
- package/examples/debug-observability.ts +92 -0
- package/examples/inventory-system-relations.ts +1 -1
- package/examples/parent-child-hierarchy.ts +18 -38
- package/package.json +1 -1
- package/skills/ecs/SKILL.md +9 -4
- package/src/__tests__/component/singleton.test.ts +40 -1
- package/src/__tests__/core/archetype.test.ts +155 -13
- package/src/__tests__/core/bitset.test.ts +12 -0
- package/src/__tests__/entity/entity.test.ts +33 -0
- package/src/__tests__/entity/id-system.test.ts +40 -0
- package/src/__tests__/perf/comprehensive.perf.test.ts +6 -9
- package/src/__tests__/perf/serialization.perf.test.ts +242 -0
- package/src/__tests__/perf/{dontfragment-wildcard.perf.test.ts → sparse-wildcard.perf.test.ts} +13 -16
- package/src/__tests__/query/caching.test.ts +62 -0
- package/src/__tests__/query/filter.test.ts +16 -22
- package/src/__tests__/query/perf.test.ts +3 -5
- package/src/__tests__/relations/hierarchy.test.ts +208 -0
- package/src/__tests__/relations/{dont-fragment → sparse}/basic.test.ts +64 -69
- package/src/__tests__/relations/{dont-fragment → sparse}/query-notification.test.ts +17 -9
- package/src/__tests__/serialization/bounds.test.ts +134 -1
- package/src/__tests__/world/commands.test.ts +337 -0
- package/src/__tests__/world/debug-stats.test.ts +206 -0
- package/src/__tests__/world/multi-component-hooks.test.ts +44 -0
- package/src/__tests__/world/serialize.test.ts +17 -0
- package/src/__tests__/world/wildcard-relation-hooks.test.ts +127 -0
- package/src/archetype/archetype.ts +96 -46
- package/src/archetype/helpers.ts +7 -29
- package/src/archetype/store.ts +35 -20
- package/src/commands/buffer.ts +5 -2
- package/src/commands/changeset.ts +0 -31
- package/src/component/registry.ts +64 -63
- package/src/entity/index.ts +6 -3
- package/src/index.ts +13 -0
- package/src/query/filter.ts +4 -10
- package/src/query/query.ts +12 -12
- package/src/storage/serialization.ts +29 -2
- package/src/types/index.ts +71 -0
- package/src/world/commands.ts +44 -56
- package/src/world/hooks.ts +8 -0
- package/src/world/serialization.ts +32 -18
- package/src/world/world.ts +387 -20
package/README.en.md
CHANGED
|
@@ -78,7 +78,7 @@ const ChildOf = component({ exclusive: true, name: "ChildOf" });
|
|
|
78
78
|
| `name` | `string` | Component name, used for serialization/debugging |
|
|
79
79
|
| `exclusive` | `boolean` | Relation components only: an entity can have at most one relation of the same base component |
|
|
80
80
|
| `cascadeDelete` | `boolean` | Entity relations only: when the target entity is deleted, the **entire referencing entity** is deleted. Differs from default behavior (default only cleans up the relation component, the entity survives). Supports transitive cascading. |
|
|
81
|
-
| `
|
|
81
|
+
| `sparse` | `boolean` | Relation components only: relations with different target entities are stored in the same Archetype, preventing excessive fragmentation (legacy `dontFragment` alias remains fully supported) |
|
|
82
82
|
| `merge` | `(prev, next) => T` | Merge strategy when `set()` is called multiple times on the same component within a single sync batch |
|
|
83
83
|
|
|
84
84
|
### Lifecycle Hooks
|
|
@@ -274,7 +274,7 @@ component<T>();
|
|
|
274
274
|
// With a name
|
|
275
275
|
component<T>("Name");
|
|
276
276
|
// With options
|
|
277
|
-
component<T>({ name?: string, exclusive?: boolean, cascadeDelete?: boolean, dontFragment?: boolean, merge?: (prev, next) => T });
|
|
277
|
+
component<T>({ name?: string, exclusive?: boolean, cascadeDelete?: boolean, sparse?: boolean, dontFragment?: boolean /* legacy alias, fully compatible */, merge?: (prev, next) => T });
|
|
278
278
|
```
|
|
279
279
|
|
|
280
280
|
### relation()
|
|
@@ -288,6 +288,29 @@ relation(componentId, "*");
|
|
|
288
288
|
relation(componentId, otherComponentId);
|
|
289
289
|
```
|
|
290
290
|
|
|
291
|
+
### Relation & Hierarchy Companion Tools (New)
|
|
292
|
+
|
|
293
|
+
To stop users from repeatedly hand-writing `buildChildrenByParent` + recursive descent for parent-child hierarchies and inventory systems, we now ship first-class helpers:
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
const ChildOf = component<void>({ exclusive: true, sparse: true });
|
|
297
|
+
const world = new World();
|
|
298
|
+
// ... build hierarchy ...
|
|
299
|
+
|
|
300
|
+
// Recommended usage (standalone functions removed to simplify API surface)
|
|
301
|
+
const kids = world.getChildren(parent, ChildOf);
|
|
302
|
+
const p = world.getParent(child, ChildOf);
|
|
303
|
+
|
|
304
|
+
for (const { entity, depth } of world.iterateDescendants(root, ChildOf)) { ... }
|
|
305
|
+
|
|
306
|
+
const items = world.getRelationTargets(player, InInventory);
|
|
307
|
+
const owners = world.getRelationSources(sword, InInventory);
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
The same functionality is also available as methods on `World` instances. All helpers are fully compatible with data-bearing relations, exclusive/non-exclusive, and post-`sync()` semantics.
|
|
311
|
+
|
|
312
|
+
See `src/relations/hierarchy.ts` and the new test suite for details.
|
|
313
|
+
|
|
291
314
|
### Component / Entity ID Rules
|
|
292
315
|
|
|
293
316
|
- Component ID: `1` – `1023`
|
|
@@ -391,7 +414,7 @@ src/
|
|
|
391
414
|
│ ├── component-registry.ts # Component registry
|
|
392
415
|
│ ├── component-entity-store.ts # Singleton component storage
|
|
393
416
|
│ ├── component-type-utils.ts # Component type utilities
|
|
394
|
-
│ ├──
|
|
417
|
+
│ ├── store.ts # SparseStore (internal sparse storage)
|
|
395
418
|
│ ├── entity.ts # Entity/component/relation type exports (aggregate)
|
|
396
419
|
│ ├── entity-types.ts # Entity ID type definitions & constants
|
|
397
420
|
│ ├── entity-relation.ts # Relation ID encoding/decoding
|
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ const ChildOf = component({ exclusive: true, name: "ChildOf" });
|
|
|
78
78
|
| `name` | `string` | 组件名称,用于序列化/调试 |
|
|
79
79
|
| `exclusive` | `boolean` | 仅关系组件:同一实体对同一基础组件最多只能有一个关系 |
|
|
80
80
|
| `cascadeDelete` | `boolean` | 仅实体关系:删除目标实体时,持有该关系的**整个实体**也会被删除。区别于默认行为(默认仅清理关系组件,实体保留)。支持传递级联。 |
|
|
81
|
-
| `
|
|
81
|
+
| `sparse` | `boolean` | 仅关系组件:不同目标实体的关系存放在同一 Archetype,防止因目标不同而过度碎片化(旧别名 `dontFragment` 仍完全兼容) |
|
|
82
82
|
| `merge` | `(prev, next) => T` | 在同一 sync 批次中对同一组件反复 `set()` 时的合并策略 |
|
|
83
83
|
|
|
84
84
|
### 生命周期钩子
|
|
@@ -274,7 +274,7 @@ component<T>();
|
|
|
274
274
|
// 指定名称
|
|
275
275
|
component<T>("Name");
|
|
276
276
|
// 带选项
|
|
277
|
-
component<T>({ name?: string, exclusive?: boolean, cascadeDelete?: boolean,
|
|
277
|
+
component<T>({ name?: string, exclusive?: boolean, cascadeDelete?: boolean, sparse?: boolean, dontFragment?: boolean /* 旧别名,完全兼容 */, merge?: (prev, next) => T });
|
|
278
278
|
```
|
|
279
279
|
|
|
280
280
|
### relation()
|
|
@@ -288,6 +288,31 @@ relation(componentId, "*");
|
|
|
288
288
|
relation(componentId, otherComponentId);
|
|
289
289
|
```
|
|
290
290
|
|
|
291
|
+
### 关系/层级配套工具(新)
|
|
292
|
+
|
|
293
|
+
为避免用户在父子层级(`ChildOf`)和库存系统(`InInventory`)中反复手写 `buildChildrenByParent` + 递归遍历逻辑,我们提供了配套工具:
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
const ChildOf = component<void>({ exclusive: true, sparse: true });
|
|
297
|
+
const world = new World();
|
|
298
|
+
// ... 创建层级 ...
|
|
299
|
+
|
|
300
|
+
// 推荐直接在 World 实例上使用(API 表面已简化)
|
|
301
|
+
const kids = world.getChildren(parent, ChildOf);
|
|
302
|
+
const p = world.getParent(child, ChildOf);
|
|
303
|
+
|
|
304
|
+
for (const { entity, depth, parent } of world.iterateDescendants(root, ChildOf)) {
|
|
305
|
+
// ...
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const items = world.getRelationTargets(player, InInventory);
|
|
309
|
+
const owners = world.getRelationSources(sword, InInventory);
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
这些工具全部在 `world` 实例方法上也有对应(`world.getChildren(...)` 等),并完整支持数据负载关系、独占/非独占、删除后一致性。
|
|
313
|
+
|
|
314
|
+
详见 `src/relations/hierarchy.ts` 和新增的测试。
|
|
315
|
+
|
|
291
316
|
### 组件 / 实体 ID 规则
|
|
292
317
|
|
|
293
318
|
- 组件 ID:`1` ~ `1023`
|
|
@@ -391,7 +416,7 @@ src/
|
|
|
391
416
|
│ ├── component-registry.ts # 组件注册表
|
|
392
417
|
│ ├── component-entity-store.ts # 单例组件存储
|
|
393
418
|
│ ├── component-type-utils.ts # 组件类型工具
|
|
394
|
-
│ ├──
|
|
419
|
+
│ ├── store.ts # SparseStore (内部稀疏存储)
|
|
395
420
|
│ ├── entity.ts # 实体/组件/关系类型导出(聚合)
|
|
396
421
|
│ ├── entity-types.ts # 实体 ID 类型定义与常量
|
|
397
422
|
│ ├── entity-relation.ts # 关系 ID 编码/解码
|
package/dist/builder.d.mts
CHANGED
|
@@ -248,7 +248,7 @@ interface ComponentOptions<T = any> {
|
|
|
248
248
|
* `cascadeDelete`, deleting the target entity will both (a) delete the
|
|
249
249
|
* referencing entity, and (b) the exclusivity constraint prevents the
|
|
250
250
|
* entity from having multiple cascade-delete relations of the same type.
|
|
251
|
-
* - **`
|
|
251
|
+
* - **`sparse`**: Compatible. Exclusivity is enforced at the data level
|
|
252
252
|
* regardless of whether the archetype is fragmented.
|
|
253
253
|
*
|
|
254
254
|
* @example
|
|
@@ -306,7 +306,8 @@ interface ComponentOptions<T = any> {
|
|
|
306
306
|
*/
|
|
307
307
|
cascadeDelete?: boolean;
|
|
308
308
|
/**
|
|
309
|
-
* If true, relations with this component will not cause
|
|
309
|
+
* If true, relations with this component use sparse storage and will not cause
|
|
310
|
+
* archetype fragmentation.
|
|
310
311
|
*
|
|
311
312
|
* **Problem it solves**: By default, each unique relation pair `(component, target)`
|
|
312
313
|
* creates a **separate archetype**. If 100 entities each have a `ChildOf` relation
|
|
@@ -314,28 +315,28 @@ interface ComponentOptions<T = any> {
|
|
|
314
315
|
* Queries that iterate over all entities with a `ChildOf` relation must check all
|
|
315
316
|
* 100 archetypes, which degrades iteration performance and increases memory overhead.
|
|
316
317
|
*
|
|
317
|
-
* **How it works**: When `
|
|
318
|
-
* contribute to the archetype signature. Entities with different targets
|
|
319
|
-
* relation component share a **single archetype**, and the per-entity
|
|
320
|
-
* stored in a separate
|
|
321
|
-
* A wildcard relation marker (`relation(Comp, "*")`) is placed
|
|
322
|
-
* component list so queries can still discover matching archetypes.
|
|
318
|
+
* **How it works (sparse storage)**: When `sparse` is enabled, the relation's target
|
|
319
|
+
* does **not** contribute to the archetype signature. Entities with different targets
|
|
320
|
+
* for the same relation component share a **single archetype**, and the per-entity
|
|
321
|
+
* target data is stored in a separate side store (historically called
|
|
322
|
+
* `DontFragmentStore`, now `SparseStore`). A wildcard relation marker (`relation(Comp, "*")`) is placed
|
|
323
|
+
* in the archetype component list so queries can still discover matching archetypes.
|
|
323
324
|
*
|
|
324
325
|
* **Use cases**:
|
|
325
326
|
* - **Hierarchy/ownership**: `ChildOf` relations where thousands of entities each
|
|
326
327
|
* point to different parent entities.
|
|
327
328
|
* - **Dynamic targeting**: Relations where targets change frequently (e.g., AI
|
|
328
|
-
* targeting, inventory slots) — without `
|
|
329
|
-
*
|
|
329
|
+
* targeting, inventory slots) — without `sparse`, each target change would cause
|
|
330
|
+
* an archetype migration, which is expensive.
|
|
330
331
|
* - **High-cardinality relations**: Any relation where the number of unique targets
|
|
331
332
|
* is large compared to the number of entities.
|
|
332
333
|
*
|
|
333
334
|
* **Performance implications**:
|
|
334
|
-
* - **Without `
|
|
335
|
+
* - **Without `sparse`**: Archetype count grows linearly with unique targets.
|
|
335
336
|
* Each archetype migration (changing a relation target) requires moving the entity's
|
|
336
337
|
* data between component arrays.
|
|
337
|
-
* - **With `
|
|
338
|
-
*
|
|
338
|
+
* - **With `sparse`**: Archetype count stays constant regardless of target diversity.
|
|
339
|
+
* Changing a relation target is an O(1) update in the sparse side store.
|
|
339
340
|
* The trade-off is an extra map lookup when accessing the relation data.
|
|
340
341
|
*
|
|
341
342
|
* **Constraints**:
|
|
@@ -344,13 +345,16 @@ interface ComponentOptions<T = any> {
|
|
|
344
345
|
* archetype carries a wildcard marker so queries can discover it.
|
|
345
346
|
* - Works with `exclusive` and `cascadeDelete` simultaneously.
|
|
346
347
|
*
|
|
348
|
+
* **Backward compatibility**: The legacy key `dontFragment` is still accepted and
|
|
349
|
+
* behaves identically. Prefer `sparse` in new code.
|
|
350
|
+
*
|
|
347
351
|
* @example
|
|
348
352
|
* ```ts
|
|
349
|
-
* // Without
|
|
353
|
+
* // Without sparse: 100 entities with different parents = 100 archetypes
|
|
350
354
|
* const ChildOf = component(); // default: fragmentation happens
|
|
351
355
|
*
|
|
352
|
-
* // With
|
|
353
|
-
* const ChildOf = component({
|
|
356
|
+
* // With sparse: 100 entities with different parents = 1 archetype
|
|
357
|
+
* const ChildOf = component({ sparse: true });
|
|
354
358
|
*
|
|
355
359
|
* for (let i = 0; i < 100; i++) {
|
|
356
360
|
* const parent = world.new();
|
|
@@ -359,11 +363,17 @@ interface ComponentOptions<T = any> {
|
|
|
359
363
|
* world.set(child, relation(ChildOf, parent));
|
|
360
364
|
* }
|
|
361
365
|
* world.sync();
|
|
362
|
-
* //
|
|
366
|
+
* // sparse: 1 archetype for all 100 entities
|
|
363
367
|
* // without: 100 archetypes, one per unique parent
|
|
364
368
|
* ```
|
|
365
369
|
*
|
|
366
|
-
* Inspired by Flecs' `DontFragment` trait.
|
|
370
|
+
* Inspired by Flecs' `DontFragment` trait (now exposed as the clearer `sparse` option).
|
|
371
|
+
*/
|
|
372
|
+
sparse?: boolean;
|
|
373
|
+
/**
|
|
374
|
+
* @deprecated Use `sparse: true` instead. This key is kept solely for backward
|
|
375
|
+
* compatibility; `component({ dontFragment: true })` continues to work exactly
|
|
376
|
+
* as before and is equivalent to `sparse: true`.
|
|
367
377
|
*/
|
|
368
378
|
dontFragment?: boolean;
|
|
369
379
|
/**
|
|
@@ -453,6 +463,53 @@ declare function getComponentIdByName(name: string): ComponentId<any> | undefine
|
|
|
453
463
|
* @returns The component name if found, undefined otherwise
|
|
454
464
|
*/
|
|
455
465
|
declare function getComponentNameById(id: ComponentId<any>): string | undefined;
|
|
466
|
+
/**
|
|
467
|
+
* Check if a component is marked as `sparse` (sparse storage for relations).
|
|
468
|
+
*
|
|
469
|
+
* When a component has `sparse: true`, relations using it do not cause archetype
|
|
470
|
+
* fragmentation — entities with different relation targets can share the same
|
|
471
|
+
* archetype. This is a fast O(1) bitset lookup. The legacy `dontFragment` key
|
|
472
|
+
* is still accepted and sets the same internal flag.
|
|
473
|
+
*
|
|
474
|
+
* @param id - The component ID to check.
|
|
475
|
+
* @returns `true` if the component was created with `sparse: true` (or the
|
|
476
|
+
* legacy `dontFragment: true`).
|
|
477
|
+
*
|
|
478
|
+
* @see {@link ComponentOptions.sparse} for the full explanation of sparse storage.
|
|
479
|
+
*/
|
|
480
|
+
declare function isSparseComponent(id: ComponentId<any>): boolean;
|
|
481
|
+
/**
|
|
482
|
+
* Check if an ID is a specific (non-wildcard) relation backed by a `sparse`
|
|
483
|
+
* component (i.e. stored in the side sparse store rather than the archetype).
|
|
484
|
+
*
|
|
485
|
+
* This is used in hot paths (archetype resolution, command processing) to determine
|
|
486
|
+
* whether a relation should be excluded from the archetype signature.
|
|
487
|
+
*
|
|
488
|
+
* @param id - The entity/relation ID to check (must be a relation ID, not a plain
|
|
489
|
+
* component ID).
|
|
490
|
+
* @returns `true` if this is a specific-target relation (not wildcard) whose base
|
|
491
|
+
* component was created with `sparse: true` (or legacy `dontFragment: true`).
|
|
492
|
+
*
|
|
493
|
+
* @see {@link isSparseWildcard} for the wildcard variant.
|
|
494
|
+
* @see {@link ComponentOptions.sparse} for the full explanation.
|
|
495
|
+
*/
|
|
496
|
+
declare function isSparseRelation(id: EntityId<any>): boolean;
|
|
497
|
+
/**
|
|
498
|
+
* Check if an ID is a wildcard relation (`relation(Comp, "*")`) backed by a
|
|
499
|
+
* `sparse` component.
|
|
500
|
+
*
|
|
501
|
+
* Wildcard markers for sparse components are placed in the archetype component
|
|
502
|
+
* list so that queries can discover archetypes containing entities with that
|
|
503
|
+
* relation type.
|
|
504
|
+
*
|
|
505
|
+
* @param id - The entity/relation ID to check.
|
|
506
|
+
* @returns `true` if this is a wildcard relation (`"*"` target) whose base
|
|
507
|
+
* component was created with `sparse: true` (or legacy `dontFragment: true`).
|
|
508
|
+
*
|
|
509
|
+
* @see {@link isSparseRelation} for the specific-target variant.
|
|
510
|
+
* @see {@link ComponentOptions.sparse} for the full explanation.
|
|
511
|
+
*/
|
|
512
|
+
declare function isSparseWildcard(id: EntityId<any>): boolean;
|
|
456
513
|
//#endregion
|
|
457
514
|
//#region src/storage/serialization.d.ts
|
|
458
515
|
type SerializedEntityId = number | string | {
|
|
@@ -558,24 +615,78 @@ interface LifecycleHookEntry {
|
|
|
558
615
|
/** Archetypes that match this hook, used for precise cleanup on unsubscription */
|
|
559
616
|
matchedArchetypes?: Set<any>;
|
|
560
617
|
}
|
|
618
|
+
/**
|
|
619
|
+
* Statistics payload delivered to callbacks registered via `World.createDebugStatsCollector`.
|
|
620
|
+
*
|
|
621
|
+
* All structural counts are snapshots taken after the sync that triggered delivery.
|
|
622
|
+
* `activity` always reflects work performed during that specific sync.
|
|
623
|
+
*
|
|
624
|
+
* Timestamps are raw `performance.now()` values suitable for `performance.measure`.
|
|
625
|
+
*/
|
|
626
|
+
interface SyncDebugStats {
|
|
627
|
+
readonly timestamps: {
|
|
628
|
+
readonly syncStart: number;
|
|
629
|
+
readonly syncEnd: number;
|
|
630
|
+
readonly commandBufferStart: number;
|
|
631
|
+
readonly commandBufferEnd: number;
|
|
632
|
+
};
|
|
633
|
+
/** Number of iterations the internal command buffer loop performed during this sync. */
|
|
634
|
+
readonly commandIterations: number;
|
|
635
|
+
readonly entities: {
|
|
636
|
+
readonly total: number;
|
|
637
|
+
readonly freelistSize: number;
|
|
638
|
+
readonly nextId: number;
|
|
639
|
+
};
|
|
640
|
+
readonly archetypes: {
|
|
641
|
+
readonly total: number;
|
|
642
|
+
readonly empty: number;
|
|
643
|
+
};
|
|
644
|
+
readonly queries: {
|
|
645
|
+
readonly cached: number;
|
|
646
|
+
readonly registered: number;
|
|
647
|
+
};
|
|
648
|
+
readonly hooks: {
|
|
649
|
+
readonly total: number;
|
|
650
|
+
};
|
|
651
|
+
/** Sizes of stable internal reverse indices (conservative set). */
|
|
652
|
+
readonly indices: {
|
|
653
|
+
readonly entityReferences: number;
|
|
654
|
+
readonly entityToReferencingArchetypes: number;
|
|
655
|
+
readonly archetypesByComponent: number;
|
|
656
|
+
};
|
|
657
|
+
/**
|
|
658
|
+
* Activity that occurred as a direct result of this sync.
|
|
659
|
+
* All fields are always present (never optional).
|
|
660
|
+
*/
|
|
661
|
+
readonly activity: {
|
|
662
|
+
/** Number of entities that performed an archetype migration (hasArchetypeStructuralChange was true). */readonly migrations: number; /** Total number of individual hook callback invocations (invokeHook calls). */
|
|
663
|
+
readonly hooksExecuted: number; /** Number of new archetypes created during this sync. */
|
|
664
|
+
readonly archetypesCreated: number; /** Number of archetypes removed during this sync. */
|
|
665
|
+
readonly archetypesRemoved: number;
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Handle returned by `World.createDebugStatsCollector`.
|
|
670
|
+
* The object itself carries no data — its only responsibility is lifetime management.
|
|
671
|
+
* Use with `using` or call `[Symbol.dispose]()` when you no longer need collection.
|
|
672
|
+
*/
|
|
673
|
+
interface DebugStatsCollector {
|
|
674
|
+
[Symbol.dispose](): void;
|
|
675
|
+
}
|
|
561
676
|
//#endregion
|
|
562
677
|
//#region src/archetype/store.d.ts
|
|
563
678
|
/**
|
|
564
|
-
* Interface for
|
|
565
|
-
*
|
|
566
|
-
* Storage is now primarily keyed by relation ComponentId (the "kind" of relation)
|
|
567
|
-
* rather than by entity. This provides O(1) or near-O(1) answers for the hot
|
|
568
|
-
* wildcard-related paths (hasRelationWithComponentId, wildcard materialization
|
|
569
|
-
* during iteration, hook matching, etc.).
|
|
679
|
+
* Interface for the sparse side store used by components declared with `sparse: true`
|
|
680
|
+
* (or the legacy `dontFragment: true` alias).
|
|
570
681
|
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
* operations (removeEntity, dump, getEntity, serialization).
|
|
682
|
+
* Relation data for these components lives here instead of in archetype columns,
|
|
683
|
+
* preventing fragmentation for high-cardinality or frequently-changing relations.
|
|
574
684
|
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
685
|
+
* Storage is primarily keyed by base relation ComponentId. This enables efficient
|
|
686
|
+
* per-component lookups required by wildcard queries (relation(Comp, "*")) and
|
|
687
|
+
* archetype filtering, while still supporting full-entity enumeration when needed.
|
|
577
688
|
*/
|
|
578
|
-
interface
|
|
689
|
+
interface SparseStore {
|
|
579
690
|
getValue(entityId: EntityId, relationType: EntityId<any>): any | undefined;
|
|
580
691
|
setValue(entityId: EntityId, relationType: EntityId<any>, data: any): void;
|
|
581
692
|
deleteValue(entityId: EntityId, relationType: EntityId<any>): boolean;
|
|
@@ -583,6 +694,12 @@ interface DontFragmentStore {
|
|
|
583
694
|
getRelationsForComponent(entityId: EntityId, componentId: EntityId<any>): [target: EntityId, data: any][];
|
|
584
695
|
getAllForEntity(entityId: EntityId): Array<[relationType: EntityId<any>, data: any]>;
|
|
585
696
|
deleteEntity(entityId: EntityId): void;
|
|
697
|
+
/**
|
|
698
|
+
* @internal Bulk helper for serialization of many entities.
|
|
699
|
+
* Default implementation simply loops getAllForEntity; subclasses / future
|
|
700
|
+
* implementations can provide a more efficient fused walk.
|
|
701
|
+
*/
|
|
702
|
+
getAllForEntities(entityIds: readonly EntityId[]): Map<EntityId, Array<[EntityId<any>, any]>>;
|
|
586
703
|
}
|
|
587
704
|
//#endregion
|
|
588
705
|
//#region src/archetype/archetype.d.ts
|
|
@@ -614,11 +731,10 @@ declare class Archetype {
|
|
|
614
731
|
*/
|
|
615
732
|
private entityToIndex;
|
|
616
733
|
/**
|
|
617
|
-
*
|
|
618
|
-
* Uses optimized RelationEntry (single/multi) for the common exclusive case.
|
|
734
|
+
* SparseStore used for relations declared with `sparse: true`.
|
|
619
735
|
* See store.ts for implementation details.
|
|
620
736
|
*/
|
|
621
|
-
private
|
|
737
|
+
private sparseRelations;
|
|
622
738
|
/**
|
|
623
739
|
* Multi-hooks that match this archetype
|
|
624
740
|
*/
|
|
@@ -627,7 +743,7 @@ declare class Archetype {
|
|
|
627
743
|
* Cache for pre-computed component data sources to avoid repeated calculations
|
|
628
744
|
*/
|
|
629
745
|
private componentDataSourcesCache;
|
|
630
|
-
constructor(componentTypes: EntityId<any>[],
|
|
746
|
+
constructor(componentTypes: EntityId<any>[], sparseStore: SparseStore);
|
|
631
747
|
get size(): number;
|
|
632
748
|
/**
|
|
633
749
|
* Check if the given component types match this archetype
|
|
@@ -637,19 +753,34 @@ declare class Archetype {
|
|
|
637
753
|
*/
|
|
638
754
|
matches(componentTypes: EntityId<any>[]): boolean;
|
|
639
755
|
addEntity(entityId: EntityId, componentData: Map<EntityId<any>, any>): void;
|
|
640
|
-
private
|
|
756
|
+
private addSparseRelations;
|
|
641
757
|
getEntity(entityId: EntityId): Map<EntityId<any>, any> | undefined;
|
|
642
758
|
/**
|
|
643
|
-
* Returns all
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
* Prefer the new DontFragmentStore methods when possible.
|
|
759
|
+
* Returns all sparse-stored relations for the given entity.
|
|
760
|
+
* Internal helper used by command processing and tests.
|
|
647
761
|
*/
|
|
648
|
-
|
|
762
|
+
getEntitySparseRelations(entityId: EntityId): Map<EntityId<any>, any> | undefined;
|
|
649
763
|
dump(): Array<{
|
|
650
764
|
entity: EntityId;
|
|
651
765
|
components: Map<EntityId<any>, any>;
|
|
652
766
|
}>;
|
|
767
|
+
/**
|
|
768
|
+
* @internal Serialization fast-path.
|
|
769
|
+
*
|
|
770
|
+
* Appends SerializedEntity records directly from the archetype's column storage
|
|
771
|
+
* (componentData arrays) plus sparse relations, avoiding per-entity Map
|
|
772
|
+
* allocation and repeated Array.from(entries()).
|
|
773
|
+
*
|
|
774
|
+
* Component type IDs should be pre-encoded by the caller (once per archetype)
|
|
775
|
+
* and passed in `encodedComponentTypes` (same order and length as this.componentTypes).
|
|
776
|
+
*
|
|
777
|
+
* The provided `encode` function should be the cached variant for best performance
|
|
778
|
+
* on entity IDs and any sparse relation type IDs.
|
|
779
|
+
*
|
|
780
|
+
* `sparseByEntity` is an optional pre-fetched map from a bulk
|
|
781
|
+
* `SparseStore.getAllForEntities` call (further reduces per-entity calls).
|
|
782
|
+
*/
|
|
783
|
+
appendSerializedEntities(out: SerializedEntity[], encode: (id: EntityId<any>) => SerializedEntityId, encodedComponentTypes: SerializedEntityId[], sparseByEntity?: Map<EntityId, Array<[EntityId<any>, any]>>): void;
|
|
653
784
|
removeEntity(entityId: EntityId): Map<EntityId<any>, any> | undefined;
|
|
654
785
|
exists(entityId: EntityId): boolean;
|
|
655
786
|
get<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, any][];
|
|
@@ -764,8 +895,8 @@ declare class Query {
|
|
|
764
895
|
_cacheKey: string | undefined;
|
|
765
896
|
/** Cached wildcard component types for faster entity filtering */
|
|
766
897
|
private wildcardTypes;
|
|
767
|
-
/** Cached specific
|
|
768
|
-
private
|
|
898
|
+
/** Cached specific sparse relation types that need entity-level filtering */
|
|
899
|
+
private specificSparseRelationTypes;
|
|
769
900
|
/**
|
|
770
901
|
* @internal Queries should be created via {@link World.createQuery}, not instantiated directly.
|
|
771
902
|
*/
|
|
@@ -787,7 +918,7 @@ declare class Query {
|
|
|
787
918
|
*/
|
|
788
919
|
getEntities(): EntityId[];
|
|
789
920
|
/**
|
|
790
|
-
* Check if entity matches all query requirements (wildcards and specific
|
|
921
|
+
* Check if entity matches all query requirements (wildcards and specific sparse relations)
|
|
791
922
|
*/
|
|
792
923
|
private entityMatchesQuery;
|
|
793
924
|
/**
|
|
@@ -892,12 +1023,16 @@ declare class World {
|
|
|
892
1023
|
private entityReferences;
|
|
893
1024
|
/** Reverse index: entity ID → set of archetypes whose componentTypes include that entity ID */
|
|
894
1025
|
private entityToReferencingArchetypes;
|
|
895
|
-
/**
|
|
896
|
-
private readonly
|
|
1026
|
+
/** Sparse relation storage (for components created with `sparse: true`), shared with all Archetype instances */
|
|
1027
|
+
private readonly sparseStore;
|
|
897
1028
|
/** Component entity (singleton) storage */
|
|
898
1029
|
private readonly componentEntities;
|
|
899
1030
|
private readonly queryRegistry;
|
|
900
1031
|
private hooks;
|
|
1032
|
+
private readonly _debugCollectors;
|
|
1033
|
+
private _debugMigrations;
|
|
1034
|
+
private _debugArchetypesCreated;
|
|
1035
|
+
private _debugArchetypesRemoved;
|
|
901
1036
|
private commandBuffer;
|
|
902
1037
|
private readonly _changeset;
|
|
903
1038
|
private readonly _removeChangeset;
|
|
@@ -1112,6 +1247,106 @@ declare class World {
|
|
|
1112
1247
|
getOptional<T>(entityId: EntityId, componentType: EntityId<T>): {
|
|
1113
1248
|
value: T;
|
|
1114
1249
|
} | undefined;
|
|
1250
|
+
/**
|
|
1251
|
+
* Retrieves all targets (and their associated data) for relations of a given
|
|
1252
|
+
* base component on an entity.
|
|
1253
|
+
*
|
|
1254
|
+
* This is the ergonomic replacement for the common pattern:
|
|
1255
|
+
* world.get(entity, relation(Comp, "*"))
|
|
1256
|
+
*
|
|
1257
|
+
* @example
|
|
1258
|
+
* const ChildOf = component({ exclusive: true, sparse: true });
|
|
1259
|
+
* const children = world.getRelationTargets(parent, ChildOf); // usually []
|
|
1260
|
+
* const items = world.getRelationTargets(player, InInventory);
|
|
1261
|
+
*
|
|
1262
|
+
* // For common hierarchy use cases, prefer the higher-level helpers:
|
|
1263
|
+
* // world.getChildren(parent, ChildOf), world.getParent(child, ChildOf)
|
|
1264
|
+
*/
|
|
1265
|
+
getRelationTargets<T = void>(entityId: EntityId, relationComp: ComponentId<T>): [target: EntityId<unknown>, data: T | undefined][];
|
|
1266
|
+
/**
|
|
1267
|
+
* Returns every entity that currently holds a relation of the given base
|
|
1268
|
+
* component pointing at `targetId`.
|
|
1269
|
+
*
|
|
1270
|
+
* This is the efficient **reverse** lookup. For common hierarchy cases,
|
|
1271
|
+
* prefer the higher-level `world.getChildren(parent, ChildOf)` instead.
|
|
1272
|
+
*
|
|
1273
|
+
* @example
|
|
1274
|
+
* const ChildOf = component({ exclusive: true, sparse: true });
|
|
1275
|
+
* const directChildren = world.getRelationSources(ship, ChildOf);
|
|
1276
|
+
*/
|
|
1277
|
+
getRelationSources(targetId: EntityId, relationComp: ComponentId<any>): EntityId[];
|
|
1278
|
+
/**
|
|
1279
|
+
* Returns true if the entity has any (or a specific-target) relation of the
|
|
1280
|
+
* given base component.
|
|
1281
|
+
*/
|
|
1282
|
+
hasRelation(entityId: EntityId, relationComp: ComponentId<any>, targetId?: EntityId): boolean;
|
|
1283
|
+
/**
|
|
1284
|
+
* Returns the number of relations of the given base component held by the entity.
|
|
1285
|
+
*/
|
|
1286
|
+
countRelations(entityId: EntityId, relationComp: ComponentId<any>): number;
|
|
1287
|
+
/**
|
|
1288
|
+
* For an *exclusive* relation (e.g. ChildOf, Owner), returns the single
|
|
1289
|
+
* target entity (or undefined if none).
|
|
1290
|
+
*
|
|
1291
|
+
* When the component was declared `exclusive: true`, this is the preferred
|
|
1292
|
+
* accessor (clearer intent than array destructuring).
|
|
1293
|
+
*/
|
|
1294
|
+
getSingleRelationTarget<T = void>(entityId: EntityId, relationComp: ComponentId<T>): EntityId | undefined;
|
|
1295
|
+
/**
|
|
1296
|
+
* Returns the direct children of `parent` for the given relationship component
|
|
1297
|
+
* (typically a `ChildOf` or similar exclusive `sparse` relation).
|
|
1298
|
+
*
|
|
1299
|
+
* This is the recommended high-level API for hierarchy traversal.
|
|
1300
|
+
* It uses the internal reverse reference index for efficiency.
|
|
1301
|
+
*
|
|
1302
|
+
* @example
|
|
1303
|
+
* const ChildOf = component({ exclusive: true, sparse: true });
|
|
1304
|
+
* const kids = world.getChildren(ship, ChildOf);
|
|
1305
|
+
*/
|
|
1306
|
+
getChildren(parent: EntityId, childOf: ComponentId<any>): EntityId[];
|
|
1307
|
+
/**
|
|
1308
|
+
* Returns the parent of `child` for the given relationship component
|
|
1309
|
+
* (typically an exclusive `ChildOf` relation).
|
|
1310
|
+
*
|
|
1311
|
+
* @example
|
|
1312
|
+
* const ChildOf = component({ exclusive: true, sparse: true });
|
|
1313
|
+
* const parent = world.getParent(turret, ChildOf);
|
|
1314
|
+
*/
|
|
1315
|
+
getParent(child: EntityId, childOf: ComponentId<any>): EntityId | undefined;
|
|
1316
|
+
/**
|
|
1317
|
+
* Returns the ancestor chain from the immediate parent up to (but not
|
|
1318
|
+
* including) the root for the given relationship component.
|
|
1319
|
+
*
|
|
1320
|
+
* @example
|
|
1321
|
+
* const ChildOf = component({ exclusive: true, sparse: true });
|
|
1322
|
+
* const ancestors = world.getAncestors(muzzle, ChildOf); // [turret, ship]
|
|
1323
|
+
*/
|
|
1324
|
+
getAncestors(entity: EntityId, childOf: ComponentId<any>): EntityId[];
|
|
1325
|
+
/**
|
|
1326
|
+
* Iteratively traverses all descendants of `root` in DFS pre-order.
|
|
1327
|
+
* This is a generator and is safe for very deep hierarchies.
|
|
1328
|
+
*
|
|
1329
|
+
* @example
|
|
1330
|
+
* for (const { entity, depth, parent } of world.iterateDescendants(root, ChildOf)) {
|
|
1331
|
+
* console.log(depth, entity);
|
|
1332
|
+
* }
|
|
1333
|
+
*/
|
|
1334
|
+
iterateDescendants(root: EntityId, childOf: ComponentId<any>, opts?: {
|
|
1335
|
+
includeSelf?: boolean;
|
|
1336
|
+
maxDepth?: number;
|
|
1337
|
+
}): IterableIterator<{
|
|
1338
|
+
entity: EntityId;
|
|
1339
|
+
depth: number;
|
|
1340
|
+
parent: EntityId | null;
|
|
1341
|
+
}>;
|
|
1342
|
+
/**
|
|
1343
|
+
* Callback-based descendant traversal (hot path friendly).
|
|
1344
|
+
* Return `false` from the visitor to stop early.
|
|
1345
|
+
*/
|
|
1346
|
+
traverseDescendants(root: EntityId, childOf: ComponentId<any>, visitor: (entity: EntityId, depth: number, parent: EntityId | null) => void | boolean, opts?: {
|
|
1347
|
+
includeSelf?: boolean;
|
|
1348
|
+
maxDepth?: number;
|
|
1349
|
+
}): void;
|
|
1115
1350
|
/**
|
|
1116
1351
|
* Registers a lifecycle hook that responds to component changes.
|
|
1117
1352
|
* The hook callback is invoked when components matching the specified types are added, updated, or removed.
|
|
@@ -1153,6 +1388,21 @@ declare class World {
|
|
|
1153
1388
|
* );
|
|
1154
1389
|
*/
|
|
1155
1390
|
hook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: LifecycleHook<T> | LifecycleCallback<T>, filter?: QueryFilter): () => void;
|
|
1391
|
+
/**
|
|
1392
|
+
* Creates a debug stats collector that will receive a `SyncDebugStats` payload
|
|
1393
|
+
* after every subsequent `sync()`.
|
|
1394
|
+
*
|
|
1395
|
+
* The returned object is a pure lifecycle handle. It does not store data.
|
|
1396
|
+
* Collection stops when you call `[Symbol.dispose]()` (or use a `using` declaration).
|
|
1397
|
+
*
|
|
1398
|
+
* All active collectors receive the exact same stats object for a given sync.
|
|
1399
|
+
* Exceptions thrown by callbacks are ignored.
|
|
1400
|
+
*
|
|
1401
|
+
* This is intended for development/debugging and leak detection.
|
|
1402
|
+
*/
|
|
1403
|
+
createDebugStatsCollector(callback: (stats: SyncDebugStats) => void): DebugStatsCollector;
|
|
1404
|
+
private _resetDebugActivityCounters;
|
|
1405
|
+
private _deliverDebugStats;
|
|
1156
1406
|
/**
|
|
1157
1407
|
* Synchronizes all buffered commands (set/remove/delete) to the world.
|
|
1158
1408
|
* This method must be called after making changes via `set()`, `remove()`, or `delete()` for them to take effect.
|
|
@@ -1393,5 +1643,5 @@ declare class EntityBuilder {
|
|
|
1393
1643
|
build(): EntityId;
|
|
1394
1644
|
}
|
|
1395
1645
|
//#endregion
|
|
1396
|
-
export {
|
|
1646
|
+
export { WildcardRelationId as A, isWildcardRelationId as C, EntityId as D, ComponentRelationId as E, isEntityId as M, isRelationId as N, EntityRelationId as O, decodeRelationId as S, ComponentId as T, getComponentIdByName as _, ComponentTuple as a, isSparseRelation as b, LifecycleCallback as c, SerializedComponent as d, SerializedEntity as f, component as g, ComponentOptions as h, Query as i, isComponentId as j, RelationId as k, LifecycleHook as l, SerializedWorld as m, EntityBuilder as n, ComponentType as o, SerializedEntityId as p, World as r, DebugStatsCollector as s, ComponentDef as t, SyncDebugStats as u, getComponentNameById as v, relation as w, isSparseWildcard as x, isSparseComponent as y };
|
|
1397
1647
|
//# sourceMappingURL=builder.d.mts.map
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
export { type ComponentDef, type ComponentId, type ComponentOptions, type ComponentRelationId, type ComponentTuple, type ComponentType, EntityBuilder, type EntityId, type EntityRelationId, type LifecycleCallback, type LifecycleHook, Query, type RelationId, type SerializedComponent, type SerializedEntity, type SerializedEntityId, type SerializedWorld, type WildcardRelationId, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isEntityId, isRelationId, isWildcardRelationId, relation };
|
|
1
|
+
import { A as WildcardRelationId, C as isWildcardRelationId, D as EntityId, E as ComponentRelationId, M as isEntityId, N as isRelationId, O as EntityRelationId, S as decodeRelationId, T as ComponentId, _ as getComponentIdByName, a as ComponentTuple, b as isSparseRelation, c as LifecycleCallback, d as SerializedComponent, f as SerializedEntity, g as component, h as ComponentOptions, i as Query, j as isComponentId, k as RelationId, l as LifecycleHook, m as SerializedWorld, n as EntityBuilder, o as ComponentType, p as SerializedEntityId, r as World, s as DebugStatsCollector, t as ComponentDef, u as SyncDebugStats, v as getComponentNameById, w as relation, x as isSparseWildcard, y as isSparseComponent } from "./builder.mjs";
|
|
2
|
+
export { type ComponentDef, type ComponentId, type ComponentOptions, type ComponentRelationId, type ComponentTuple, type ComponentType, type DebugStatsCollector, EntityBuilder, type EntityId, type EntityRelationId, type LifecycleCallback, type LifecycleHook, Query, type RelationId, type SerializedComponent, type SerializedEntity, type SerializedEntityId, type SerializedWorld, type SyncDebugStats, type WildcardRelationId, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isSparseComponent as isDontFragmentComponent, isSparseComponent, isSparseRelation as isDontFragmentRelation, isSparseRelation, isSparseWildcard as isDontFragmentWildcard, isSparseWildcard, isEntityId, isRelationId, isWildcardRelationId, relation };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as getComponentIdByName, c as
|
|
2
|
-
export { EntityBuilder, Query, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isEntityId, isRelationId, isWildcardRelationId, relation };
|
|
1
|
+
import { a as getComponentIdByName, c as isSparseRelation, d as isWildcardRelationId, f as relation, h as isRelationId, i as component, l as isSparseWildcard, m as isEntityId, n as Query, o as getComponentNameById, p as isComponentId, r as EntityBuilder, s as isSparseComponent, t as World, u as decodeRelationId } from "./world.mjs";
|
|
2
|
+
export { EntityBuilder, Query, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isSparseComponent as isDontFragmentComponent, isSparseComponent, isSparseRelation as isDontFragmentRelation, isSparseRelation, isSparseWildcard as isDontFragmentWildcard, isSparseWildcard, isEntityId, isRelationId, isWildcardRelationId, relation };
|
package/dist/testing.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as WildcardRelationId, D as EntityId, T as ComponentId, c as LifecycleCallback, g as component, i as Query, k as RelationId, l as LifecycleHook, n as EntityBuilder, r as World, t as ComponentDef, w as relation } from "./builder.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/testing/index.d.ts
|
|
4
4
|
/**
|
package/dist/testing.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { d as isWildcardRelationId, f as relation, i as component, r as EntityBuilder, t as World } from "./world.mjs";
|
|
2
2
|
//#region src/testing/index.ts
|
|
3
3
|
/**
|
|
4
4
|
* A test fixture that manages a World instance and provides convenient
|