@codehz/ecs 0.8.2 → 0.10.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.
Files changed (57) hide show
  1. package/README.en.md +26 -3
  2. package/README.md +41 -4
  3. package/dist/builder.d.mts +348 -83
  4. package/dist/index.d.mts +2 -2
  5. package/dist/index.mjs +2 -2
  6. package/dist/testing.d.mts +1 -1
  7. package/dist/testing.mjs +1 -1
  8. package/dist/world.mjs +1922 -1400
  9. package/dist/world.mjs.map +1 -1
  10. package/examples/debug-observability.ts +92 -0
  11. package/examples/inventory-system-relations.ts +1 -1
  12. package/examples/parent-child-hierarchy.ts +18 -38
  13. package/examples/spatial-grid.ts +1 -1
  14. package/package.json +1 -1
  15. package/skills/ecs/SKILL.md +4 -4
  16. package/src/__tests__/component/singleton.test.ts +116 -35
  17. package/src/__tests__/core/archetype.test.ts +155 -13
  18. package/src/__tests__/core/bitset.test.ts +12 -0
  19. package/src/__tests__/entity/entity.test.ts +33 -0
  20. package/src/__tests__/entity/id-system.test.ts +40 -0
  21. package/src/__tests__/perf/comprehensive.perf.test.ts +6 -9
  22. package/src/__tests__/perf/serialization.perf.test.ts +242 -0
  23. package/src/__tests__/perf/{dontfragment-wildcard.perf.test.ts → sparse-wildcard.perf.test.ts} +13 -16
  24. package/src/__tests__/query/caching.test.ts +62 -0
  25. package/src/__tests__/query/filter.test.ts +16 -22
  26. package/src/__tests__/query/perf.test.ts +3 -5
  27. package/src/__tests__/relations/hierarchy.test.ts +208 -0
  28. package/src/__tests__/relations/{dont-fragment → sparse}/basic.test.ts +64 -69
  29. package/src/__tests__/relations/{dont-fragment → sparse}/query-notification.test.ts +17 -9
  30. package/src/__tests__/serialization/bounds.test.ts +133 -1
  31. package/src/__tests__/world/commands.test.ts +337 -0
  32. package/src/__tests__/world/component-management.test.ts +6 -5
  33. package/src/__tests__/world/debug-stats.test.ts +206 -0
  34. package/src/__tests__/world/multi-component-hooks.test.ts +44 -0
  35. package/src/__tests__/world/serialize.test.ts +17 -0
  36. package/src/__tests__/world/wildcard-relation-hooks.test.ts +127 -0
  37. package/src/archetype/archetype.ts +96 -46
  38. package/src/archetype/helpers.ts +7 -29
  39. package/src/archetype/store.ts +35 -20
  40. package/src/commands/buffer.ts +5 -2
  41. package/src/commands/changeset.ts +0 -31
  42. package/src/component/registry.ts +64 -63
  43. package/src/entity/index.ts +6 -3
  44. package/src/index.ts +15 -0
  45. package/src/query/filter.ts +4 -10
  46. package/src/query/query.ts +12 -12
  47. package/src/storage/serialization.ts +29 -2
  48. package/src/types/index.ts +71 -0
  49. package/src/world/archetype-manager.ts +283 -0
  50. package/src/world/command-executor.ts +258 -0
  51. package/src/world/commands.ts +44 -56
  52. package/src/world/debug-stats.ts +147 -0
  53. package/src/world/hooks.ts +8 -0
  54. package/src/world/operations.ts +88 -0
  55. package/src/world/serialization.ts +32 -18
  56. package/src/world/singleton.ts +51 -0
  57. package/src/world/world.ts +429 -457
@@ -1,40 +1,36 @@
1
- import { Archetype } from "../archetype/archetype";
2
- import { DontFragmentStoreImpl } from "../archetype/store";
3
- import { CommandBuffer, type Command } from "../commands/buffer";
4
- import { ComponentChangeset } from "../commands/changeset";
1
+ import type { Archetype } from "../archetype/archetype";
2
+ import { SparseStoreImpl } from "../archetype/store";
3
+ import { CommandBuffer } from "../commands/buffer";
5
4
  import { ComponentEntityStore } from "../component/entity-store";
6
5
  import { normalizeComponentTypes } from "../component/type-utils";
7
6
  import type { ComponentId, EntityId, WildcardRelationId } from "../entity";
8
7
  import {
9
- ENTITY_ID_START,
10
8
  EntityIdManager,
11
9
  RELATION_SHIFT,
12
10
  getComponentIdFromRelationId,
13
- getDetailedIdType,
14
- getTargetIdFromRelationId,
15
11
  isCascadeDeleteRelation,
16
- isDontFragmentRelation,
17
- isDontFragmentWildcard,
18
- isEntityRelation,
19
- isExclusiveComponent,
12
+ isSparseRelation,
20
13
  isWildcardRelationId,
14
+ relation,
21
15
  } from "../entity";
22
- import { matchesFilter, serializeQueryFilter, type QueryFilter } from "../query/filter";
16
+ import { serializeQueryFilter, type QueryFilter } from "../query/filter";
23
17
  import type { Query } from "../query/query";
24
18
  import { QueryRegistry } from "../query/registry";
25
19
  import type { SerializedWorld } from "../storage/serialization";
26
- import type { ComponentTuple, ComponentType, LifecycleCallback, LifecycleHook, LifecycleHookEntry } from "../types";
20
+ import type {
21
+ ComponentTuple,
22
+ ComponentType,
23
+ DebugStatsCollector,
24
+ LifecycleCallback,
25
+ LifecycleHook,
26
+ LifecycleHookEntry,
27
+ SyncDebugStats,
28
+ } from "../types";
27
29
  import { isOptionalEntityId } from "../types";
28
- import { getOrCompute } from "../utils/utils";
30
+ import { ArchetypeManager } from "./archetype-manager";
29
31
  import { EntityBuilder } from "./builder";
30
- import {
31
- applyChangeset,
32
- filterRegularComponentTypes,
33
- maybeRemoveWildcardMarker,
34
- processCommands,
35
- removeMatchingRelations,
36
- type CommandProcessorContext,
37
- } from "./commands";
32
+ import { CommandExecutor, type CommandExecutorContext } from "./command-executor";
33
+ import { DebugStatsManager } from "./debug-stats";
38
34
  import {
39
35
  collectMultiHookComponents,
40
36
  triggerLifecycleHooks,
@@ -42,12 +38,14 @@ import {
42
38
  type HooksContext,
43
39
  } from "./hooks";
44
40
  import {
45
- getEntityReferences,
46
- trackEntityReference,
47
- untrackEntityReference,
48
- type EntityReferencesMap,
49
- } from "./references";
41
+ assertEntityExists,
42
+ assertSetComponentTypeValid,
43
+ resolveRemoveOperation,
44
+ resolveSetOperation,
45
+ } from "./operations";
46
+ import { getEntityReferences, type EntityReferencesMap } from "./references";
50
47
  import { deserializeWorld, serializeWorld } from "./serialization";
48
+ import { SingletonHandle } from "./singleton";
51
49
 
52
50
  /**
53
51
  * World class for ECS architecture
@@ -56,44 +54,59 @@ import { deserializeWorld, serializeWorld } from "./serialization";
56
54
  export class World {
57
55
  // Core data structures for entity and archetype management
58
56
  private entityIdManager = new EntityIdManager();
59
- private archetypes: Archetype[] = [];
60
- private archetypeBySignature = new Map<string, Archetype>();
61
- private entityToArchetype = new Map<EntityId, Archetype>();
62
- private archetypesByComponent = new Map<EntityId<any>, Set<Archetype>>();
63
57
  private entityReferences: EntityReferencesMap = new Map();
64
- /** Reverse index: entity ID set of archetypes whose componentTypes include that entity ID */
65
- private entityToReferencingArchetypes = new Map<EntityId, Set<Archetype>>();
66
- /** DontFragment relation storage, shared with all Archetype instances */
67
- private readonly dontFragmentStore = new DontFragmentStoreImpl();
58
+ /** Sparse relation storage (for components created with `sparse: true`), shared with all Archetype instances */
59
+ private readonly sparseStore = new SparseStoreImpl();
68
60
  /** Component entity (singleton) storage */
69
61
  private readonly componentEntities = new ComponentEntityStore();
70
62
 
63
+ // Archetype storage, indexes, creation/removal, and referencing are now encapsulated
64
+ // in ArchetypeManager (extracted to reduce World line count and improve cohesion).
65
+ private archetypeManager!: ArchetypeManager;
66
+
67
+ // Temporary forwarding accessors so the rest of World (and its internal collaborators)
68
+ // can continue using the familiar names with almost zero call-site changes.
69
+ // These can be removed in a follow-up cleanup pass if desired.
70
+ private get archetypes() {
71
+ return this.archetypeManager.archetypes;
72
+ }
73
+ private get entityToArchetype() {
74
+ return this.archetypeManager.entityToArchetype;
75
+ }
76
+ private get archetypesByComponent() {
77
+ return this.archetypeManager.archetypesByComponent;
78
+ }
79
+ private get entityToReferencingArchetypes() {
80
+ return this.archetypeManager.entityToReferencingArchetypes;
81
+ }
82
+
71
83
  // Query registry – manages caching, ref counts, and archetype notifications
72
84
  private readonly queryRegistry = new QueryRegistry();
73
85
 
74
86
  // Lifecycle hooks (declared before cached contexts that reference them)
75
87
  private hooks: Set<LifecycleHookEntry> = new Set();
76
88
 
77
- // Command execution
78
- private commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
79
-
80
- // Reusable instances to reduce per-frame allocations
81
- private readonly _changeset = new ComponentChangeset();
82
- private readonly _removeChangeset = new ComponentChangeset();
83
- /** Cached command processor context to avoid per-entity object allocation */
84
- private readonly _commandCtx: CommandProcessorContext = {
85
- dontFragmentStore: this.dontFragmentStore,
86
- ensureArchetype: (ct) => this.ensureArchetype(ct),
87
- };
88
- /** Cached hooks context to avoid per-entity object allocation */
89
- private readonly _hooksCtx: HooksContext = {
90
- multiHooks: this.hooks,
91
- has: (eid, ct) => this.has(eid, ct),
92
- get: (eid, ct) => this.get(eid, ct),
93
- getOptional: (eid, ct) => this.getOptional(eid, ct),
94
- };
89
+ // Debug observability (extracted to DebugStatsManager to reduce World line count)
90
+ private readonly debugStats = new DebugStatsManager();
91
+
92
+ // Command execution (orchestration extracted to CommandExecutor)
93
+ private commandBuffer!: CommandBuffer;
94
+ private commandExecutor!: CommandExecutor;
95
95
 
96
96
  constructor(snapshot?: SerializedWorld) {
97
+ // Must create the manager before any code that may invoke ensureArchetype
98
+ // (including the snapshot deserialization path below, and the closures
99
+ // captured in the field-initialized _commandCtx).
100
+ this.archetypeManager = new ArchetypeManager(
101
+ {
102
+ queryRegistry: this.queryRegistry,
103
+ hooks: this.hooks,
104
+ recordArchetypeCreated: () => this.debugStats.recordArchetypeCreated(),
105
+ recordArchetypeRemoved: () => this.debugStats.recordArchetypeRemoved(),
106
+ },
107
+ this.sparseStore,
108
+ );
109
+
97
110
  if (snapshot && typeof snapshot === "object") {
98
111
  deserializeWorld(
99
112
  {
@@ -101,15 +114,34 @@ export class World {
101
114
  componentEntities: this.componentEntities,
102
115
  entityReferences: this.entityReferences,
103
116
  ensureArchetype: (ct) => this.ensureArchetype(ct),
104
- setEntityToArchetype: (eid, arch) => this.entityToArchetype.set(eid, arch),
117
+ setEntityToArchetype: (eid, arch) => this.archetypeManager.entityToArchetype.set(eid, arch),
105
118
  },
106
119
  snapshot,
107
120
  );
108
121
  }
109
- }
110
122
 
111
- private createArchetypeSignature(componentTypes: EntityId<any>[]): string {
112
- return componentTypes.join(",");
123
+ // CommandExecutor must be created after archetypeManager (and debugStats) because its context
124
+ // closes over several pieces and the destroy fast path.
125
+ const execCtx: CommandExecutorContext = {
126
+ componentEntities: this.componentEntities,
127
+ entityReferences: this.entityReferences,
128
+ hooks: this.hooks,
129
+ entityToArchetype: this.entityToArchetype,
130
+ ensureArchetype: (ct) => this.ensureArchetype(ct),
131
+ sparseStore: this.sparseStore,
132
+ has: (eid, ct) => this.has(eid, ct),
133
+ get: (eid, ct) => this.get(eid, ct),
134
+ getOptional: (eid, ct) => this.getOptional(eid, ct),
135
+ destroyEntityImmediate: (eid) => this.destroyEntityImmediate(eid),
136
+ incrementMigrations: () => this.debugStats.incrementMigrations(),
137
+ triggerLifecycleHooks,
138
+ triggerRemoveHooksForEntityDeletion,
139
+ };
140
+ this.commandExecutor = new CommandExecutor(execCtx);
141
+
142
+ this.commandBuffer = new CommandBuffer((entityId, commands) =>
143
+ this.commandExecutor.executeEntityCommands(entityId, commands),
144
+ );
113
145
  }
114
146
 
115
147
  /**
@@ -198,7 +230,7 @@ export class World {
198
230
  }
199
231
  }
200
232
 
201
- // Remove entity from archetype - this also cleans up dontFragment relations
233
+ // Remove entity from archetype - this also cleans up sparse relations
202
234
  // and returns all removed component data
203
235
  this.entityReferences.delete(cur);
204
236
  const removedComponents = archetype.removeEntity(cur)!;
@@ -237,71 +269,6 @@ export class World {
237
269
  return this.entityToArchetype.has(entityId);
238
270
  }
239
271
 
240
- private assertEntityExists(entityId: EntityId, label: "Entity" | "Component entity"): void {
241
- if (!this.exists(entityId)) {
242
- throw new Error(`${label} ${entityId} does not exist`);
243
- }
244
- }
245
-
246
- private assertComponentTypeValid(componentType: EntityId): void {
247
- const detailedType = getDetailedIdType(componentType);
248
- if (detailedType.type === "invalid") {
249
- throw new Error(`Invalid component type: ${componentType}`);
250
- }
251
- }
252
-
253
- private assertSetComponentTypeValid(componentType: EntityId): void {
254
- const detailedType = getDetailedIdType(componentType);
255
- if (detailedType.type === "invalid") {
256
- throw new Error(`Invalid component type: ${componentType}`);
257
- }
258
- if (detailedType.type === "wildcard-relation") {
259
- throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
260
- }
261
- }
262
-
263
- private resolveSetOperation(
264
- entityId: EntityId | ComponentId,
265
- componentTypeOrComponent?: EntityId | any,
266
- maybeComponent?: any,
267
- ): { entityId: EntityId; componentType: EntityId; component: any } {
268
- // Handle singleton component overload: set(componentId, data)
269
- if (maybeComponent === undefined && componentTypeOrComponent !== undefined) {
270
- const detailedType = getDetailedIdType(entityId);
271
- if (detailedType.type === "component" || detailedType.type === "component-relation") {
272
- const componentId = entityId as ComponentId;
273
- this.assertEntityExists(componentId, "Component entity");
274
- this.assertSetComponentTypeValid(componentId);
275
- return { entityId: componentId, componentType: componentId, component: componentTypeOrComponent };
276
- }
277
- }
278
-
279
- const targetEntityId = entityId as EntityId;
280
- const componentType = componentTypeOrComponent as EntityId;
281
- this.assertEntityExists(targetEntityId, "Entity");
282
- this.assertSetComponentTypeValid(componentType);
283
-
284
- return { entityId: targetEntityId, componentType, component: maybeComponent };
285
- }
286
-
287
- private resolveRemoveOperation<T>(
288
- entityId: EntityId | ComponentId,
289
- componentType?: EntityId<T>,
290
- ): { entityId: EntityId; componentType: EntityId } {
291
- // Handle singleton component overload: remove(componentId)
292
- if (componentType === undefined) {
293
- const componentId = entityId as ComponentId<T>;
294
- this.assertEntityExists(componentId, "Component entity");
295
- return { entityId: componentId, componentType: componentId };
296
- }
297
-
298
- const targetEntityId = entityId as EntityId;
299
- this.assertEntityExists(targetEntityId, "Entity");
300
- this.assertComponentTypeValid(componentType);
301
-
302
- return { entityId: targetEntityId, componentType };
303
- }
304
-
305
272
  /**
306
273
  * Adds or updates a component on an entity (or marks void component as present).
307
274
  * The change is buffered and takes effect after calling `world.sync()`.
@@ -313,27 +280,23 @@ export class World {
313
280
  * @overload set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void
314
281
  * Adds or updates a component with data on the entity
315
282
  *
316
- * @overload set<T>(componentId: ComponentId<T>, component: NoInfer<T>): void
317
- * Adds or updates a singleton component (shorthand for set(componentId, componentId, component))
318
- *
319
283
  * @throws {Error} If the entity does not exist
320
284
  * @throws {Error} If the component type is invalid or is a wildcard relation
321
285
  *
322
286
  * @example
323
287
  * world.set(entity, Position, { x: 10, y: 20 });
324
288
  * world.set(entity, Marker); // void component
325
- * world.set(GlobalConfig, { debug: true }); // singleton component
289
+ * world.singleton(GlobalConfig).set({ debug: true }); // singleton component
326
290
  * world.sync(); // Apply changes
327
291
  */
328
292
  set(entityId: EntityId, componentType: EntityId<void>): void;
329
293
  set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void;
330
- set<T>(componentId: ComponentId<T>, component: NoInfer<T>): void;
331
294
  set(entityId: EntityId | ComponentId, componentTypeOrComponent?: EntityId | any, maybeComponent?: any): void {
332
295
  const {
333
296
  entityId: targetEntityId,
334
297
  componentType,
335
298
  component,
336
- } = this.resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent);
299
+ } = resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent, (id) => this.exists(id));
337
300
  this.commandBuffer.set(targetEntityId, componentType, component);
338
301
  }
339
302
 
@@ -363,9 +326,10 @@ export class World {
363
326
  remove<T>(componentId: ComponentId<T>): void;
364
327
  remove<T>(entityId: EntityId, componentType: EntityId<T>): void;
365
328
  remove<T>(entityId: EntityId | ComponentId, componentType?: EntityId<T>): void {
366
- const { entityId: targetEntityId, componentType: targetComponentType } = this.resolveRemoveOperation(
329
+ const { entityId: targetEntityId, componentType: targetComponentType } = resolveRemoveOperation(
367
330
  entityId,
368
331
  componentType,
332
+ (id) => this.exists(id),
369
333
  );
370
334
  this.commandBuffer.remove(targetEntityId, targetComponentType);
371
335
  }
@@ -385,6 +349,32 @@ export class World {
385
349
  this.commandBuffer.delete(entityId);
386
350
  }
387
351
 
352
+ /**
353
+ * Returns an explicit handle for a singleton component (component-as-entity).
354
+ *
355
+ * This is the preferred API for singleton components.
356
+ *
357
+ * @example
358
+ * const config = world.singleton(GlobalConfig);
359
+ * config.set({ debug: true });
360
+ * world.sync();
361
+ * console.log(config.get());
362
+ */
363
+ singleton<T>(componentId: ComponentId<T>): SingletonHandle<T> {
364
+ assertEntityExists(componentId, "Component entity", (id) => this.exists(id));
365
+ assertSetComponentTypeValid(componentId);
366
+
367
+ return new SingletonHandle(componentId, {
368
+ has: () => this.componentEntities.hasSingleton(componentId),
369
+ get: () => this.get(componentId),
370
+ getOptional: () => this.getOptional(componentId),
371
+ remove: () => this.commandBuffer.remove(componentId, componentId),
372
+ set: (value) => {
373
+ this.commandBuffer.set(componentId, componentId as EntityId<any>, value as any);
374
+ },
375
+ });
376
+ }
377
+
388
378
  /**
389
379
  * Checks if a specific **component** is present on an entity.
390
380
  *
@@ -443,11 +433,11 @@ export class World {
443
433
 
444
434
  if (archetype.componentTypeSet.has(componentType)) return true;
445
435
 
446
- if (isDontFragmentRelation(componentType)) {
436
+ if (isSparseRelation(componentType)) {
447
437
  // Use getValue; presence check via getAllForEntity only if value can legitimately be undefined
448
- const val = this.dontFragmentStore.getValue(entityId, componentType);
438
+ const val = this.sparseStore.getValue(entityId, componentType);
449
439
  if (val !== undefined) return true;
450
- return this.dontFragmentStore.getAllForEntity(entityId).some(([t]) => t === componentType);
440
+ return this.sparseStore.getAllForEntity(entityId).some(([t]) => t === componentType);
451
441
  }
452
442
 
453
443
  return false;
@@ -495,12 +485,12 @@ export class World {
495
485
 
496
486
  if (componentType >= 0 || componentType % RELATION_SHIFT !== 0) {
497
487
  const inArchetype = archetype.componentTypeSet.has(componentType);
498
- const hasDontFragment = isDontFragmentRelation(componentType);
488
+ const hasSparse = isSparseRelation(componentType);
499
489
  const hasComponent =
500
490
  inArchetype ||
501
- (hasDontFragment &&
502
- (this.dontFragmentStore.getValue(entityId, componentType) !== undefined ||
503
- this.dontFragmentStore.getAllForEntity(entityId).some(([t]) => t === componentType)));
491
+ (hasSparse &&
492
+ (this.sparseStore.getValue(entityId, componentType) !== undefined ||
493
+ this.sparseStore.getAllForEntity(entityId).some(([t]) => t === componentType)));
504
494
 
505
495
  if (!hasComponent) {
506
496
  throw new Error(
@@ -571,6 +561,213 @@ export class World {
571
561
  return archetype.getOptional(entityId, componentType);
572
562
  }
573
563
 
564
+ // ==========================================================================
565
+ // Relation & Hierarchy Companion Tools (public API)
566
+ // ==========================================================================
567
+
568
+ /**
569
+ * Retrieves all targets (and their associated data) for relations of a given
570
+ * base component on an entity.
571
+ *
572
+ * This is the ergonomic replacement for the common pattern:
573
+ * world.get(entity, relation(Comp, "*"))
574
+ *
575
+ * @example
576
+ * const ChildOf = component({ exclusive: true, sparse: true });
577
+ * const children = world.getRelationTargets(parent, ChildOf); // usually []
578
+ * const items = world.getRelationTargets(player, InInventory);
579
+ *
580
+ * // For common hierarchy use cases, prefer the higher-level helpers:
581
+ * // world.getChildren(parent, ChildOf), world.getParent(child, ChildOf)
582
+ */
583
+ getRelationTargets<T = void>(
584
+ entityId: EntityId,
585
+ relationComp: ComponentId<T>,
586
+ ): [target: EntityId<unknown>, data: T | undefined][] {
587
+ assertEntityExists(entityId, "Entity", (id) => this.exists(id));
588
+
589
+ const wildcard = relation(relationComp, "*") as WildcardRelationId<T>;
590
+
591
+ // For component entities (singletons) the path is different; they rarely host relations
592
+ if (this.componentEntities.exists(entityId)) {
593
+ return this.componentEntities.getWildcard(entityId, wildcard);
594
+ }
595
+
596
+ // Regular entity path — archetype.get for wildcard always materializes the array
597
+ // (even if empty for a sparse relation that only has the marker)
598
+ const data = this.get(entityId, wildcard);
599
+ return data as [EntityId<unknown>, T | undefined][];
600
+ }
601
+
602
+ /**
603
+ * Returns every entity that currently holds a relation of the given base
604
+ * component pointing at `targetId`.
605
+ *
606
+ * This is the efficient **reverse** lookup. For common hierarchy cases,
607
+ * prefer the higher-level `world.getChildren(parent, ChildOf)` instead.
608
+ *
609
+ * @example
610
+ * const ChildOf = component({ exclusive: true, sparse: true });
611
+ * const directChildren = world.getRelationSources(ship, ChildOf);
612
+ */
613
+ getRelationSources(targetId: EntityId, relationComp: ComponentId<any>): EntityId[] {
614
+ const refs = getEntityReferences(this.entityReferences, targetId);
615
+ const result: EntityId[] = [];
616
+
617
+ for (const [source, relType] of refs) {
618
+ // Only consider still-living sources
619
+ if (!this.entityToArchetype.has(source) && !this.componentEntities.exists(source)) continue;
620
+
621
+ const decodedComp = getComponentIdFromRelationId(relType);
622
+ if (decodedComp === relationComp) {
623
+ result.push(source);
624
+ }
625
+ }
626
+ return result;
627
+ }
628
+
629
+ /**
630
+ * Returns true if the entity has any (or a specific-target) relation of the
631
+ * given base component.
632
+ */
633
+ hasRelation(entityId: EntityId, relationComp: ComponentId<any>, targetId?: EntityId): boolean {
634
+ assertEntityExists(entityId, "Entity", (id) => this.exists(id));
635
+
636
+ if (targetId !== undefined) {
637
+ const specific = relation(relationComp, targetId);
638
+ return this.has(entityId, specific);
639
+ }
640
+
641
+ // Any target of this relation kind?
642
+ const targets = this.getRelationTargets(entityId, relationComp);
643
+ return targets.length > 0;
644
+ }
645
+
646
+ /**
647
+ * Returns the number of relations of the given base component held by the entity.
648
+ */
649
+ countRelations(entityId: EntityId, relationComp: ComponentId<any>): number {
650
+ assertEntityExists(entityId, "Entity", (id) => this.exists(id));
651
+ const targets = this.getRelationTargets(entityId, relationComp);
652
+ return targets.length;
653
+ }
654
+
655
+ /**
656
+ * For an *exclusive* relation (e.g. ChildOf, Owner), returns the single
657
+ * target entity (or undefined if none).
658
+ *
659
+ * When the component was declared `exclusive: true`, this is the preferred
660
+ * accessor (clearer intent than array destructuring).
661
+ */
662
+ getSingleRelationTarget<T = void>(entityId: EntityId, relationComp: ComponentId<T>): EntityId | undefined {
663
+ const targets = this.getRelationTargets(entityId, relationComp);
664
+ return targets.length > 0 ? (targets[0]![0] as EntityId) : undefined;
665
+ }
666
+
667
+ // --------------------------------------------------------------------------
668
+ // High-level hierarchy helpers (convenience methods on World)
669
+ // --------------------------------------------------------------------------
670
+
671
+ /**
672
+ * Returns the direct children of `parent` for the given relationship component
673
+ * (typically a `ChildOf` or similar exclusive `sparse` relation).
674
+ *
675
+ * This is the recommended high-level API for hierarchy traversal.
676
+ * It uses the internal reverse reference index for efficiency.
677
+ *
678
+ * @example
679
+ * const ChildOf = component({ exclusive: true, sparse: true });
680
+ * const kids = world.getChildren(ship, ChildOf);
681
+ */
682
+ getChildren(parent: EntityId, childOf: ComponentId<any>): EntityId[] {
683
+ return this.getRelationSources(parent, childOf);
684
+ }
685
+
686
+ /**
687
+ * Returns the parent of `child` for the given relationship component
688
+ * (typically an exclusive `ChildOf` relation).
689
+ *
690
+ * @example
691
+ * const ChildOf = component({ exclusive: true, sparse: true });
692
+ * const parent = world.getParent(turret, ChildOf);
693
+ */
694
+ getParent(child: EntityId, childOf: ComponentId<any>): EntityId | undefined {
695
+ return this.getSingleRelationTarget(child, childOf);
696
+ }
697
+
698
+ /**
699
+ * Returns the ancestor chain from the immediate parent up to (but not
700
+ * including) the root for the given relationship component.
701
+ *
702
+ * @example
703
+ * const ChildOf = component({ exclusive: true, sparse: true });
704
+ * const ancestors = world.getAncestors(muzzle, ChildOf); // [turret, ship]
705
+ */
706
+ getAncestors(entity: EntityId, childOf: ComponentId<any>): EntityId[] {
707
+ const ancestors: EntityId[] = [];
708
+ let cur = this.getParent(entity, childOf);
709
+ while (cur !== undefined) {
710
+ ancestors.push(cur);
711
+ cur = this.getParent(cur, childOf);
712
+ }
713
+ return ancestors;
714
+ }
715
+
716
+ /**
717
+ * Iteratively traverses all descendants of `root` in DFS pre-order.
718
+ * This is a generator and is safe for very deep hierarchies.
719
+ *
720
+ * @example
721
+ * for (const { entity, depth, parent } of world.iterateDescendants(root, ChildOf)) {
722
+ * console.log(depth, entity);
723
+ * }
724
+ */
725
+ *iterateDescendants(
726
+ root: EntityId,
727
+ childOf: ComponentId<any>,
728
+ opts: { includeSelf?: boolean; maxDepth?: number } = {},
729
+ ): IterableIterator<{ entity: EntityId; depth: number; parent: EntityId | null }> {
730
+ const { includeSelf = false, maxDepth } = opts;
731
+ const stack: Array<{ entity: EntityId; depth: number; parent: EntityId | null }> = [];
732
+
733
+ if (includeSelf) {
734
+ stack.push({ entity: root, depth: 0, parent: null });
735
+ } else {
736
+ for (const child of this.getChildren(root, childOf)) {
737
+ stack.push({ entity: child, depth: 1, parent: root });
738
+ }
739
+ }
740
+
741
+ while (stack.length > 0) {
742
+ const current = stack.pop()!;
743
+ if (maxDepth !== undefined && current.depth > maxDepth) continue;
744
+
745
+ yield current;
746
+
747
+ const kids = this.getChildren(current.entity, childOf);
748
+ for (let i = kids.length - 1; i >= 0; i--) {
749
+ const k = kids[i]!;
750
+ stack.push({ entity: k, depth: current.depth + 1, parent: current.entity });
751
+ }
752
+ }
753
+ }
754
+
755
+ /**
756
+ * Callback-based descendant traversal (hot path friendly).
757
+ * Return `false` from the visitor to stop early.
758
+ */
759
+ traverseDescendants(
760
+ root: EntityId,
761
+ childOf: ComponentId<any>,
762
+ visitor: (entity: EntityId, depth: number, parent: EntityId | null) => void | boolean,
763
+ opts: { includeSelf?: boolean; maxDepth?: number } = {},
764
+ ): void {
765
+ for (const { entity, depth, parent } of this.iterateDescendants(root, childOf, opts)) {
766
+ const res = visitor(entity, depth, parent);
767
+ if (res === false) return;
768
+ }
769
+ }
770
+
574
771
  /**
575
772
  * Registers a lifecycle hook that responds to component changes.
576
773
  * The hook callback is invoked when components matching the specified types are added, updated, or removed.
@@ -684,6 +881,22 @@ export class World {
684
881
  };
685
882
  }
686
883
 
884
+ /**
885
+ * Creates a debug stats collector that will receive a `SyncDebugStats` payload
886
+ * after every subsequent `sync()`.
887
+ *
888
+ * The returned object is a pure lifecycle handle. It does not store data.
889
+ * Collection stops when you call `[Symbol.dispose]()` (or use a `using` declaration).
890
+ *
891
+ * All active collectors receive the exact same stats object for a given sync.
892
+ * Exceptions thrown by callbacks are ignored.
893
+ *
894
+ * This is intended for development/debugging and leak detection.
895
+ */
896
+ createDebugStatsCollector(callback: (stats: SyncDebugStats) => void): DebugStatsCollector {
897
+ return this.debugStats.createCollector(callback);
898
+ }
899
+
687
900
  /**
688
901
  * Synchronizes all buffered commands (set/remove/delete) to the world.
689
902
  * This method must be called after making changes via `set()`, `remove()`, or `delete()` for them to take effect.
@@ -695,7 +908,56 @@ export class World {
695
908
  * world.sync(); // Apply all buffered changes
696
909
  */
697
910
  sync(): void {
698
- this.commandBuffer.execute();
911
+ if (!this.debugStats.hasActiveCollectors()) {
912
+ // Fast path: no debug collectors, skip all timing and stats work
913
+ this.commandBuffer.execute();
914
+ return;
915
+ }
916
+
917
+ // Slow path: full instrumentation for active debug stats collectors
918
+ const syncStart = performance.now();
919
+ this.debugStats.resetActivity();
920
+
921
+ const commandBufferStart = performance.now();
922
+ const commandIterations = this.commandBuffer.execute();
923
+ const commandBufferEnd = performance.now();
924
+
925
+ const syncEnd = performance.now();
926
+
927
+ // Build the data bag for the extracted manager (keeps it decoupled from internal maps)
928
+ const entityCount = this.entityToArchetype.size;
929
+ let emptyArchetypes = 0;
930
+ for (const arch of this.archetypes) {
931
+ if (arch.size === 0) emptyArchetypes++;
932
+ }
933
+
934
+ let archetypesByComponentSize = 0;
935
+ for (const set of this.archetypesByComponent.values()) {
936
+ archetypesByComponentSize += set.size;
937
+ }
938
+
939
+ this.debugStats.deliver(
940
+ {
941
+ syncStart,
942
+ syncEnd,
943
+ commandBufferStart,
944
+ commandBufferEnd,
945
+ commandIterations,
946
+ },
947
+ {
948
+ entityCount,
949
+ freelistSize: this.entityIdManager.getFreelistSize(),
950
+ nextId: this.entityIdManager.getNextId(),
951
+ archetypeCount: this.archetypes.length,
952
+ emptyArchetypes,
953
+ archetypesByComponentSize,
954
+ cachedQueryCount: (this.queryRegistry as any).cache?.size ?? 0,
955
+ registeredQueryCount: (this.queryRegistry as any).queries?.size ?? 0,
956
+ hookCount: this.hooks.size,
957
+ entityReferencesSize: this.entityReferences.size,
958
+ entityToReferencingArchetypesSize: this.entityToReferencingArchetypes.size,
959
+ },
960
+ );
699
961
  }
700
962
 
701
963
  /**
@@ -732,7 +994,7 @@ export class World {
732
994
  createQuery(componentTypes: EntityId<any>[], filter: QueryFilter = {}): Query {
733
995
  const sortedTypes = normalizeComponentTypes(componentTypes);
734
996
  const filterKey = serializeQueryFilter(filter);
735
- const key = `${this.createArchetypeSignature(sortedTypes)}${filterKey ? `|${filterKey}` : ""}`;
997
+ const key = `${sortedTypes.join(",")}${filterKey ? `|${filterKey}` : ""}`;
736
998
  return this.queryRegistry.getOrCreate(this, sortedTypes, key, filter);
737
999
  }
738
1000
 
@@ -802,71 +1064,7 @@ export class World {
802
1064
  * @internal
803
1065
  */
804
1066
  getMatchingArchetypes(componentTypes: EntityId<any>[]): Archetype[] {
805
- if (componentTypes.length === 0) {
806
- return [...this.archetypes];
807
- }
808
-
809
- const regularComponents: EntityId<any>[] = [];
810
- const wildcardRelations: { componentId: ComponentId<any>; relationId: EntityId<any> }[] = [];
811
-
812
- for (const componentType of componentTypes) {
813
- if (isWildcardRelationId(componentType)) {
814
- const componentId = getComponentIdFromRelationId(componentType);
815
- if (componentId !== undefined) {
816
- wildcardRelations.push({ componentId, relationId: componentType });
817
- }
818
- } else {
819
- regularComponents.push(componentType);
820
- }
821
- }
822
-
823
- let matchingArchetypes = this.getArchetypesWithComponents(regularComponents);
824
-
825
- for (const { componentId, relationId } of wildcardRelations) {
826
- const markerSet = this.archetypesByComponent.get(relationId);
827
- const archetypesWithMarker = markerSet ? Array.from(markerSet) : [];
828
- matchingArchetypes =
829
- matchingArchetypes.length === 0
830
- ? archetypesWithMarker
831
- : matchingArchetypes.filter((a) => markerSet?.has(a) || a.hasRelationWithComponentId(componentId));
832
- }
833
-
834
- return matchingArchetypes;
835
- }
836
-
837
- private getArchetypesWithComponents(componentTypes: EntityId<any>[]): Archetype[] {
838
- if (componentTypes.length === 0) return [...this.archetypes];
839
- if (componentTypes.length === 1) {
840
- const set = this.archetypesByComponent.get(componentTypes[0]!);
841
- return set ? Array.from(set) : [];
842
- }
843
-
844
- // Sort by Set size, intersect starting from the smallest
845
- const sets = componentTypes
846
- .map((type) => this.archetypesByComponent.get(type))
847
- .filter((s): s is Set<Archetype> => s !== undefined && s.size > 0)
848
- .sort((a, b) => a.size - b.size);
849
-
850
- if (sets.length === 0) return [];
851
- if (sets.length < componentTypes.length) return []; // One component has no matching archetypes
852
-
853
- const smallest = sets[0]!;
854
-
855
- // 2-component fast path
856
- if (sets.length === 2) {
857
- const other = sets[1]!;
858
- return Array.from(smallest).filter((a) => other.has(a));
859
- }
860
-
861
- // Multi-component intersection
862
- let result = new Set(smallest);
863
- for (let i = 1; i < sets.length; i++) {
864
- for (const item of result) {
865
- if (!sets[i]!.has(item)) result.delete(item);
866
- }
867
- if (result.size === 0) return [];
868
- }
869
- return Array.from(result);
1067
+ return this.archetypeManager.getMatchingArchetypes(componentTypes);
870
1068
  }
871
1069
 
872
1070
  /**
@@ -924,264 +1122,17 @@ export class World {
924
1122
  }
925
1123
  }
926
1124
 
927
- private executeEntityCommands(entityId: EntityId, commands: Command[]): void {
928
- this._changeset.clear();
929
-
930
- // 1. Route: component entities use flat-map storage
931
- if (this.componentEntities.exists(entityId)) {
932
- this.componentEntities.executeCommands(entityId, commands);
933
- return;
934
- }
935
-
936
- // 2. Route: destroy uses fast path
937
- if (commands.some((cmd) => cmd.type === "destroy")) {
938
- this.destroyEntityImmediate(entityId);
939
- return;
940
- }
941
-
942
- // 3. Apply structural changes
943
- this.applyEntityCommands(entityId, commands);
944
- }
945
-
946
- private applyEntityCommands(entityId: EntityId, commands: Command[]): void {
947
- const currentArchetype = this.entityToArchetype.get(entityId);
948
- if (!currentArchetype) return;
949
-
950
- const changeset = this._changeset;
951
- processCommands(entityId, currentArchetype, commands, changeset, (eid, arch, compId) => {
952
- if (isExclusiveComponent(compId)) {
953
- removeMatchingRelations(eid, arch, compId, changeset);
954
- }
955
- });
956
-
957
- const hasStructuralChange = changeset.removes.size > 0 || changeset.adds.size > 0;
958
-
959
- if (this.hooks.size === 0) {
960
- // Fast path: no hooks, skip removedComponents map allocation and hook triggering
961
- applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype, null);
962
- if (hasStructuralChange) {
963
- this.updateEntityReferences(entityId, changeset);
964
- }
965
- return;
966
- }
967
-
968
- const removedComponents = new Map<EntityId<any>, any>();
969
- const newArchetype = applyChangeset(
970
- this._commandCtx,
971
- entityId,
972
- currentArchetype,
973
- changeset,
974
- this.entityToArchetype,
975
- removedComponents,
976
- );
977
-
978
- if (hasStructuralChange) {
979
- this.updateEntityReferences(entityId, changeset);
980
- }
981
- triggerLifecycleHooks(
982
- this.createHooksContext(),
983
- entityId,
984
- changeset.adds,
985
- removedComponents,
986
- currentArchetype,
987
- newArchetype,
988
- );
989
- }
990
-
991
- private createHooksContext(): HooksContext {
992
- return this._hooksCtx;
993
- }
994
-
995
- private removeComponentImmediate(entityId: EntityId, componentType: EntityId<any>, targetEntityId: EntityId): void {
996
- const sourceArchetype = this.entityToArchetype.get(entityId);
997
- if (!sourceArchetype) return;
998
-
999
- const changeset = this._removeChangeset;
1000
- changeset.clear();
1001
- changeset.delete(componentType);
1002
- maybeRemoveWildcardMarker(
1003
- entityId,
1004
- sourceArchetype,
1005
- componentType,
1006
- getComponentIdFromRelationId(componentType),
1007
- changeset,
1008
- );
1009
-
1010
- const removedComponent = sourceArchetype.get(entityId, componentType);
1011
- const newArchetype = applyChangeset(
1012
- this._commandCtx,
1013
- entityId,
1014
- sourceArchetype,
1015
- changeset,
1016
- this.entityToArchetype,
1017
- null,
1018
- );
1019
- untrackEntityReference(this.entityReferences, entityId, componentType, targetEntityId);
1020
- triggerLifecycleHooks(
1021
- this.createHooksContext(),
1022
- entityId,
1023
- new Map(),
1024
- new Map([[componentType, removedComponent]]),
1025
- sourceArchetype,
1026
- newArchetype,
1027
- );
1028
- }
1029
-
1030
- private updateEntityReferences(entityId: EntityId, changeset: ComponentChangeset): void {
1031
- for (const componentType of changeset.removes) {
1032
- if (isEntityRelation(componentType)) {
1033
- const targetId = getTargetIdFromRelationId(componentType)!;
1034
- untrackEntityReference(this.entityReferences, entityId, componentType, targetId);
1035
- } else if (componentType >= ENTITY_ID_START) {
1036
- untrackEntityReference(this.entityReferences, entityId, componentType, componentType);
1037
- }
1038
- }
1039
-
1040
- for (const [componentType] of changeset.adds) {
1041
- if (isEntityRelation(componentType)) {
1042
- const targetId = getTargetIdFromRelationId(componentType)!;
1043
- trackEntityReference(this.entityReferences, entityId, componentType, targetId);
1044
- } else if (componentType >= ENTITY_ID_START) {
1045
- trackEntityReference(this.entityReferences, entityId, componentType, componentType);
1046
- }
1047
- }
1048
- }
1049
-
1125
+ // Delegators to the extracted ArchetypeManager (keeps public + internal call sites unchanged).
1050
1126
  private ensureArchetype(componentTypes: Iterable<EntityId<any>>): Archetype {
1051
- const regularTypes = filterRegularComponentTypes(componentTypes);
1052
- const sortedTypes = normalizeComponentTypes(regularTypes);
1053
- const hashKey = this.createArchetypeSignature(sortedTypes);
1054
-
1055
- return getOrCompute(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
1056
- }
1057
-
1058
- /** Add componentType to the reverse index if it contains an entity ID */
1059
- private addToReferencingIndex(componentType: EntityId<any>, archetype: Archetype): void {
1060
- const detailedType = getDetailedIdType(componentType);
1061
- let entityId: EntityId | undefined;
1062
-
1063
- if (detailedType.type === "entity") {
1064
- entityId = componentType as EntityId;
1065
- } else if (detailedType.type === "entity-relation") {
1066
- entityId = detailedType.targetId;
1067
- }
1068
-
1069
- if (entityId !== undefined) {
1070
- let refs = this.entityToReferencingArchetypes.get(entityId);
1071
- if (!refs) {
1072
- refs = new Set();
1073
- this.entityToReferencingArchetypes.set(entityId, refs);
1074
- }
1075
- refs.add(archetype);
1076
- }
1077
- }
1078
-
1079
- /** Remove componentType from the reverse index */
1080
- private removeFromReferencingIndex(componentType: EntityId<any>, archetype: Archetype): void {
1081
- const detailedType = getDetailedIdType(componentType);
1082
- let entityId: EntityId | undefined;
1083
-
1084
- if (detailedType.type === "entity") {
1085
- entityId = componentType as EntityId;
1086
- } else if (detailedType.type === "entity-relation") {
1087
- entityId = detailedType.targetId;
1088
- }
1089
-
1090
- if (entityId !== undefined) {
1091
- const refs = this.entityToReferencingArchetypes.get(entityId);
1092
- if (refs) {
1093
- refs.delete(archetype);
1094
- if (refs.size === 0) {
1095
- this.entityToReferencingArchetypes.delete(entityId);
1096
- }
1097
- }
1098
- }
1099
- }
1100
-
1101
- private createNewArchetype(componentTypes: EntityId<any>[]): Archetype {
1102
- const newArchetype = new Archetype(componentTypes, this.dontFragmentStore);
1103
- this.archetypes.push(newArchetype);
1104
-
1105
- for (const componentType of componentTypes) {
1106
- let archetypes = this.archetypesByComponent.get(componentType);
1107
- if (!archetypes) {
1108
- archetypes = new Set();
1109
- this.archetypesByComponent.set(componentType, archetypes);
1110
- }
1111
- archetypes.add(newArchetype);
1112
-
1113
- // Update reverse index
1114
- this.addToReferencingIndex(componentType, newArchetype);
1115
- }
1116
-
1117
- this.queryRegistry.onNewArchetype(newArchetype);
1118
- this.updateArchetypeHookMatches(newArchetype);
1119
-
1120
- return newArchetype;
1121
- }
1122
-
1123
- private updateArchetypeHookMatches(archetype: Archetype): void {
1124
- for (const entry of this.hooks) {
1125
- if (this.archetypeMatchesHook(archetype, entry)) {
1126
- archetype.matchingMultiHooks.add(entry);
1127
- if (entry.matchedArchetypes) {
1128
- entry.matchedArchetypes.add(archetype);
1129
- }
1130
- }
1131
- }
1132
- }
1133
-
1134
- private archetypeMatchesHook(archetype: Archetype, entry: LifecycleHookEntry): boolean {
1135
- return (
1136
- entry.requiredComponents.every((c: EntityId<any>) => {
1137
- if (isWildcardRelationId(c)) {
1138
- if (isDontFragmentWildcard(c)) return true;
1139
- const componentId = getComponentIdFromRelationId(c);
1140
- return componentId !== undefined && archetype.hasRelationWithComponentId(componentId);
1141
- }
1142
- return archetype.componentTypeSet.has(c) || isDontFragmentRelation(c);
1143
- }) && matchesFilter(archetype, entry.filter)
1144
- );
1127
+ return this.archetypeManager.ensureArchetype(componentTypes);
1145
1128
  }
1146
1129
 
1147
1130
  private cleanupArchetypesReferencingEntity(entityId: EntityId): void {
1148
- const refs = this.entityToReferencingArchetypes.get(entityId);
1149
- if (!refs) return;
1150
-
1151
- for (const archetype of refs) {
1152
- if (archetype.getEntities().length === 0) {
1153
- this.removeArchetype(archetype);
1154
- }
1155
- }
1156
- // removeArchetype already cleans up the reverse index entries
1157
- this.entityToReferencingArchetypes.delete(entityId);
1131
+ this.archetypeManager.cleanupArchetypesReferencingEntity(entityId);
1158
1132
  }
1159
1133
 
1160
- private removeArchetype(archetype: Archetype): void {
1161
- const index = this.archetypes.indexOf(archetype);
1162
- if (index !== -1) {
1163
- // swap-and-pop: O(1) removal
1164
- const last = this.archetypes[this.archetypes.length - 1]!;
1165
- this.archetypes[index] = last;
1166
- this.archetypes.pop();
1167
- }
1168
-
1169
- this.archetypeBySignature.delete(this.createArchetypeSignature(archetype.componentTypes));
1170
-
1171
- for (const componentType of archetype.componentTypes) {
1172
- const archetypes = this.archetypesByComponent.get(componentType);
1173
- if (archetypes) {
1174
- archetypes.delete(archetype);
1175
- if (archetypes.size === 0) {
1176
- this.archetypesByComponent.delete(componentType);
1177
- }
1178
- }
1179
-
1180
- // Clean up reverse index
1181
- this.removeFromReferencingIndex(componentType, archetype);
1182
- }
1183
-
1184
- this.queryRegistry.onArchetypeRemoved(archetype);
1134
+ private archetypeMatchesHook(archetype: Archetype, entry: LifecycleHookEntry): boolean {
1135
+ return this.archetypeManager.archetypeMatchesHook(archetype, entry);
1185
1136
  }
1186
1137
 
1187
1138
  /**
@@ -1205,7 +1156,28 @@ export class World {
1205
1156
  * const savedData = JSON.parse(localStorage.getItem('save'));
1206
1157
  * const newWorld = new World(savedData);
1207
1158
  */
1159
+ // Thin delegator to the extracted CommandExecutor for cascade paths (destroy* methods).
1160
+ private removeComponentImmediate(entityId: EntityId, componentType: EntityId<any>, targetEntityId: EntityId): void {
1161
+ this.commandExecutor.removeComponentImmediate(entityId, componentType, targetEntityId);
1162
+ }
1163
+
1164
+ private createHooksContext(): HooksContext {
1165
+ // The executor owns the current HooksContext; expose for any remaining internal use.
1166
+ return (
1167
+ (this.commandExecutor as any).getHooksContext?.() ?? {
1168
+ multiHooks: this.hooks,
1169
+ has: (eid, ct) => this.has(eid, ct),
1170
+ get: (eid, ct) => this.get(eid, ct),
1171
+ getOptional: (eid, ct) => this.getOptional(eid, ct),
1172
+ }
1173
+ );
1174
+ }
1175
+
1208
1176
  serialize(): SerializedWorld {
1209
- return serializeWorld(this.archetypes, this.componentEntities, this.entityIdManager);
1177
+ return serializeWorld(
1178
+ this.archetypeManager.archetypes as Archetype[],
1179
+ this.componentEntities,
1180
+ this.entityIdManager,
1181
+ );
1210
1182
  }
1211
1183
  }