@codehz/ecs 0.9.0 → 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.
- package/README.md +13 -1
- package/dist/builder.d.mts +59 -44
- 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 +1136 -887
- package/dist/world.mjs.map +1 -1
- package/examples/spatial-grid.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/component/singleton.test.ts +76 -34
- package/src/__tests__/serialization/bounds.test.ts +2 -3
- package/src/__tests__/world/component-management.test.ts +6 -5
- package/src/index.ts +2 -0
- package/src/world/archetype-manager.ts +283 -0
- package/src/world/command-executor.ts +258 -0
- package/src/world/debug-stats.ts +147 -0
- package/src/world/operations.ts +88 -0
- package/src/world/singleton.ts +51 -0
- package/src/world/world.ts +178 -573
package/src/world/world.ts
CHANGED
|
@@ -1,26 +1,19 @@
|
|
|
1
|
-
import { Archetype } from "../archetype/archetype";
|
|
1
|
+
import type { Archetype } from "../archetype/archetype";
|
|
2
2
|
import { SparseStoreImpl } from "../archetype/store";
|
|
3
|
-
import { CommandBuffer
|
|
4
|
-
import { ComponentChangeset } from "../commands/changeset";
|
|
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
|
-
isEntityRelation,
|
|
17
|
-
isExclusiveComponent,
|
|
18
12
|
isSparseRelation,
|
|
19
|
-
isSparseWildcard,
|
|
20
13
|
isWildcardRelationId,
|
|
21
14
|
relation,
|
|
22
15
|
} from "../entity";
|
|
23
|
-
import {
|
|
16
|
+
import { serializeQueryFilter, type QueryFilter } from "../query/filter";
|
|
24
17
|
import type { Query } from "../query/query";
|
|
25
18
|
import { QueryRegistry } from "../query/registry";
|
|
26
19
|
import type { SerializedWorld } from "../storage/serialization";
|
|
@@ -34,30 +27,25 @@ import type {
|
|
|
34
27
|
SyncDebugStats,
|
|
35
28
|
} from "../types";
|
|
36
29
|
import { isOptionalEntityId } from "../types";
|
|
37
|
-
import {
|
|
30
|
+
import { ArchetypeManager } from "./archetype-manager";
|
|
38
31
|
import { EntityBuilder } from "./builder";
|
|
39
|
-
import {
|
|
40
|
-
|
|
41
|
-
filterRegularComponentTypes,
|
|
42
|
-
maybeRemoveWildcardMarker,
|
|
43
|
-
processCommands,
|
|
44
|
-
removeMatchingRelations,
|
|
45
|
-
type CommandProcessorContext,
|
|
46
|
-
} from "./commands";
|
|
32
|
+
import { CommandExecutor, type CommandExecutorContext } from "./command-executor";
|
|
33
|
+
import { DebugStatsManager } from "./debug-stats";
|
|
47
34
|
import {
|
|
48
35
|
collectMultiHookComponents,
|
|
49
|
-
debugHookExecutionCounter,
|
|
50
36
|
triggerLifecycleHooks,
|
|
51
37
|
triggerRemoveHooksForEntityDeletion,
|
|
52
38
|
type HooksContext,
|
|
53
39
|
} from "./hooks";
|
|
54
40
|
import {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
} from "./
|
|
41
|
+
assertEntityExists,
|
|
42
|
+
assertSetComponentTypeValid,
|
|
43
|
+
resolveRemoveOperation,
|
|
44
|
+
resolveSetOperation,
|
|
45
|
+
} from "./operations";
|
|
46
|
+
import { getEntityReferences, type EntityReferencesMap } from "./references";
|
|
60
47
|
import { deserializeWorld, serializeWorld } from "./serialization";
|
|
48
|
+
import { SingletonHandle } from "./singleton";
|
|
61
49
|
|
|
62
50
|
/**
|
|
63
51
|
* World class for ECS architecture
|
|
@@ -66,52 +54,59 @@ import { deserializeWorld, serializeWorld } from "./serialization";
|
|
|
66
54
|
export class World {
|
|
67
55
|
// Core data structures for entity and archetype management
|
|
68
56
|
private entityIdManager = new EntityIdManager();
|
|
69
|
-
private archetypes: Archetype[] = [];
|
|
70
|
-
private archetypeBySignature = new Map<string, Archetype>();
|
|
71
|
-
private entityToArchetype = new Map<EntityId, Archetype>();
|
|
72
|
-
private archetypesByComponent = new Map<EntityId<any>, Set<Archetype>>();
|
|
73
57
|
private entityReferences: EntityReferencesMap = new Map();
|
|
74
|
-
/** Reverse index: entity ID → set of archetypes whose componentTypes include that entity ID */
|
|
75
|
-
private entityToReferencingArchetypes = new Map<EntityId, Set<Archetype>>();
|
|
76
58
|
/** Sparse relation storage (for components created with `sparse: true`), shared with all Archetype instances */
|
|
77
59
|
private readonly sparseStore = new SparseStoreImpl();
|
|
78
60
|
/** Component entity (singleton) storage */
|
|
79
61
|
private readonly componentEntities = new ComponentEntityStore();
|
|
80
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
|
+
|
|
81
83
|
// Query registry – manages caching, ref counts, and archetype notifications
|
|
82
84
|
private readonly queryRegistry = new QueryRegistry();
|
|
83
85
|
|
|
84
86
|
// Lifecycle hooks (declared before cached contexts that reference them)
|
|
85
87
|
private hooks: Set<LifecycleHookEntry> = new Set();
|
|
86
88
|
|
|
87
|
-
// Debug observability
|
|
88
|
-
private readonly
|
|
89
|
-
|
|
90
|
-
//
|
|
91
|
-
private
|
|
92
|
-
private
|
|
93
|
-
private _debugArchetypesRemoved = 0;
|
|
94
|
-
|
|
95
|
-
// Command execution
|
|
96
|
-
private commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
|
|
97
|
-
|
|
98
|
-
// Reusable instances to reduce per-frame allocations
|
|
99
|
-
private readonly _changeset = new ComponentChangeset();
|
|
100
|
-
private readonly _removeChangeset = new ComponentChangeset();
|
|
101
|
-
/** Cached command processor context to avoid per-entity object allocation */
|
|
102
|
-
private readonly _commandCtx: CommandProcessorContext = {
|
|
103
|
-
sparseStore: this.sparseStore,
|
|
104
|
-
ensureArchetype: (ct) => this.ensureArchetype(ct),
|
|
105
|
-
};
|
|
106
|
-
/** Cached hooks context to avoid per-entity object allocation */
|
|
107
|
-
private readonly _hooksCtx: HooksContext = {
|
|
108
|
-
multiHooks: this.hooks,
|
|
109
|
-
has: (eid, ct) => this.has(eid, ct),
|
|
110
|
-
get: (eid, ct) => this.get(eid, ct),
|
|
111
|
-
getOptional: (eid, ct) => this.getOptional(eid, ct),
|
|
112
|
-
};
|
|
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;
|
|
113
95
|
|
|
114
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
|
+
|
|
115
110
|
if (snapshot && typeof snapshot === "object") {
|
|
116
111
|
deserializeWorld(
|
|
117
112
|
{
|
|
@@ -119,15 +114,34 @@ export class World {
|
|
|
119
114
|
componentEntities: this.componentEntities,
|
|
120
115
|
entityReferences: this.entityReferences,
|
|
121
116
|
ensureArchetype: (ct) => this.ensureArchetype(ct),
|
|
122
|
-
setEntityToArchetype: (eid, arch) => this.entityToArchetype.set(eid, arch),
|
|
117
|
+
setEntityToArchetype: (eid, arch) => this.archetypeManager.entityToArchetype.set(eid, arch),
|
|
123
118
|
},
|
|
124
119
|
snapshot,
|
|
125
120
|
);
|
|
126
121
|
}
|
|
127
|
-
}
|
|
128
122
|
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
);
|
|
131
145
|
}
|
|
132
146
|
|
|
133
147
|
/**
|
|
@@ -255,71 +269,6 @@ export class World {
|
|
|
255
269
|
return this.entityToArchetype.has(entityId);
|
|
256
270
|
}
|
|
257
271
|
|
|
258
|
-
private assertEntityExists(entityId: EntityId, label: "Entity" | "Component entity"): void {
|
|
259
|
-
if (!this.exists(entityId)) {
|
|
260
|
-
throw new Error(`${label} ${entityId} does not exist`);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
private assertComponentTypeValid(componentType: EntityId): void {
|
|
265
|
-
const detailedType = getDetailedIdType(componentType);
|
|
266
|
-
if (detailedType.type === "invalid") {
|
|
267
|
-
throw new Error(`Invalid component type: ${componentType}`);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
private assertSetComponentTypeValid(componentType: EntityId): void {
|
|
272
|
-
const detailedType = getDetailedIdType(componentType);
|
|
273
|
-
if (detailedType.type === "invalid") {
|
|
274
|
-
throw new Error(`Invalid component type: ${componentType}`);
|
|
275
|
-
}
|
|
276
|
-
if (detailedType.type === "wildcard-relation") {
|
|
277
|
-
throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
private resolveSetOperation(
|
|
282
|
-
entityId: EntityId | ComponentId,
|
|
283
|
-
componentTypeOrComponent?: EntityId | any,
|
|
284
|
-
maybeComponent?: any,
|
|
285
|
-
): { entityId: EntityId; componentType: EntityId; component: any } {
|
|
286
|
-
// Handle singleton component overload: set(componentId, data)
|
|
287
|
-
if (maybeComponent === undefined && componentTypeOrComponent !== undefined) {
|
|
288
|
-
const detailedType = getDetailedIdType(entityId);
|
|
289
|
-
if (detailedType.type === "component" || detailedType.type === "component-relation") {
|
|
290
|
-
const componentId = entityId as ComponentId;
|
|
291
|
-
this.assertEntityExists(componentId, "Component entity");
|
|
292
|
-
this.assertSetComponentTypeValid(componentId);
|
|
293
|
-
return { entityId: componentId, componentType: componentId, component: componentTypeOrComponent };
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const targetEntityId = entityId as EntityId;
|
|
298
|
-
const componentType = componentTypeOrComponent as EntityId;
|
|
299
|
-
this.assertEntityExists(targetEntityId, "Entity");
|
|
300
|
-
this.assertSetComponentTypeValid(componentType);
|
|
301
|
-
|
|
302
|
-
return { entityId: targetEntityId, componentType, component: maybeComponent };
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
private resolveRemoveOperation<T>(
|
|
306
|
-
entityId: EntityId | ComponentId,
|
|
307
|
-
componentType?: EntityId<T>,
|
|
308
|
-
): { entityId: EntityId; componentType: EntityId } {
|
|
309
|
-
// Handle singleton component overload: remove(componentId)
|
|
310
|
-
if (componentType === undefined) {
|
|
311
|
-
const componentId = entityId as ComponentId<T>;
|
|
312
|
-
this.assertEntityExists(componentId, "Component entity");
|
|
313
|
-
return { entityId: componentId, componentType: componentId };
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const targetEntityId = entityId as EntityId;
|
|
317
|
-
this.assertEntityExists(targetEntityId, "Entity");
|
|
318
|
-
this.assertComponentTypeValid(componentType);
|
|
319
|
-
|
|
320
|
-
return { entityId: targetEntityId, componentType };
|
|
321
|
-
}
|
|
322
|
-
|
|
323
272
|
/**
|
|
324
273
|
* Adds or updates a component on an entity (or marks void component as present).
|
|
325
274
|
* The change is buffered and takes effect after calling `world.sync()`.
|
|
@@ -331,27 +280,23 @@ export class World {
|
|
|
331
280
|
* @overload set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void
|
|
332
281
|
* Adds or updates a component with data on the entity
|
|
333
282
|
*
|
|
334
|
-
* @overload set<T>(componentId: ComponentId<T>, component: NoInfer<T>): void
|
|
335
|
-
* Adds or updates a singleton component (shorthand for set(componentId, componentId, component))
|
|
336
|
-
*
|
|
337
283
|
* @throws {Error} If the entity does not exist
|
|
338
284
|
* @throws {Error} If the component type is invalid or is a wildcard relation
|
|
339
285
|
*
|
|
340
286
|
* @example
|
|
341
287
|
* world.set(entity, Position, { x: 10, y: 20 });
|
|
342
288
|
* world.set(entity, Marker); // void component
|
|
343
|
-
* world.set(
|
|
289
|
+
* world.singleton(GlobalConfig).set({ debug: true }); // singleton component
|
|
344
290
|
* world.sync(); // Apply changes
|
|
345
291
|
*/
|
|
346
292
|
set(entityId: EntityId, componentType: EntityId<void>): void;
|
|
347
293
|
set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void;
|
|
348
|
-
set<T>(componentId: ComponentId<T>, component: NoInfer<T>): void;
|
|
349
294
|
set(entityId: EntityId | ComponentId, componentTypeOrComponent?: EntityId | any, maybeComponent?: any): void {
|
|
350
295
|
const {
|
|
351
296
|
entityId: targetEntityId,
|
|
352
297
|
componentType,
|
|
353
298
|
component,
|
|
354
|
-
} =
|
|
299
|
+
} = resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent, (id) => this.exists(id));
|
|
355
300
|
this.commandBuffer.set(targetEntityId, componentType, component);
|
|
356
301
|
}
|
|
357
302
|
|
|
@@ -381,9 +326,10 @@ export class World {
|
|
|
381
326
|
remove<T>(componentId: ComponentId<T>): void;
|
|
382
327
|
remove<T>(entityId: EntityId, componentType: EntityId<T>): void;
|
|
383
328
|
remove<T>(entityId: EntityId | ComponentId, componentType?: EntityId<T>): void {
|
|
384
|
-
const { entityId: targetEntityId, componentType: targetComponentType } =
|
|
329
|
+
const { entityId: targetEntityId, componentType: targetComponentType } = resolveRemoveOperation(
|
|
385
330
|
entityId,
|
|
386
331
|
componentType,
|
|
332
|
+
(id) => this.exists(id),
|
|
387
333
|
);
|
|
388
334
|
this.commandBuffer.remove(targetEntityId, targetComponentType);
|
|
389
335
|
}
|
|
@@ -403,6 +349,32 @@ export class World {
|
|
|
403
349
|
this.commandBuffer.delete(entityId);
|
|
404
350
|
}
|
|
405
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
|
+
|
|
406
378
|
/**
|
|
407
379
|
* Checks if a specific **component** is present on an entity.
|
|
408
380
|
*
|
|
@@ -612,7 +584,7 @@ export class World {
|
|
|
612
584
|
entityId: EntityId,
|
|
613
585
|
relationComp: ComponentId<T>,
|
|
614
586
|
): [target: EntityId<unknown>, data: T | undefined][] {
|
|
615
|
-
|
|
587
|
+
assertEntityExists(entityId, "Entity", (id) => this.exists(id));
|
|
616
588
|
|
|
617
589
|
const wildcard = relation(relationComp, "*") as WildcardRelationId<T>;
|
|
618
590
|
|
|
@@ -659,7 +631,7 @@ export class World {
|
|
|
659
631
|
* given base component.
|
|
660
632
|
*/
|
|
661
633
|
hasRelation(entityId: EntityId, relationComp: ComponentId<any>, targetId?: EntityId): boolean {
|
|
662
|
-
|
|
634
|
+
assertEntityExists(entityId, "Entity", (id) => this.exists(id));
|
|
663
635
|
|
|
664
636
|
if (targetId !== undefined) {
|
|
665
637
|
const specific = relation(relationComp, targetId);
|
|
@@ -675,7 +647,7 @@ export class World {
|
|
|
675
647
|
* Returns the number of relations of the given base component held by the entity.
|
|
676
648
|
*/
|
|
677
649
|
countRelations(entityId: EntityId, relationComp: ComponentId<any>): number {
|
|
678
|
-
|
|
650
|
+
assertEntityExists(entityId, "Entity", (id) => this.exists(id));
|
|
679
651
|
const targets = this.getRelationTargets(entityId, relationComp);
|
|
680
652
|
return targets.length;
|
|
681
653
|
}
|
|
@@ -922,88 +894,7 @@ export class World {
|
|
|
922
894
|
* This is intended for development/debugging and leak detection.
|
|
923
895
|
*/
|
|
924
896
|
createDebugStatsCollector(callback: (stats: SyncDebugStats) => void): DebugStatsCollector {
|
|
925
|
-
this.
|
|
926
|
-
|
|
927
|
-
return {
|
|
928
|
-
[Symbol.dispose]: () => {
|
|
929
|
-
this._debugCollectors.delete(callback);
|
|
930
|
-
},
|
|
931
|
-
};
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
private _resetDebugActivityCounters(): void {
|
|
935
|
-
this._debugMigrations = 0;
|
|
936
|
-
this._debugArchetypesCreated = 0;
|
|
937
|
-
this._debugArchetypesRemoved = 0;
|
|
938
|
-
debugHookExecutionCounter.value = 0;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
private _deliverDebugStats(timings: {
|
|
942
|
-
syncStart: number;
|
|
943
|
-
syncEnd: number;
|
|
944
|
-
commandBufferStart: number;
|
|
945
|
-
commandBufferEnd: number;
|
|
946
|
-
commandIterations: number;
|
|
947
|
-
}): void {
|
|
948
|
-
// Build structural counts (post-sync)
|
|
949
|
-
// Note: singletons (component-as-entity) are not included in the main archetype map.
|
|
950
|
-
// For debug purposes the dominant number is regular entities; we keep it simple here.
|
|
951
|
-
const entityCount = this.entityToArchetype.size;
|
|
952
|
-
let emptyArchetypes = 0;
|
|
953
|
-
for (const arch of this.archetypes) {
|
|
954
|
-
if (arch.size === 0) emptyArchetypes++;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
let archetypesByComponentSize = 0;
|
|
958
|
-
for (const set of this.archetypesByComponent.values()) {
|
|
959
|
-
archetypesByComponentSize += set.size;
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
const stats: SyncDebugStats = {
|
|
963
|
-
timestamps: {
|
|
964
|
-
syncStart: timings.syncStart,
|
|
965
|
-
syncEnd: timings.syncEnd,
|
|
966
|
-
commandBufferStart: timings.commandBufferStart,
|
|
967
|
-
commandBufferEnd: timings.commandBufferEnd,
|
|
968
|
-
},
|
|
969
|
-
commandIterations: timings.commandIterations,
|
|
970
|
-
|
|
971
|
-
entities: {
|
|
972
|
-
total: entityCount,
|
|
973
|
-
freelistSize: this.entityIdManager.getFreelistSize(),
|
|
974
|
-
nextId: this.entityIdManager.getNextId(),
|
|
975
|
-
},
|
|
976
|
-
archetypes: {
|
|
977
|
-
total: this.archetypes.length,
|
|
978
|
-
empty: emptyArchetypes,
|
|
979
|
-
},
|
|
980
|
-
queries: {
|
|
981
|
-
cached: (this.queryRegistry as any).cache?.size ?? 0,
|
|
982
|
-
registered: (this.queryRegistry as any).queries?.size ?? 0,
|
|
983
|
-
},
|
|
984
|
-
hooks: {
|
|
985
|
-
total: this.hooks.size,
|
|
986
|
-
},
|
|
987
|
-
indices: {
|
|
988
|
-
entityReferences: this.entityReferences.size,
|
|
989
|
-
entityToReferencingArchetypes: this.entityToReferencingArchetypes.size,
|
|
990
|
-
archetypesByComponent: archetypesByComponentSize,
|
|
991
|
-
},
|
|
992
|
-
activity: {
|
|
993
|
-
migrations: this._debugMigrations,
|
|
994
|
-
hooksExecuted: debugHookExecutionCounter.value,
|
|
995
|
-
archetypesCreated: this._debugArchetypesCreated,
|
|
996
|
-
archetypesRemoved: this._debugArchetypesRemoved,
|
|
997
|
-
},
|
|
998
|
-
};
|
|
999
|
-
|
|
1000
|
-
for (const cb of this._debugCollectors) {
|
|
1001
|
-
try {
|
|
1002
|
-
cb(stats);
|
|
1003
|
-
} catch {
|
|
1004
|
-
// Intentionally ignore user callback errors
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
897
|
+
return this.debugStats.createCollector(callback);
|
|
1007
898
|
}
|
|
1008
899
|
|
|
1009
900
|
/**
|
|
@@ -1017,29 +908,56 @@ export class World {
|
|
|
1017
908
|
* world.sync(); // Apply all buffered changes
|
|
1018
909
|
*/
|
|
1019
910
|
sync(): void {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
if (hasCollectors) {
|
|
1025
|
-
this._resetDebugActivityCounters();
|
|
911
|
+
if (!this.debugStats.hasActiveCollectors()) {
|
|
912
|
+
// Fast path: no debug collectors, skip all timing and stats work
|
|
913
|
+
this.commandBuffer.execute();
|
|
914
|
+
return;
|
|
1026
915
|
}
|
|
1027
916
|
|
|
1028
|
-
|
|
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();
|
|
1029
922
|
const commandIterations = this.commandBuffer.execute();
|
|
1030
|
-
const commandBufferEnd =
|
|
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
|
+
}
|
|
1031
933
|
|
|
1032
|
-
|
|
934
|
+
let archetypesByComponentSize = 0;
|
|
935
|
+
for (const set of this.archetypesByComponent.values()) {
|
|
936
|
+
archetypesByComponentSize += set.size;
|
|
937
|
+
}
|
|
1033
938
|
|
|
1034
|
-
|
|
1035
|
-
|
|
939
|
+
this.debugStats.deliver(
|
|
940
|
+
{
|
|
1036
941
|
syncStart,
|
|
1037
942
|
syncEnd,
|
|
1038
943
|
commandBufferStart,
|
|
1039
944
|
commandBufferEnd,
|
|
1040
945
|
commandIterations,
|
|
1041
|
-
}
|
|
1042
|
-
|
|
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
|
+
);
|
|
1043
961
|
}
|
|
1044
962
|
|
|
1045
963
|
/**
|
|
@@ -1076,7 +994,7 @@ export class World {
|
|
|
1076
994
|
createQuery(componentTypes: EntityId<any>[], filter: QueryFilter = {}): Query {
|
|
1077
995
|
const sortedTypes = normalizeComponentTypes(componentTypes);
|
|
1078
996
|
const filterKey = serializeQueryFilter(filter);
|
|
1079
|
-
const key = `${
|
|
997
|
+
const key = `${sortedTypes.join(",")}${filterKey ? `|${filterKey}` : ""}`;
|
|
1080
998
|
return this.queryRegistry.getOrCreate(this, sortedTypes, key, filter);
|
|
1081
999
|
}
|
|
1082
1000
|
|
|
@@ -1146,71 +1064,7 @@ export class World {
|
|
|
1146
1064
|
* @internal
|
|
1147
1065
|
*/
|
|
1148
1066
|
getMatchingArchetypes(componentTypes: EntityId<any>[]): Archetype[] {
|
|
1149
|
-
|
|
1150
|
-
return [...this.archetypes];
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
const regularComponents: EntityId<any>[] = [];
|
|
1154
|
-
const wildcardRelations: { componentId: ComponentId<any>; relationId: EntityId<any> }[] = [];
|
|
1155
|
-
|
|
1156
|
-
for (const componentType of componentTypes) {
|
|
1157
|
-
if (isWildcardRelationId(componentType)) {
|
|
1158
|
-
const componentId = getComponentIdFromRelationId(componentType);
|
|
1159
|
-
if (componentId !== undefined) {
|
|
1160
|
-
wildcardRelations.push({ componentId, relationId: componentType });
|
|
1161
|
-
}
|
|
1162
|
-
} else {
|
|
1163
|
-
regularComponents.push(componentType);
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
let matchingArchetypes = this.getArchetypesWithComponents(regularComponents);
|
|
1168
|
-
|
|
1169
|
-
for (const { componentId, relationId } of wildcardRelations) {
|
|
1170
|
-
const markerSet = this.archetypesByComponent.get(relationId);
|
|
1171
|
-
const archetypesWithMarker = markerSet ? Array.from(markerSet) : [];
|
|
1172
|
-
matchingArchetypes =
|
|
1173
|
-
matchingArchetypes.length === 0
|
|
1174
|
-
? archetypesWithMarker
|
|
1175
|
-
: matchingArchetypes.filter((a) => markerSet?.has(a) || a.hasRelationWithComponentId(componentId));
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
return matchingArchetypes;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
private getArchetypesWithComponents(componentTypes: EntityId<any>[]): Archetype[] {
|
|
1182
|
-
if (componentTypes.length === 0) return [...this.archetypes];
|
|
1183
|
-
if (componentTypes.length === 1) {
|
|
1184
|
-
const set = this.archetypesByComponent.get(componentTypes[0]!);
|
|
1185
|
-
return set ? Array.from(set) : [];
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
// Sort by Set size, intersect starting from the smallest
|
|
1189
|
-
const sets = componentTypes
|
|
1190
|
-
.map((type) => this.archetypesByComponent.get(type))
|
|
1191
|
-
.filter((s): s is Set<Archetype> => s !== undefined && s.size > 0)
|
|
1192
|
-
.sort((a, b) => a.size - b.size);
|
|
1193
|
-
|
|
1194
|
-
if (sets.length === 0) return [];
|
|
1195
|
-
if (sets.length < componentTypes.length) return []; // One component has no matching archetypes
|
|
1196
|
-
|
|
1197
|
-
const smallest = sets[0]!;
|
|
1198
|
-
|
|
1199
|
-
// 2-component fast path
|
|
1200
|
-
if (sets.length === 2) {
|
|
1201
|
-
const other = sets[1]!;
|
|
1202
|
-
return Array.from(smallest).filter((a) => other.has(a));
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// Multi-component intersection
|
|
1206
|
-
let result = new Set(smallest);
|
|
1207
|
-
for (let i = 1; i < sets.length; i++) {
|
|
1208
|
-
for (const item of result) {
|
|
1209
|
-
if (!sets[i]!.has(item)) result.delete(item);
|
|
1210
|
-
}
|
|
1211
|
-
if (result.size === 0) return [];
|
|
1212
|
-
}
|
|
1213
|
-
return Array.from(result);
|
|
1067
|
+
return this.archetypeManager.getMatchingArchetypes(componentTypes);
|
|
1214
1068
|
}
|
|
1215
1069
|
|
|
1216
1070
|
/**
|
|
@@ -1268,287 +1122,17 @@ export class World {
|
|
|
1268
1122
|
}
|
|
1269
1123
|
}
|
|
1270
1124
|
|
|
1271
|
-
|
|
1272
|
-
this._changeset.clear();
|
|
1273
|
-
|
|
1274
|
-
// 1. Route: component entities use flat-map storage
|
|
1275
|
-
if (this.componentEntities.exists(entityId)) {
|
|
1276
|
-
this.componentEntities.executeCommands(entityId, commands);
|
|
1277
|
-
return;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
// 2. Route: destroy uses fast path
|
|
1281
|
-
if (commands.some((cmd) => cmd.type === "destroy")) {
|
|
1282
|
-
this.destroyEntityImmediate(entityId);
|
|
1283
|
-
return;
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
// 3. Apply structural changes
|
|
1287
|
-
this.applyEntityCommands(entityId, commands);
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
private applyEntityCommands(entityId: EntityId, commands: Command[]): void {
|
|
1291
|
-
const currentArchetype = this.entityToArchetype.get(entityId);
|
|
1292
|
-
if (!currentArchetype) return;
|
|
1293
|
-
|
|
1294
|
-
const changeset = this._changeset;
|
|
1295
|
-
processCommands(entityId, currentArchetype, commands, changeset, (eid, arch, compId) => {
|
|
1296
|
-
if (isExclusiveComponent(compId)) {
|
|
1297
|
-
removeMatchingRelations(eid, arch, compId, changeset);
|
|
1298
|
-
}
|
|
1299
|
-
});
|
|
1300
|
-
|
|
1301
|
-
const hasStructuralChange = changeset.removes.size > 0 || changeset.adds.size > 0;
|
|
1302
|
-
|
|
1303
|
-
if (this.hooks.size === 0) {
|
|
1304
|
-
// Fast path: no hooks, skip removedComponents map allocation and hook triggering
|
|
1305
|
-
const newArchetype = applyChangeset(
|
|
1306
|
-
this._commandCtx,
|
|
1307
|
-
entityId,
|
|
1308
|
-
currentArchetype,
|
|
1309
|
-
changeset,
|
|
1310
|
-
this.entityToArchetype,
|
|
1311
|
-
null,
|
|
1312
|
-
);
|
|
1313
|
-
if (hasStructuralChange) {
|
|
1314
|
-
this.updateEntityReferences(entityId, changeset);
|
|
1315
|
-
}
|
|
1316
|
-
if (this._debugCollectors.size > 0 && newArchetype !== currentArchetype) {
|
|
1317
|
-
this._debugMigrations++;
|
|
1318
|
-
}
|
|
1319
|
-
return;
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
const removedComponents = new Map<EntityId<any>, any>();
|
|
1323
|
-
const newArchetype = applyChangeset(
|
|
1324
|
-
this._commandCtx,
|
|
1325
|
-
entityId,
|
|
1326
|
-
currentArchetype,
|
|
1327
|
-
changeset,
|
|
1328
|
-
this.entityToArchetype,
|
|
1329
|
-
removedComponents,
|
|
1330
|
-
);
|
|
1331
|
-
|
|
1332
|
-
if (hasStructuralChange) {
|
|
1333
|
-
this.updateEntityReferences(entityId, changeset);
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
if (this._debugCollectors.size > 0 && newArchetype !== currentArchetype) {
|
|
1337
|
-
this._debugMigrations++;
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
triggerLifecycleHooks(
|
|
1341
|
-
this.createHooksContext(),
|
|
1342
|
-
entityId,
|
|
1343
|
-
changeset.adds,
|
|
1344
|
-
removedComponents,
|
|
1345
|
-
currentArchetype,
|
|
1346
|
-
newArchetype,
|
|
1347
|
-
);
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
private createHooksContext(): HooksContext {
|
|
1351
|
-
return this._hooksCtx;
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
private removeComponentImmediate(entityId: EntityId, componentType: EntityId<any>, targetEntityId: EntityId): void {
|
|
1355
|
-
const sourceArchetype = this.entityToArchetype.get(entityId);
|
|
1356
|
-
if (!sourceArchetype) return;
|
|
1357
|
-
|
|
1358
|
-
const changeset = this._removeChangeset;
|
|
1359
|
-
changeset.clear();
|
|
1360
|
-
changeset.delete(componentType);
|
|
1361
|
-
maybeRemoveWildcardMarker(
|
|
1362
|
-
entityId,
|
|
1363
|
-
sourceArchetype,
|
|
1364
|
-
componentType,
|
|
1365
|
-
getComponentIdFromRelationId(componentType),
|
|
1366
|
-
changeset,
|
|
1367
|
-
);
|
|
1368
|
-
|
|
1369
|
-
const removedComponent = sourceArchetype.get(entityId, componentType);
|
|
1370
|
-
const newArchetype = applyChangeset(
|
|
1371
|
-
this._commandCtx,
|
|
1372
|
-
entityId,
|
|
1373
|
-
sourceArchetype,
|
|
1374
|
-
changeset,
|
|
1375
|
-
this.entityToArchetype,
|
|
1376
|
-
null,
|
|
1377
|
-
);
|
|
1378
|
-
untrackEntityReference(this.entityReferences, entityId, componentType, targetEntityId);
|
|
1379
|
-
triggerLifecycleHooks(
|
|
1380
|
-
this.createHooksContext(),
|
|
1381
|
-
entityId,
|
|
1382
|
-
new Map(),
|
|
1383
|
-
new Map([[componentType, removedComponent]]),
|
|
1384
|
-
sourceArchetype,
|
|
1385
|
-
newArchetype,
|
|
1386
|
-
);
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
private updateEntityReferences(entityId: EntityId, changeset: ComponentChangeset): void {
|
|
1390
|
-
for (const componentType of changeset.removes) {
|
|
1391
|
-
if (isEntityRelation(componentType)) {
|
|
1392
|
-
const targetId = getTargetIdFromRelationId(componentType)!;
|
|
1393
|
-
untrackEntityReference(this.entityReferences, entityId, componentType, targetId);
|
|
1394
|
-
} else if (componentType >= ENTITY_ID_START) {
|
|
1395
|
-
untrackEntityReference(this.entityReferences, entityId, componentType, componentType);
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
for (const [componentType] of changeset.adds) {
|
|
1400
|
-
if (isEntityRelation(componentType)) {
|
|
1401
|
-
const targetId = getTargetIdFromRelationId(componentType)!;
|
|
1402
|
-
trackEntityReference(this.entityReferences, entityId, componentType, targetId);
|
|
1403
|
-
} else if (componentType >= ENTITY_ID_START) {
|
|
1404
|
-
trackEntityReference(this.entityReferences, entityId, componentType, componentType);
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1125
|
+
// Delegators to the extracted ArchetypeManager (keeps public + internal call sites unchanged).
|
|
1409
1126
|
private ensureArchetype(componentTypes: Iterable<EntityId<any>>): Archetype {
|
|
1410
|
-
|
|
1411
|
-
const sortedTypes = normalizeComponentTypes(regularTypes);
|
|
1412
|
-
const hashKey = this.createArchetypeSignature(sortedTypes);
|
|
1413
|
-
|
|
1414
|
-
return getOrCompute(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
/** Add componentType to the reverse index if it contains an entity ID */
|
|
1418
|
-
private addToReferencingIndex(componentType: EntityId<any>, archetype: Archetype): void {
|
|
1419
|
-
const detailedType = getDetailedIdType(componentType);
|
|
1420
|
-
let entityId: EntityId | undefined;
|
|
1421
|
-
|
|
1422
|
-
if (detailedType.type === "entity") {
|
|
1423
|
-
entityId = componentType as EntityId;
|
|
1424
|
-
} else if (detailedType.type === "entity-relation") {
|
|
1425
|
-
entityId = detailedType.targetId;
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
if (entityId !== undefined) {
|
|
1429
|
-
let refs = this.entityToReferencingArchetypes.get(entityId);
|
|
1430
|
-
if (!refs) {
|
|
1431
|
-
refs = new Set();
|
|
1432
|
-
this.entityToReferencingArchetypes.set(entityId, refs);
|
|
1433
|
-
}
|
|
1434
|
-
refs.add(archetype);
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
/** Remove componentType from the reverse index */
|
|
1439
|
-
private removeFromReferencingIndex(componentType: EntityId<any>, archetype: Archetype): void {
|
|
1440
|
-
const detailedType = getDetailedIdType(componentType);
|
|
1441
|
-
let entityId: EntityId | undefined;
|
|
1442
|
-
|
|
1443
|
-
if (detailedType.type === "entity") {
|
|
1444
|
-
entityId = componentType as EntityId;
|
|
1445
|
-
} else if (detailedType.type === "entity-relation") {
|
|
1446
|
-
entityId = detailedType.targetId;
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
if (entityId !== undefined) {
|
|
1450
|
-
const refs = this.entityToReferencingArchetypes.get(entityId);
|
|
1451
|
-
if (refs) {
|
|
1452
|
-
refs.delete(archetype);
|
|
1453
|
-
if (refs.size === 0) {
|
|
1454
|
-
this.entityToReferencingArchetypes.delete(entityId);
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
private createNewArchetype(componentTypes: EntityId<any>[]): Archetype {
|
|
1461
|
-
const newArchetype = new Archetype(componentTypes, this.sparseStore);
|
|
1462
|
-
this.archetypes.push(newArchetype);
|
|
1463
|
-
|
|
1464
|
-
if (this._debugCollectors.size > 0) {
|
|
1465
|
-
this._debugArchetypesCreated++;
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
for (const componentType of componentTypes) {
|
|
1469
|
-
let archetypes = this.archetypesByComponent.get(componentType);
|
|
1470
|
-
if (!archetypes) {
|
|
1471
|
-
archetypes = new Set();
|
|
1472
|
-
this.archetypesByComponent.set(componentType, archetypes);
|
|
1473
|
-
}
|
|
1474
|
-
archetypes.add(newArchetype);
|
|
1475
|
-
|
|
1476
|
-
// Update reverse index
|
|
1477
|
-
this.addToReferencingIndex(componentType, newArchetype);
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
this.queryRegistry.onNewArchetype(newArchetype);
|
|
1481
|
-
this.updateArchetypeHookMatches(newArchetype);
|
|
1482
|
-
|
|
1483
|
-
return newArchetype;
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
private updateArchetypeHookMatches(archetype: Archetype): void {
|
|
1487
|
-
for (const entry of this.hooks) {
|
|
1488
|
-
if (this.archetypeMatchesHook(archetype, entry)) {
|
|
1489
|
-
archetype.matchingMultiHooks.add(entry);
|
|
1490
|
-
if (entry.matchedArchetypes) {
|
|
1491
|
-
entry.matchedArchetypes.add(archetype);
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
private archetypeMatchesHook(archetype: Archetype, entry: LifecycleHookEntry): boolean {
|
|
1498
|
-
return (
|
|
1499
|
-
entry.requiredComponents.every((c: EntityId<any>) => {
|
|
1500
|
-
if (isWildcardRelationId(c)) {
|
|
1501
|
-
if (isSparseWildcard(c)) return true;
|
|
1502
|
-
const componentId = getComponentIdFromRelationId(c);
|
|
1503
|
-
return componentId !== undefined && archetype.hasRelationWithComponentId(componentId);
|
|
1504
|
-
}
|
|
1505
|
-
return archetype.componentTypeSet.has(c) || isSparseRelation(c);
|
|
1506
|
-
}) && matchesFilter(archetype, entry.filter)
|
|
1507
|
-
);
|
|
1127
|
+
return this.archetypeManager.ensureArchetype(componentTypes);
|
|
1508
1128
|
}
|
|
1509
1129
|
|
|
1510
1130
|
private cleanupArchetypesReferencingEntity(entityId: EntityId): void {
|
|
1511
|
-
|
|
1512
|
-
if (!refs) return;
|
|
1513
|
-
|
|
1514
|
-
for (const archetype of refs) {
|
|
1515
|
-
if (archetype.getEntities().length === 0) {
|
|
1516
|
-
this.removeArchetype(archetype);
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
// removeArchetype already cleans up the reverse index entries
|
|
1520
|
-
this.entityToReferencingArchetypes.delete(entityId);
|
|
1131
|
+
this.archetypeManager.cleanupArchetypesReferencingEntity(entityId);
|
|
1521
1132
|
}
|
|
1522
1133
|
|
|
1523
|
-
private
|
|
1524
|
-
|
|
1525
|
-
if (index !== -1) {
|
|
1526
|
-
// swap-and-pop: O(1) removal
|
|
1527
|
-
const last = this.archetypes[this.archetypes.length - 1]!;
|
|
1528
|
-
this.archetypes[index] = last;
|
|
1529
|
-
this.archetypes.pop();
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
if (this._debugCollectors.size > 0) {
|
|
1533
|
-
this._debugArchetypesRemoved++;
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
this.archetypeBySignature.delete(this.createArchetypeSignature(archetype.componentTypes));
|
|
1537
|
-
|
|
1538
|
-
for (const componentType of archetype.componentTypes) {
|
|
1539
|
-
const archetypes = this.archetypesByComponent.get(componentType);
|
|
1540
|
-
if (archetypes) {
|
|
1541
|
-
archetypes.delete(archetype);
|
|
1542
|
-
if (archetypes.size === 0) {
|
|
1543
|
-
this.archetypesByComponent.delete(componentType);
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
// Clean up reverse index
|
|
1548
|
-
this.removeFromReferencingIndex(componentType, archetype);
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
this.queryRegistry.onArchetypeRemoved(archetype);
|
|
1134
|
+
private archetypeMatchesHook(archetype: Archetype, entry: LifecycleHookEntry): boolean {
|
|
1135
|
+
return this.archetypeManager.archetypeMatchesHook(archetype, entry);
|
|
1552
1136
|
}
|
|
1553
1137
|
|
|
1554
1138
|
/**
|
|
@@ -1572,7 +1156,28 @@ export class World {
|
|
|
1572
1156
|
* const savedData = JSON.parse(localStorage.getItem('save'));
|
|
1573
1157
|
* const newWorld = new World(savedData);
|
|
1574
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
|
+
|
|
1575
1176
|
serialize(): SerializedWorld {
|
|
1576
|
-
return serializeWorld(
|
|
1177
|
+
return serializeWorld(
|
|
1178
|
+
this.archetypeManager.archetypes as Archetype[],
|
|
1179
|
+
this.componentEntities,
|
|
1180
|
+
this.entityIdManager,
|
|
1181
|
+
);
|
|
1577
1182
|
}
|
|
1578
1183
|
}
|